MM-53855 - Handle serverError undefined or null (#24630)

* handle serverError undefined or null

* exclude bots from list and search for adding users to roles

* add import, fix lint

* add unit tests

* update test, team/channel admin based on membership, not user roles

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Scott Bishel
2023-10-13 09:57:17 -06:00
коммит произвёл GitHub
родитель bd982356a7
Коммит f4b4afdae6
7 изменённых файлов: 869 добавлений и 17 удалений

Просмотреть файл

@@ -1,6 +1,395 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`admin_console/add_users_to_role_modal should match snapshot 1`] = `
exports[`admin_console/add_users_to_role_modal search should not include bot user 1`] = `
<Modal
animation={true}
autoFocus={true}
backdrop={true}
bsClass="modal"
dialogClassName="a11y__modal more-modal more-direct-channels"
dialogComponentClass={[Function]}
enforceFocus={true}
id="addUsersToRoleModal"
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[Function]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
show={true}
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
>
<ModalTitle
bsClass="modal-title"
componentClass="h1"
>
<MemoizedFormattedMessage
defaultMessage="Add users to {roleName}"
id="add_users_to_role.title"
values={
Object {
"roleName": <strong>
<Memo(MemoizedFormattedMessage)
defaultMessage="role_name"
id="admin.permissions.roles.role_name.name"
/>
</strong>,
}
}
/>
</ModalTitle>
</ModalHeader>
<ModalBody
bsClass="modal-body"
componentClass="div"
>
<MultiSelect
ariaLabelRenderer={[Function]}
buttonSubmitLoadingText="Adding..."
buttonSubmitText="Add"
focusOnLoad={true}
handleAdd={[Function]}
handleDelete={[Function]}
handleInput={[Function]}
handlePageChange={[Function]}
handleSubmit={[Function]}
key="addUsersToRoleKey"
loading={true}
maxValues={20}
numRemainingText={
<div
id="numPeopleRemaining"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Use ↑↓ to browse, ↵ to select. You can add {num, number} more {num, plural, one {person} other {people}}. "
id="multiselect.numPeopleRemaining"
values={
Object {
"num": 20,
}
}
/>
</div>
}
optionRenderer={[Function]}
options={
Array [
Object {
"auth_service": "",
"bot_description": "",
"create_at": 0,
"delete_at": 0,
"email": "",
"first_name": "",
"id": "user_id",
"is_bot": false,
"label": "some-user",
"last_activity_at": 0,
"last_name": "",
"last_password_update": 0,
"last_picture_update": 0,
"locale": "",
"mfa_active": false,
"nickname": "",
"notify_props": Object {
"calls_desktop_sound": "true",
"channel": "false",
"comments": "never",
"desktop": "default",
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
"push_status": "offline",
},
"password": "",
"position": "",
"props": Object {},
"roles": "",
"terms_of_service_create_at": 0,
"terms_of_service_id": "",
"update_at": 0,
"username": "some-user",
"value": "user_id",
},
]
}
perPage={50}
placeholderText="Search and add members"
saveButtonPosition="top"
saving={false}
savingEnabled={true}
valueRenderer={[Function]}
valueWithImage={false}
values={Array []}
/>
</ModalBody>
</Modal>
`;
exports[`admin_console/add_users_to_role_modal should exclude user 1`] = `
<Modal
animation={true}
autoFocus={true}
backdrop={true}
bsClass="modal"
dialogClassName="a11y__modal more-modal more-direct-channels"
dialogComponentClass={[Function]}
enforceFocus={true}
id="addUsersToRoleModal"
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[Function]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
show={true}
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
>
<ModalTitle
bsClass="modal-title"
componentClass="h1"
>
<MemoizedFormattedMessage
defaultMessage="Add users to {roleName}"
id="add_users_to_role.title"
values={
Object {
"roleName": <strong>
<Memo(MemoizedFormattedMessage)
defaultMessage="role_name"
id="admin.permissions.roles.role_name.name"
/>
</strong>,
}
}
/>
</ModalTitle>
</ModalHeader>
<ModalBody
bsClass="modal-body"
componentClass="div"
>
<MultiSelect
ariaLabelRenderer={[Function]}
buttonSubmitLoadingText="Adding..."
buttonSubmitText="Add"
focusOnLoad={true}
handleAdd={[Function]}
handleDelete={[Function]}
handleInput={[Function]}
handlePageChange={[Function]}
handleSubmit={[Function]}
key="addUsersToRoleKey"
loading={true}
maxValues={20}
numRemainingText={
<div
id="numPeopleRemaining"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Use ↑↓ to browse, ↵ to select. You can add {num, number} more {num, plural, one {person} other {people}}. "
id="multiselect.numPeopleRemaining"
values={
Object {
"num": 20,
}
}
/>
</div>
}
optionRenderer={[Function]}
options={Array []}
perPage={50}
placeholderText="Search and add members"
saveButtonPosition="top"
saving={false}
savingEnabled={true}
valueRenderer={[Function]}
valueWithImage={false}
values={Array []}
/>
</ModalBody>
</Modal>
`;
exports[`admin_console/add_users_to_role_modal should have single passed value 1`] = `
<Modal
animation={true}
autoFocus={true}
backdrop={true}
bsClass="modal"
dialogClassName="a11y__modal more-modal more-direct-channels"
dialogComponentClass={[Function]}
enforceFocus={true}
id="addUsersToRoleModal"
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[Function]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
show={true}
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
>
<ModalTitle
bsClass="modal-title"
componentClass="h1"
>
<MemoizedFormattedMessage
defaultMessage="Add users to {roleName}"
id="add_users_to_role.title"
values={
Object {
"roleName": <strong>
<Memo(MemoizedFormattedMessage)
defaultMessage="role_name"
id="admin.permissions.roles.role_name.name"
/>
</strong>,
}
}
/>
</ModalTitle>
</ModalHeader>
<ModalBody
bsClass="modal-body"
componentClass="div"
>
<MultiSelect
ariaLabelRenderer={[Function]}
buttonSubmitLoadingText="Adding..."
buttonSubmitText="Add"
focusOnLoad={true}
handleAdd={[Function]}
handleDelete={[Function]}
handleInput={[Function]}
handlePageChange={[Function]}
handleSubmit={[Function]}
key="addUsersToRoleKey"
loading={true}
maxValues={20}
numRemainingText={
<div
id="numPeopleRemaining"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Use ↑↓ to browse, ↵ to select. You can add {num, number} more {num, plural, one {person} other {people}}. "
id="multiselect.numPeopleRemaining"
values={
Object {
"num": 20,
}
}
/>
</div>
}
optionRenderer={[Function]}
options={
Array [
Object {
"auth_service": "",
"bot_description": "",
"create_at": 0,
"delete_at": 0,
"email": "",
"first_name": "",
"id": "user_id",
"is_bot": false,
"label": "some-user",
"last_activity_at": 0,
"last_name": "",
"last_password_update": 0,
"last_picture_update": 0,
"locale": "",
"mfa_active": false,
"nickname": "",
"notify_props": Object {
"calls_desktop_sound": "true",
"channel": "false",
"comments": "never",
"desktop": "default",
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
"push_status": "offline",
},
"password": "",
"position": "",
"props": Object {},
"roles": "",
"terms_of_service_create_at": 0,
"terms_of_service_id": "",
"update_at": 0,
"username": "some-user",
"value": "user_id",
},
]
}
perPage={50}
placeholderText="Search and add members"
saveButtonPosition="top"
saving={false}
savingEnabled={true}
valueRenderer={[Function]}
valueWithImage={false}
values={Array []}
/>
</ModalBody>
</Modal>
`;
exports[`admin_console/add_users_to_role_modal should include additional user 1`] = `
<Modal
animation={true}
autoFocus={true}
@@ -183,3 +572,331 @@ exports[`admin_console/add_users_to_role_modal should match snapshot 1`] = `
</ModalBody>
</Modal>
`;
exports[`admin_console/add_users_to_role_modal should include additional user 2`] = `
<Modal
animation={true}
autoFocus={true}
backdrop={true}
bsClass="modal"
dialogClassName="a11y__modal more-modal more-direct-channels"
dialogComponentClass={[Function]}
enforceFocus={true}
id="addUsersToRoleModal"
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[Function]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
show={true}
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
>
<ModalTitle
bsClass="modal-title"
componentClass="h1"
>
<MemoizedFormattedMessage
defaultMessage="Add users to {roleName}"
id="add_users_to_role.title"
values={
Object {
"roleName": <strong>
<Memo(MemoizedFormattedMessage)
defaultMessage="role_name"
id="admin.permissions.roles.role_name.name"
/>
</strong>,
}
}
/>
</ModalTitle>
</ModalHeader>
<ModalBody
bsClass="modal-body"
componentClass="div"
>
<MultiSelect
ariaLabelRenderer={[Function]}
buttonSubmitLoadingText="Adding..."
buttonSubmitText="Add"
focusOnLoad={true}
handleAdd={[Function]}
handleDelete={[Function]}
handleInput={[Function]}
handlePageChange={[Function]}
handleSubmit={[Function]}
key="addUsersToRoleKey"
loading={true}
maxValues={20}
numRemainingText={
<div
id="numPeopleRemaining"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Use ↑↓ to browse, ↵ to select. You can add {num, number} more {num, plural, one {person} other {people}}. "
id="multiselect.numPeopleRemaining"
values={
Object {
"num": 20,
}
}
/>
</div>
}
optionRenderer={[Function]}
options={
Array [
Object {
"auth_service": "",
"bot_description": "",
"create_at": 0,
"delete_at": 0,
"email": "",
"first_name": "",
"id": "user_id",
"is_bot": false,
"label": "some-user",
"last_activity_at": 0,
"last_name": "",
"last_password_update": 0,
"last_picture_update": 0,
"locale": "",
"mfa_active": false,
"nickname": "",
"notify_props": Object {
"calls_desktop_sound": "true",
"channel": "false",
"comments": "never",
"desktop": "default",
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
"push_status": "offline",
},
"password": "",
"position": "",
"props": Object {},
"roles": "",
"terms_of_service_create_at": 0,
"terms_of_service_id": "",
"update_at": 0,
"username": "some-user",
"value": "user_id",
},
Object {
"auth_service": "",
"bot_description": "",
"create_at": 0,
"delete_at": 0,
"email": "",
"first_name": "",
"id": "user_id",
"is_bot": false,
"label": "some-user",
"last_activity_at": 0,
"last_name": "",
"last_password_update": 0,
"last_picture_update": 0,
"locale": "",
"mfa_active": false,
"nickname": "",
"notify_props": Object {
"calls_desktop_sound": "true",
"channel": "false",
"comments": "never",
"desktop": "default",
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
"push_status": "offline",
},
"password": "",
"position": "",
"props": Object {},
"roles": "",
"terms_of_service_create_at": 0,
"terms_of_service_id": "",
"update_at": 0,
"username": "some-user",
"value": "user_id",
},
]
}
perPage={50}
placeholderText="Search and add members"
saveButtonPosition="top"
saving={false}
savingEnabled={true}
valueRenderer={[Function]}
valueWithImage={false}
values={Array []}
/>
</ModalBody>
</Modal>
`;
exports[`admin_console/add_users_to_role_modal should not include bot user 1`] = `
<Modal
animation={true}
autoFocus={true}
backdrop={true}
bsClass="modal"
dialogClassName="a11y__modal more-modal more-direct-channels"
dialogComponentClass={[Function]}
enforceFocus={true}
id="addUsersToRoleModal"
keyboard={true}
manager={
ModalManager {
"add": [Function],
"containers": Array [],
"data": Array [],
"handleContainerOverflow": true,
"hideSiblingNodes": true,
"isTopModal": [Function],
"modals": Array [],
"remove": [Function],
}
}
onExited={[Function]}
onHide={[Function]}
renderBackdrop={[Function]}
restoreFocus={true}
show={true}
>
<ModalHeader
bsClass="modal-header"
closeButton={true}
closeLabel="Close"
>
<ModalTitle
bsClass="modal-title"
componentClass="h1"
>
<MemoizedFormattedMessage
defaultMessage="Add users to {roleName}"
id="add_users_to_role.title"
values={
Object {
"roleName": <strong>
<Memo(MemoizedFormattedMessage)
defaultMessage="role_name"
id="admin.permissions.roles.role_name.name"
/>
</strong>,
}
}
/>
</ModalTitle>
</ModalHeader>
<ModalBody
bsClass="modal-body"
componentClass="div"
>
<MultiSelect
ariaLabelRenderer={[Function]}
buttonSubmitLoadingText="Adding..."
buttonSubmitText="Add"
focusOnLoad={true}
handleAdd={[Function]}
handleDelete={[Function]}
handleInput={[Function]}
handlePageChange={[Function]}
handleSubmit={[Function]}
key="addUsersToRoleKey"
loading={true}
maxValues={20}
numRemainingText={
<div
id="numPeopleRemaining"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Use ↑↓ to browse, ↵ to select. You can add {num, number} more {num, plural, one {person} other {people}}. "
id="multiselect.numPeopleRemaining"
values={
Object {
"num": 20,
}
}
/>
</div>
}
optionRenderer={[Function]}
options={
Array [
Object {
"auth_service": "",
"bot_description": "",
"create_at": 0,
"delete_at": 0,
"email": "",
"first_name": "",
"id": "user_id",
"is_bot": false,
"label": "some-user",
"last_activity_at": 0,
"last_name": "",
"last_password_update": 0,
"last_picture_update": 0,
"locale": "",
"mfa_active": false,
"nickname": "",
"notify_props": Object {
"calls_desktop_sound": "true",
"channel": "false",
"comments": "never",
"desktop": "default",
"desktop_sound": "false",
"email": "false",
"first_name": "false",
"mark_unread": "mention",
"mention_keys": "",
"push": "none",
"push_status": "offline",
},
"password": "",
"position": "",
"props": Object {},
"roles": "",
"terms_of_service_create_at": 0,
"terms_of_service_id": "",
"update_at": 0,
"username": "some-user",
"value": "user_id",
},
]
}
perPage={50}
placeholderText="Search and add members"
saveButtonPosition="top"
saving={false}
savingEnabled={true}
valueRenderer={[Function]}
valueWithImage={false}
values={Array []}
/>
</ModalBody>
</Modal>
`;

Просмотреть файл

@@ -9,15 +9,11 @@ import {TestHelper} from 'utils/test_helper';
import AddUsersToRoleModal from './add_users_to_role_modal';
describe('admin_console/add_users_to_role_modal', () => {
const props = {
const baseProps = {
role: TestHelper.getRoleMock(),
users: [TestHelper.getUserMock()],
excludeUsers: {
asdf123: TestHelper.getUserMock(),
},
includeUsers: {
asdf123: TestHelper.getUserMock(),
},
excludeUsers: {},
includeUsers: {},
onAddCallback: jest.fn(),
onExited: jest.fn(),
actions: {
@@ -26,12 +22,76 @@ describe('admin_console/add_users_to_role_modal', () => {
},
};
test('should match snapshot', () => {
test('should have single passed value', () => {
const wrapper = shallow(
<AddUsersToRoleModal
{...baseProps}
/>);
expect(wrapper.find('MultiSelect').prop('options')).toHaveLength(1);
expect(wrapper).toMatchSnapshot();
});
test('should exclude user', () => {
const props = {...baseProps, excludeUsers: {user_id: TestHelper.getUserMock()}};
const wrapper = shallow(
<AddUsersToRoleModal
{...props}
/>);
expect(wrapper.find('MultiSelect').prop('options')).toHaveLength(0);
expect(wrapper).toMatchSnapshot();
});
test('should include additional user', () => {
const props = {...baseProps, includeUsers: {user_id1: TestHelper.getUserMock()}};
const wrapper = shallow(
<AddUsersToRoleModal
{...props}
/>);
expect(wrapper.find('MultiSelect').prop('options')).toHaveLength(2);
expect(wrapper).toMatchSnapshot();
});
test('should include additional user', () => {
const props = {...baseProps, includeUsers: {user_id1: TestHelper.getUserMock()}};
const wrapper = shallow(
<AddUsersToRoleModal
{...props}
/>);
expect(wrapper.find('MultiSelect').prop('options')).toHaveLength(2);
expect(wrapper).toMatchSnapshot();
});
test('should not include bot user', () => {
const botUser = TestHelper.getUserMock();
botUser.is_bot = true;
const props = {...baseProps,
actions: {
getProfiles: jest.fn().mockResolvedValue({data: [TestHelper.getUserMock(), botUser]}),
searchProfiles: jest.fn(),
},
};
const wrapper = shallow(
<AddUsersToRoleModal
{...props}
/>);
expect(wrapper.find('MultiSelect').prop('options')).toHaveLength(1);
expect(wrapper).toMatchSnapshot();
});
test('search should not include bot user', () => {
const botUser = TestHelper.getUserMock();
botUser.is_bot = true;
const props = {...baseProps,
actions: {
searchProfiles: jest.fn().mockResolvedValue({data: [TestHelper.getUserMock(), botUser]}),
getProfiles: jest.fn(),
},
};
const wrapper = shallow(
<AddUsersToRoleModal
{...props}
/>);
expect(wrapper.find('MultiSelect').prop('options')).toHaveLength(1);
expect(wrapper).toMatchSnapshot();
});
});

Просмотреть файл

@@ -82,11 +82,15 @@ export default class AddUsersToRoleModal extends React.PureComponent<Props, Stat
search = async (term: string) => {
this.setUsersLoadingState(true);
let searchResults: UserProfile[] = [];
const searchResults: UserProfile[] = [];
const search = term !== '';
if (search) {
const {data} = await this.props.actions.searchProfiles(term, {replace: true});
searchResults = data;
data.forEach((user) => {
if (!user.is_bot) {
searchResults.push(user);
}
});
} else {
await this.props.actions.getProfiles(0, USERS_PER_PAGE * 2);
}

Просмотреть файл

@@ -16,7 +16,7 @@ import AddUsersToRoleModal from './add_users_to_role_modal';
import type {Props} from './add_users_to_role_modal';
function mapStateToProps(state: GlobalState, props: Props) {
const filterOptions: {[key: string]: any} = {active: true, exclude_roles: [props.role.name]};
const filterOptions: {[key: string]: any} = {active: true, exclude_roles: [props.role.name], exclude_bots: true};
const users: UserProfile[] = selectProfiles(state, filterOptions);
return {

Просмотреть файл

@@ -156,7 +156,7 @@ export default class SystemRole extends React.PureComponent<Props, State> {
}
const userIdsToAdd = Object.keys(usersToAdd);
if (userIdsToAdd.length > 0 && serverError == null) {
if (userIdsToAdd.length > 0 && !serverError) {
const addUserPromises: Array<Promise<ActionResult>> = [];
userIdsToAdd.forEach((userId) => {
const user = usersToAdd[userId];
@@ -174,16 +174,16 @@ export default class SystemRole extends React.PureComponent<Props, State> {
}
let {saveKey} = this.state;
if (serverError === null) {
if (!serverError) {
saveKey += 1;
}
if (serverError === null) {
if (!serverError) {
getHistory().push('/admin_console/user_management/system_roles');
}
setNavigationBlocked(serverError !== null);
setNavigationBlocked(Boolean(serverError));
this.setState({
saveNeeded: (serverError !== null),
saveNeeded: Boolean(serverError),
saving: false,
serverError,
usersToAdd: {},

Просмотреть файл

@@ -819,4 +819,70 @@ describe('Selectors.Users', () => {
expect(Selectors.currentUserHasAnAdminRole(state)).toEqual(false);
});
});
describe('filterProfiles', () => {
it('no filter, return all users', () => {
expect(Object.keys(Selectors.filterProfiles(profiles)).length).toEqual(7);
});
it('filter role', () => {
const filter = {
role: 'system_admin',
};
expect(Object.keys(Selectors.filterProfiles(profiles, filter)).length).toEqual(3);
});
it('filter roles', () => {
const filter = {
roles: ['system_admin'],
team_roles: ['team_admin'],
};
const membership = TestHelper.fakeTeamMember(user3.id, team1.id);
membership.scheme_admin = true;
const memberships = {[user3.id]: membership};
expect(Object.keys(Selectors.filterProfiles(profiles, filter, memberships)).length).toEqual(4);
});
it('exclude_roles', () => {
const filter = {
exclude_roles: ['system_admin'],
};
expect(Object.keys(Selectors.filterProfiles(profiles, filter)).length).toEqual(4);
});
it('exclude bots', () => {
const filter = {
exclude_bots: true,
};
const botUser = {
...user1,
id: 'test_bot_id',
username: 'botusername',
first_name: '',
last_name: '',
is_bot: true,
};
const newProfiles = {
...profiles,
[botUser.id]: botUser,
};
expect(Object.keys(Selectors.filterProfiles(newProfiles, filter)).length).toEqual(7);
});
it('filter inactive', () => {
const filter = {
inactive: true,
};
expect(Object.keys(Selectors.filterProfiles(profiles, filter)).length).toEqual(2);
});
it('filter active', () => {
const filter = {
active: true,
};
expect(Object.keys(Selectors.filterProfiles(profiles, filter)).length).toEqual(5);
});
});
});

Просмотреть файл

@@ -49,6 +49,7 @@ type Filters = {
exclude_roles?: string[];
channel_roles?: string[];
team_roles?: string[];
exclude_bots?: boolean;
};
export function getUserIdsInChannels(state: GlobalState): RelationOneToManyUnique<Channel, UserProfile> {
@@ -304,6 +305,10 @@ export function filterProfiles(profiles: IDMappedObjects<UserProfile>, filters?:
});
}
if (filters.exclude_bots) {
users = users.filter((user) => !user.is_bot);
}
if (filters.inactive) {
users = users.filter((user) => user.delete_at !== 0);
} else if (filters.active) {