MM-64030/MM-64025/MM-63985/MM-63986/MM-63987/MM-63976/MM-64018/MM-64017/MM-64034/MM-64029 - Various accessibility fixes around User Groups modals (#31147)
* Convert user groups main modal to GenericModal, convert menu to Menu component, fixes * Added useFocusTrap on other modals, fix DOM ordering * [MM-64018] Add proper aria-labels to the menu button * [MM-64025] Convert user group button to an actual button * [MM-64017] Fixed aria-label on 3-dot menu in view user group modal * [MM-64030] Show aria-live region when search results are rendered * Fix tests * PR feedback * PR feedback
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
583eb3f62c
Коммит
2358699d91
@@ -31,87 +31,109 @@ exports[`component/add_users_to_group_modal should match snapshot 1`] = `
|
|||||||
role="none"
|
role="none"
|
||||||
show={true}
|
show={true}
|
||||||
>
|
>
|
||||||
<ModalHeader
|
<div>
|
||||||
bsClass="modal-header"
|
<ModalHeader
|
||||||
closeButton={true}
|
bsClass="modal-header"
|
||||||
closeLabel="Close"
|
closeButton={false}
|
||||||
>
|
closeLabel="Close"
|
||||||
<div
|
|
||||||
className="d-flex align-items-center"
|
|
||||||
>
|
>
|
||||||
<button
|
<div
|
||||||
aria-label="Back"
|
className="d-flex align-items-center"
|
||||||
className="modal-header-back-button btn btn-icon"
|
|
||||||
onClick={[Function]}
|
|
||||||
type="button"
|
|
||||||
>
|
>
|
||||||
<i
|
<button
|
||||||
className="icon icon-arrow-left"
|
aria-label="Back"
|
||||||
/>
|
className="modal-header-back-button btn btn-icon"
|
||||||
</button>
|
onClick={[Function]}
|
||||||
<ModalTitle
|
type="button"
|
||||||
bsClass="modal-title"
|
|
||||||
componentClass="h1"
|
|
||||||
id="addUsersToGroupsModalLabel"
|
|
||||||
>
|
|
||||||
<MemoizedFormattedMessage
|
|
||||||
defaultMessage="Add people to {group}"
|
|
||||||
id="user_groups_modal.addPeopleTitle"
|
|
||||||
values={
|
|
||||||
Object {
|
|
||||||
"group": "Group Name",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ModalTitle>
|
|
||||||
</div>
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalBody
|
|
||||||
bsClass="modal-body"
|
|
||||||
className="overflow--visible"
|
|
||||||
componentClass="div"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="user-groups-modal__content"
|
|
||||||
>
|
|
||||||
<form
|
|
||||||
role="form"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="group-add-user"
|
|
||||||
>
|
>
|
||||||
<Connect(injectIntl(AddUserToGroupMultiSelect))
|
<i
|
||||||
addUserCallback={[Function]}
|
className="icon icon-arrow-left"
|
||||||
backButtonClass="multiselect-back"
|
/>
|
||||||
backButtonClick={[Function]}
|
</button>
|
||||||
buttonSubmitLoadingText={
|
<ModalTitle
|
||||||
|
bsClass="modal-title"
|
||||||
|
componentClass="h1"
|
||||||
|
id="addUsersToGroupsModalLabel"
|
||||||
|
>
|
||||||
|
<MemoizedFormattedMessage
|
||||||
|
defaultMessage="Add people to {group}"
|
||||||
|
id="user_groups_modal.addPeopleTitle"
|
||||||
|
values={
|
||||||
Object {
|
Object {
|
||||||
"defaultMessage": "Adding...",
|
"group": "Group Name",
|
||||||
"id": "multiselect.adding",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buttonSubmitText={
|
|
||||||
Object {
|
|
||||||
"defaultMessage": "Add People",
|
|
||||||
"id": "multiselect.addPeopleToGroup",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
deleteUserCallback={[Function]}
|
|
||||||
focusOnLoad={false}
|
|
||||||
groupId="groupid123"
|
|
||||||
multilSelectKey="addUsersToGroupKey"
|
|
||||||
onSubmitCallback={[Function]}
|
|
||||||
saving={false}
|
|
||||||
savingEnabled={false}
|
|
||||||
searchOptions={
|
|
||||||
Object {
|
|
||||||
"not_in_group_id": "groupid123",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</ModalTitle>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
<button
|
||||||
</ModalBody>
|
aria-label="Close"
|
||||||
|
className="close"
|
||||||
|
onClick={[MockFunction]}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="sr-only"
|
||||||
|
>
|
||||||
|
<MemoizedFormattedMessage
|
||||||
|
defaultMessage="Close"
|
||||||
|
id="generic.close"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody
|
||||||
|
bsClass="modal-body"
|
||||||
|
className="overflow--visible"
|
||||||
|
componentClass="div"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="user-groups-modal__content"
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
role="form"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="group-add-user"
|
||||||
|
>
|
||||||
|
<Connect(injectIntl(AddUserToGroupMultiSelect))
|
||||||
|
addUserCallback={[Function]}
|
||||||
|
backButtonClass="multiselect-back"
|
||||||
|
backButtonClick={[Function]}
|
||||||
|
buttonSubmitLoadingText={
|
||||||
|
Object {
|
||||||
|
"defaultMessage": "Adding...",
|
||||||
|
"id": "multiselect.adding",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buttonSubmitText={
|
||||||
|
Object {
|
||||||
|
"defaultMessage": "Add People",
|
||||||
|
"id": "multiselect.addPeopleToGroup",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deleteUserCallback={[Function]}
|
||||||
|
focusOnLoad={false}
|
||||||
|
groupId="groupid123"
|
||||||
|
multilSelectKey="addUsersToGroupKey"
|
||||||
|
onSubmitCallback={[Function]}
|
||||||
|
saving={false}
|
||||||
|
savingEnabled={false}
|
||||||
|
searchOptions={
|
||||||
|
Object {
|
||||||
|
"not_in_group_id": "groupid123",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {useState, useCallback, useMemo} from 'react';
|
import React, {useState, useCallback, useMemo, useRef} from 'react';
|
||||||
import {Modal} from 'react-bootstrap';
|
import {Modal} from 'react-bootstrap';
|
||||||
import {defineMessage, FormattedMessage, useIntl} from 'react-intl';
|
import {defineMessage, FormattedMessage, useIntl} from 'react-intl';
|
||||||
|
|
||||||
|
import {useFocusTrap} from '@mattermost/components/src/hooks/useFocusTrap';
|
||||||
import type {Group} from '@mattermost/types/groups';
|
import type {Group} from '@mattermost/types/groups';
|
||||||
import type {UserProfile} from '@mattermost/types/users';
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
@@ -34,6 +35,9 @@ const AddUsersToGroupModal = (props: Props) => {
|
|||||||
const [showUnknownError, setShowUnknownError] = useState(false);
|
const [showUnknownError, setShowUnknownError] = useState(false);
|
||||||
const {formatMessage} = useIntl();
|
const {formatMessage} = useIntl();
|
||||||
|
|
||||||
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
|
useFocusTrap(show, modalRef);
|
||||||
|
|
||||||
const doHide = useCallback(() => {
|
const doHide = useCallback(() => {
|
||||||
setShow(false);
|
setShow(false);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -97,63 +101,79 @@ const AddUsersToGroupModal = (props: Props) => {
|
|||||||
aria-labelledby='createUserGroupsModalLabel'
|
aria-labelledby='createUserGroupsModalLabel'
|
||||||
id='addUsersToGroupsModal'
|
id='addUsersToGroupsModal'
|
||||||
>
|
>
|
||||||
<Modal.Header closeButton={true}>
|
<div ref={modalRef}>
|
||||||
<div className='d-flex align-items-center'>
|
<Modal.Header>
|
||||||
|
<div className='d-flex align-items-center'>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='modal-header-back-button btn btn-icon'
|
||||||
|
aria-label={formatMessage({id: 'user_groups_modal.goBackLabel', defaultMessage: 'Back'})}
|
||||||
|
onClick={goBack}
|
||||||
|
>
|
||||||
|
<i className='icon icon-arrow-left'/>
|
||||||
|
</button>
|
||||||
|
<Modal.Title
|
||||||
|
componentClass='h1'
|
||||||
|
id='addUsersToGroupsModalLabel'
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='user_groups_modal.addPeopleTitle'
|
||||||
|
defaultMessage='Add people to {group}'
|
||||||
|
values={titleValue}
|
||||||
|
/>
|
||||||
|
</Modal.Title>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
className='modal-header-back-button btn btn-icon'
|
className='close'
|
||||||
aria-label={formatMessage({id: 'user_groups_modal.goBackLabel', defaultMessage: 'Back'})}
|
onClick={props.onExited}
|
||||||
onClick={goBack}
|
aria-label={formatMessage({id: 'generic.close', defaultMessage: 'Close'})}
|
||||||
>
|
>
|
||||||
<i className='icon icon-arrow-left'/>
|
<span aria-hidden='true'>{'×'}</span>
|
||||||
</button>
|
<span className='sr-only'>
|
||||||
<Modal.Title
|
<FormattedMessage
|
||||||
componentClass='h1'
|
id='generic.close'
|
||||||
id='addUsersToGroupsModalLabel'
|
defaultMessage='Close'
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id='user_groups_modal.addPeopleTitle'
|
|
||||||
defaultMessage='Add people to {group}'
|
|
||||||
values={titleValue}
|
|
||||||
/>
|
|
||||||
</Modal.Title>
|
|
||||||
</div>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Body
|
|
||||||
className='overflow--visible'
|
|
||||||
>
|
|
||||||
<div className='user-groups-modal__content'>
|
|
||||||
<form role='form'>
|
|
||||||
<div className='group-add-user'>
|
|
||||||
<AddUserToGroupMultiSelect
|
|
||||||
multilSelectKey={'addUsersToGroupKey'}
|
|
||||||
onSubmitCallback={addUsersToGroup}
|
|
||||||
focusOnLoad={false}
|
|
||||||
savingEnabled={isSaveEnabled()}
|
|
||||||
addUserCallback={addUserCallback}
|
|
||||||
deleteUserCallback={deleteUserCallback}
|
|
||||||
groupId={props.groupId}
|
|
||||||
searchOptions={searchOptions}
|
|
||||||
buttonSubmitText={defineMessage({id: 'multiselect.addPeopleToGroup', defaultMessage: 'Add People'})}
|
|
||||||
buttonSubmitLoadingText={defineMessage({id: 'multiselect.adding', defaultMessage: 'Adding...'})}
|
|
||||||
backButtonClick={goBack}
|
|
||||||
backButtonClass={'multiselect-back'}
|
|
||||||
saving={saving}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</span>
|
||||||
{
|
</button>
|
||||||
showUnknownError &&
|
</Modal.Header>
|
||||||
<div className='Input___error group-error'>
|
<Modal.Body
|
||||||
<i className='icon icon-alert-outline'/>
|
className='overflow--visible'
|
||||||
<FormattedMessage
|
>
|
||||||
id='user_groups_modal.unknownError'
|
<div className='user-groups-modal__content'>
|
||||||
defaultMessage='An unknown error has occurred.'
|
<form role='form'>
|
||||||
|
<div className='group-add-user'>
|
||||||
|
<AddUserToGroupMultiSelect
|
||||||
|
multilSelectKey={'addUsersToGroupKey'}
|
||||||
|
onSubmitCallback={addUsersToGroup}
|
||||||
|
focusOnLoad={false}
|
||||||
|
savingEnabled={isSaveEnabled()}
|
||||||
|
addUserCallback={addUserCallback}
|
||||||
|
deleteUserCallback={deleteUserCallback}
|
||||||
|
groupId={props.groupId}
|
||||||
|
searchOptions={searchOptions}
|
||||||
|
buttonSubmitText={defineMessage({id: 'multiselect.addPeopleToGroup', defaultMessage: 'Add People'})}
|
||||||
|
buttonSubmitLoadingText={defineMessage({id: 'multiselect.adding', defaultMessage: 'Adding...'})}
|
||||||
|
backButtonClick={goBack}
|
||||||
|
backButtonClass={'multiselect-back'}
|
||||||
|
saving={saving}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
{
|
||||||
</form>
|
showUnknownError &&
|
||||||
</div>
|
<div className='Input___error group-error'>
|
||||||
</Modal.Body>
|
<i className='icon icon-alert-outline'/>
|
||||||
|
<FormattedMessage
|
||||||
|
id='user_groups_modal.unknownError'
|
||||||
|
defaultMessage='An unknown error has occurred.'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Modal.Body>
|
||||||
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ exports[`component/user_group_popover should match snapshot 1`] = `
|
|||||||
className="overflow--ellipsis text-nowrap"
|
className="overflow--ellipsis text-nowrap"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="Title-gqsoLG eUZcLS overflow--ellipsis text-nowrap"
|
className="Title-kjfqTJ hEcOkX overflow--ellipsis text-nowrap"
|
||||||
>
|
>
|
||||||
group_display_name
|
group_display_name
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,200 +1,193 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`component/user_groups_modal should match snapshot with groups 1`] = `
|
exports[`component/user_groups_modal should match snapshot with groups 1`] = `
|
||||||
<Modal
|
<GenericModal
|
||||||
animation={true}
|
ariaLabel="userGroupsModalLabel"
|
||||||
aria-labelledby="userGroupsModalLabel"
|
bodyPadding={false}
|
||||||
autoFocus={true}
|
className="a11y__modal user-groups-modal"
|
||||||
backdrop={true}
|
compassDesign={true}
|
||||||
bsClass="modal"
|
|
||||||
dialogClassName="a11y__modal user-groups-modal"
|
|
||||||
dialogComponentClass={[Function]}
|
|
||||||
enforceFocus={true}
|
enforceFocus={true}
|
||||||
|
headerButton={
|
||||||
|
<button
|
||||||
|
className="user-groups-create btn btn-secondary btn-sm"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Create Group"
|
||||||
|
id="user_groups_modal.createNew"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
id="userGroupsModal"
|
id="userGroupsModal"
|
||||||
keyboard={true}
|
modalHeaderText={
|
||||||
manager={
|
<Memo(MemoizedFormattedMessage)
|
||||||
ModalManager {
|
defaultMessage="User Groups"
|
||||||
"add": [Function],
|
id="user_groups_modal.title"
|
||||||
"containers": Array [],
|
/>
|
||||||
"data": Array [],
|
|
||||||
"handleContainerOverflow": true,
|
|
||||||
"hideSiblingNodes": true,
|
|
||||||
"isTopModal": [Function],
|
|
||||||
"modals": Array [],
|
|
||||||
"remove": [Function],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
onExited={[MockFunction]}
|
onExited={[MockFunction]}
|
||||||
onHide={[Function]}
|
onHide={[Function]}
|
||||||
renderBackdrop={[Function]}
|
|
||||||
restoreFocus={true}
|
|
||||||
role="none"
|
|
||||||
show={true}
|
show={true}
|
||||||
>
|
>
|
||||||
<Connect(Component)
|
<div
|
||||||
backButtonAction={[MockFunction]}
|
className="user-groups-search"
|
||||||
onExited={[MockFunction]}
|
|
||||||
/>
|
|
||||||
<ModalBody
|
|
||||||
bsClass="modal-body"
|
|
||||||
componentClass="div"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="user-groups-search"
|
aria-atomic="true"
|
||||||
>
|
aria-live="polite"
|
||||||
<ForwardRef
|
className="sr-only"
|
||||||
className="user-group-search-input"
|
role="status"
|
||||||
data-testid="searchInput"
|
|
||||||
inputPrefix={
|
|
||||||
<i
|
|
||||||
className="icon icon-magnify"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onChange={[Function]}
|
|
||||||
placeholder={
|
|
||||||
Object {
|
|
||||||
"defaultMessage": "Search Groups",
|
|
||||||
"id": "user_groups_modal.searchGroups",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Memo(UserGroupsFilter)
|
|
||||||
getGroups={[Function]}
|
|
||||||
selectedFilter="all"
|
|
||||||
/>
|
/>
|
||||||
<Connect(Component)
|
<ForwardRef
|
||||||
backButtonAction={[MockFunction]}
|
className="user-group-search-input"
|
||||||
groups={
|
data-testid="searchInput"
|
||||||
Array [
|
inputPrefix={
|
||||||
Object {
|
<i
|
||||||
"allow_reference": true,
|
className="icon icon-magnify"
|
||||||
"create_at": 1637349374137,
|
/>
|
||||||
"delete_at": 0,
|
|
||||||
"description": "Group 0 description",
|
|
||||||
"display_name": "Group 0",
|
|
||||||
"has_syncables": false,
|
|
||||||
"id": "group0",
|
|
||||||
"member_count": 1,
|
|
||||||
"name": "group0",
|
|
||||||
"remote_id": null,
|
|
||||||
"scheme_admin": false,
|
|
||||||
"source": "custom",
|
|
||||||
"update_at": 1637349374137,
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"allow_reference": true,
|
|
||||||
"create_at": 1637349374137,
|
|
||||||
"delete_at": 0,
|
|
||||||
"description": "Group 1 description",
|
|
||||||
"display_name": "Group 1",
|
|
||||||
"has_syncables": false,
|
|
||||||
"id": "group1",
|
|
||||||
"member_count": 2,
|
|
||||||
"name": "group1",
|
|
||||||
"remote_id": null,
|
|
||||||
"scheme_admin": false,
|
|
||||||
"source": "custom",
|
|
||||||
"update_at": 1637349374137,
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"allow_reference": true,
|
|
||||||
"create_at": 1637349374137,
|
|
||||||
"delete_at": 0,
|
|
||||||
"description": "Group 2 description",
|
|
||||||
"display_name": "Group 2",
|
|
||||||
"has_syncables": false,
|
|
||||||
"id": "group2",
|
|
||||||
"member_count": 3,
|
|
||||||
"name": "group2",
|
|
||||||
"remote_id": null,
|
|
||||||
"scheme_admin": false,
|
|
||||||
"source": "custom",
|
|
||||||
"update_at": 1637349374137,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
hasNextPage={true}
|
onChange={[Function]}
|
||||||
loadMoreGroups={[Function]}
|
placeholder={
|
||||||
loading={false}
|
Object {
|
||||||
onExited={[MockFunction]}
|
"defaultMessage": "Search Groups",
|
||||||
searchTerm=""
|
"id": "user_groups_modal.searchGroups",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
/>
|
/>
|
||||||
</ModalBody>
|
</div>
|
||||||
</Modal>
|
<Memo(UserGroupsFilter)
|
||||||
|
getGroups={[Function]}
|
||||||
|
onToggle={[Function]}
|
||||||
|
selectedFilter="all"
|
||||||
|
/>
|
||||||
|
<Connect(Component)
|
||||||
|
backButtonAction={[MockFunction]}
|
||||||
|
groups={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"allow_reference": true,
|
||||||
|
"create_at": 1637349374137,
|
||||||
|
"delete_at": 0,
|
||||||
|
"description": "Group 0 description",
|
||||||
|
"display_name": "Group 0",
|
||||||
|
"has_syncables": false,
|
||||||
|
"id": "group0",
|
||||||
|
"member_count": 1,
|
||||||
|
"name": "group0",
|
||||||
|
"remote_id": null,
|
||||||
|
"scheme_admin": false,
|
||||||
|
"source": "custom",
|
||||||
|
"update_at": 1637349374137,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"allow_reference": true,
|
||||||
|
"create_at": 1637349374137,
|
||||||
|
"delete_at": 0,
|
||||||
|
"description": "Group 1 description",
|
||||||
|
"display_name": "Group 1",
|
||||||
|
"has_syncables": false,
|
||||||
|
"id": "group1",
|
||||||
|
"member_count": 2,
|
||||||
|
"name": "group1",
|
||||||
|
"remote_id": null,
|
||||||
|
"scheme_admin": false,
|
||||||
|
"source": "custom",
|
||||||
|
"update_at": 1637349374137,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"allow_reference": true,
|
||||||
|
"create_at": 1637349374137,
|
||||||
|
"delete_at": 0,
|
||||||
|
"description": "Group 2 description",
|
||||||
|
"display_name": "Group 2",
|
||||||
|
"has_syncables": false,
|
||||||
|
"id": "group2",
|
||||||
|
"member_count": 3,
|
||||||
|
"name": "group2",
|
||||||
|
"remote_id": null,
|
||||||
|
"scheme_admin": false,
|
||||||
|
"source": "custom",
|
||||||
|
"update_at": 1637349374137,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
hasNextPage={true}
|
||||||
|
loadMoreGroups={[Function]}
|
||||||
|
loading={false}
|
||||||
|
onExited={[MockFunction]}
|
||||||
|
onToggle={[Function]}
|
||||||
|
searchTerm=""
|
||||||
|
/>
|
||||||
|
</GenericModal>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`component/user_groups_modal should match snapshot without groups 1`] = `
|
exports[`component/user_groups_modal should match snapshot without groups 1`] = `
|
||||||
<Modal
|
<GenericModal
|
||||||
animation={true}
|
ariaLabel="userGroupsModalLabel"
|
||||||
aria-labelledby="userGroupsModalLabel"
|
bodyPadding={false}
|
||||||
autoFocus={true}
|
className="a11y__modal user-groups-modal"
|
||||||
backdrop={true}
|
compassDesign={true}
|
||||||
bsClass="modal"
|
|
||||||
dialogClassName="a11y__modal user-groups-modal"
|
|
||||||
dialogComponentClass={[Function]}
|
|
||||||
enforceFocus={true}
|
enforceFocus={true}
|
||||||
|
headerButton={
|
||||||
|
<button
|
||||||
|
className="user-groups-create btn btn-secondary btn-sm"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
defaultMessage="Create Group"
|
||||||
|
id="user_groups_modal.createNew"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
id="userGroupsModal"
|
id="userGroupsModal"
|
||||||
keyboard={true}
|
modalHeaderText={
|
||||||
manager={
|
<Memo(MemoizedFormattedMessage)
|
||||||
ModalManager {
|
defaultMessage="User Groups"
|
||||||
"add": [Function],
|
id="user_groups_modal.title"
|
||||||
"containers": Array [],
|
/>
|
||||||
"data": Array [],
|
|
||||||
"handleContainerOverflow": true,
|
|
||||||
"hideSiblingNodes": true,
|
|
||||||
"isTopModal": [Function],
|
|
||||||
"modals": Array [],
|
|
||||||
"remove": [Function],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
onExited={[MockFunction]}
|
onExited={[MockFunction]}
|
||||||
onHide={[Function]}
|
onHide={[Function]}
|
||||||
renderBackdrop={[Function]}
|
|
||||||
restoreFocus={true}
|
|
||||||
role="none"
|
|
||||||
show={true}
|
show={true}
|
||||||
>
|
>
|
||||||
<Connect(Component)
|
<div
|
||||||
backButtonAction={[MockFunction]}
|
className="user-groups-search"
|
||||||
onExited={[MockFunction]}
|
|
||||||
/>
|
|
||||||
<ModalBody
|
|
||||||
bsClass="modal-body"
|
|
||||||
componentClass="div"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="user-groups-search"
|
aria-atomic="true"
|
||||||
>
|
aria-live="polite"
|
||||||
<ForwardRef
|
className="sr-only"
|
||||||
className="user-group-search-input"
|
role="status"
|
||||||
data-testid="searchInput"
|
|
||||||
inputPrefix={
|
|
||||||
<i
|
|
||||||
className="icon icon-magnify"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onChange={[Function]}
|
|
||||||
placeholder={
|
|
||||||
Object {
|
|
||||||
"defaultMessage": "Search Groups",
|
|
||||||
"id": "user_groups_modal.searchGroups",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type="text"
|
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Memo(UserGroupsFilter)
|
|
||||||
getGroups={[Function]}
|
|
||||||
selectedFilter="all"
|
|
||||||
/>
|
/>
|
||||||
<NoResultsIndicator
|
<ForwardRef
|
||||||
variant="UserGroups"
|
className="user-group-search-input"
|
||||||
|
data-testid="searchInput"
|
||||||
|
inputPrefix={
|
||||||
|
<i
|
||||||
|
className="icon icon-magnify"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder={
|
||||||
|
Object {
|
||||||
|
"defaultMessage": "Search Groups",
|
||||||
|
"id": "user_groups_modal.searchGroups",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
/>
|
/>
|
||||||
<Memo(ADLDAPUpsellBanner) />
|
</div>
|
||||||
</ModalBody>
|
<Memo(UserGroupsFilter)
|
||||||
</Modal>
|
getGroups={[Function]}
|
||||||
|
onToggle={[Function]}
|
||||||
|
selectedFilter="all"
|
||||||
|
/>
|
||||||
|
<NoResultsIndicator
|
||||||
|
variant="UserGroups"
|
||||||
|
/>
|
||||||
|
<Memo(ADLDAPUpsellBanner) />
|
||||||
|
</GenericModal>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ import type {Dispatch} from 'redux';
|
|||||||
import type {Group} from '@mattermost/types/groups';
|
import type {Group} from '@mattermost/types/groups';
|
||||||
|
|
||||||
import {getGroups, getGroupsByUserIdPaginated, searchGroups} from 'mattermost-redux/actions/groups';
|
import {getGroups, getGroupsByUserIdPaginated, searchGroups} from 'mattermost-redux/actions/groups';
|
||||||
|
import {Permissions} from 'mattermost-redux/constants';
|
||||||
import {makeGetAllAssociatedGroupsForReference, makeGetMyAllowReferencedGroups, searchAllowReferencedGroups, searchMyAllowReferencedGroups, searchArchivedGroups, getArchivedGroups} from 'mattermost-redux/selectors/entities/groups';
|
import {makeGetAllAssociatedGroupsForReference, makeGetMyAllowReferencedGroups, searchAllowReferencedGroups, searchMyAllowReferencedGroups, searchArchivedGroups, getArchivedGroups} from 'mattermost-redux/selectors/entities/groups';
|
||||||
|
import {haveISystemPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
|
||||||
|
import {openModal} from 'actions/views/modals';
|
||||||
import {setModalSearchTerm} from 'actions/views/search';
|
import {setModalSearchTerm} from 'actions/views/search';
|
||||||
import {isModalOpen} from 'selectors/views/modals';
|
import {isModalOpen} from 'selectors/views/modals';
|
||||||
|
|
||||||
@@ -47,6 +50,7 @@ function makeMapStateToProps() {
|
|||||||
myGroups,
|
myGroups,
|
||||||
archivedGroups,
|
archivedGroups,
|
||||||
currentUserId: getCurrentUserId(state),
|
currentUserId: getCurrentUserId(state),
|
||||||
|
canCreateCustomGroups: haveISystemPermission(state, {permission: Permissions.CREATE_CUSTOM_GROUP}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -58,6 +62,7 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
|||||||
setModalSearchTerm,
|
setModalSearchTerm,
|
||||||
getGroupsByUserIdPaginated,
|
getGroupsByUserIdPaginated,
|
||||||
searchGroups,
|
searchGroups,
|
||||||
|
openModal,
|
||||||
}, dispatch),
|
}, dispatch),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {useCallback} from 'react';
|
import React, {useCallback} from 'react';
|
||||||
import {useIntl} from 'react-intl';
|
import {FormattedMessage, useIntl} from 'react-intl';
|
||||||
|
|
||||||
import Menu from 'components/widgets/menu/menu';
|
import * as Menu from 'components/menu';
|
||||||
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
selectedFilter: string;
|
selectedFilter: string;
|
||||||
getGroups: (page: number, groupType: string) => void;
|
getGroups: (page: number, groupType: string) => void;
|
||||||
|
onToggle: (isOpen: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserGroupsFilter = (props: Props) => {
|
const UserGroupsFilter = (props: Props) => {
|
||||||
@@ -45,42 +45,58 @@ const UserGroupsFilter = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='more-modal__dropdown'>
|
<div className='more-modal__dropdown'>
|
||||||
<MenuWrapper id='groupsFilterDropdown'>
|
<Menu.Container
|
||||||
<a>
|
menuButton={{
|
||||||
<span>{filterLabel()}</span>
|
id: 'groupsFilterDropdown',
|
||||||
<span className='icon icon-chevron-down'/>
|
class: 'groups-filter-btn',
|
||||||
</a>
|
children: (
|
||||||
<Menu
|
<>
|
||||||
openLeft={false}
|
<span>{filterLabel()}</span>
|
||||||
ariaLabel={intl.formatMessage({id: 'user_groups_modal.filterAriaLabel', defaultMessage: 'Groups Filter Menu'})}
|
<span className='icon icon-chevron-down'/>
|
||||||
>
|
</>
|
||||||
<Menu.Group>
|
),
|
||||||
<Menu.ItemAction
|
}}
|
||||||
id='groupsDropdownAll'
|
menu={{
|
||||||
buttonClass='groups-filter-btn'
|
id: 'groupsFilterDropdownMenu',
|
||||||
onClick={allGroupsOnClick}
|
onToggle: props.onToggle,
|
||||||
text={intl.formatMessage({id: 'user_groups_modal.allGroups', defaultMessage: 'All Groups'})}
|
'aria-label': intl.formatMessage({id: 'user_groups_modal.filterAriaLabel', defaultMessage: 'Groups Filter'}),
|
||||||
rightDecorator={selectedFilter === 'all' && <i className='icon icon-check'/>}
|
}}
|
||||||
|
>
|
||||||
|
<Menu.Item
|
||||||
|
id='groupsDropdownAll'
|
||||||
|
onClick={allGroupsOnClick}
|
||||||
|
labels={
|
||||||
|
<FormattedMessage
|
||||||
|
id='user_groups_modal.allGroups'
|
||||||
|
defaultMessage='All Groups'
|
||||||
/>
|
/>
|
||||||
<Menu.ItemAction
|
}
|
||||||
id='groupsDropdownMy'
|
trailingElements={selectedFilter === 'all' && <i className='icon icon-check'/>}
|
||||||
buttonClass='groups-filter-btn'
|
/>
|
||||||
onClick={myGroupsOnClick}
|
<Menu.Item
|
||||||
text={intl.formatMessage({id: 'user_groups_modal.myGroups', defaultMessage: 'My Groups'})}
|
id='groupsDropdownMy'
|
||||||
rightDecorator={selectedFilter === 'my' && <i className='icon icon-check'/>}
|
onClick={myGroupsOnClick}
|
||||||
|
labels={
|
||||||
|
<FormattedMessage
|
||||||
|
id='user_groups_modal.myGroups'
|
||||||
|
defaultMessage='My Groups'
|
||||||
/>
|
/>
|
||||||
</Menu.Group>
|
}
|
||||||
<Menu.Group>
|
trailingElements={selectedFilter === 'my' && <i className='icon icon-check'/>}
|
||||||
<Menu.ItemAction
|
/>
|
||||||
id='groupsDropdownArchived'
|
<Menu.Separator/>
|
||||||
buttonClass='groups-filter-btn'
|
<Menu.Item
|
||||||
onClick={archivedGroupsOnClick}
|
id='groupsDropdownArchived'
|
||||||
text={intl.formatMessage({id: 'user_groups_modal.archivedGroups', defaultMessage: 'Archived Groups'})}
|
onClick={archivedGroupsOnClick}
|
||||||
rightDecorator={selectedFilter === 'archived' && <i className='icon icon-check'/>}
|
labels={
|
||||||
|
<FormattedMessage
|
||||||
|
id='user_groups_modal.archivedGroups'
|
||||||
|
defaultMessage='Archived Groups'
|
||||||
/>
|
/>
|
||||||
</Menu.Group>
|
}
|
||||||
</Menu>
|
trailingElements={selectedFilter === 'archived' && <i className='icon icon-check'/>}
|
||||||
</MenuWrapper>
|
/>
|
||||||
|
</Menu.Container>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ describe('component/user_groups_modal', () => {
|
|||||||
const baseProps = {
|
const baseProps = {
|
||||||
onExited: jest.fn(),
|
onExited: jest.fn(),
|
||||||
onScroll: jest.fn(),
|
onScroll: jest.fn(),
|
||||||
|
onToggle: jest.fn(),
|
||||||
groups: [],
|
groups: [],
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
backButtonAction: jest.fn(),
|
backButtonAction: jest.fn(),
|
||||||
|
|||||||
@@ -12,11 +12,10 @@ import type {Group, GroupPermissions} from '@mattermost/types/groups';
|
|||||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||||
|
|
||||||
import LoadingScreen from 'components/loading_screen';
|
import LoadingScreen from 'components/loading_screen';
|
||||||
|
import * as Menu from 'components/menu';
|
||||||
import NoResultsIndicator from 'components/no_results_indicator';
|
import NoResultsIndicator from 'components/no_results_indicator';
|
||||||
import {NoResultsVariant} from 'components/no_results_indicator/types';
|
import {NoResultsVariant} from 'components/no_results_indicator/types';
|
||||||
import ViewUserGroupModal from 'components/view_user_group_modal';
|
import ViewUserGroupModal from 'components/view_user_group_modal';
|
||||||
import Menu from 'components/widgets/menu/menu';
|
|
||||||
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
|
||||||
|
|
||||||
import {ModalIdentifiers} from 'utils/constants';
|
import {ModalIdentifiers} from 'utils/constants';
|
||||||
|
|
||||||
@@ -33,6 +32,7 @@ export type Props = {
|
|||||||
onExited: () => void;
|
onExited: () => void;
|
||||||
backButtonAction: () => void;
|
backButtonAction: () => void;
|
||||||
hasNextPage: boolean;
|
hasNextPage: boolean;
|
||||||
|
onToggle: (isOpen: boolean) => void;
|
||||||
actions: {
|
actions: {
|
||||||
archiveGroup: (groupId: string) => Promise<ActionResult>;
|
archiveGroup: (groupId: string) => Promise<ActionResult>;
|
||||||
restoreGroup: (groupId: string) => Promise<ActionResult>;
|
restoreGroup: (groupId: string) => Promise<ActionResult>;
|
||||||
@@ -51,6 +51,7 @@ const UserGroupsList = (props: Props) => {
|
|||||||
backButtonAction,
|
backButtonAction,
|
||||||
onExited,
|
onExited,
|
||||||
actions,
|
actions,
|
||||||
|
onToggle,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const infiniteLoaderRef = useRef<InfiniteLoader | null>(null);
|
const infiniteLoaderRef = useRef<InfiniteLoader | null>(null);
|
||||||
@@ -108,13 +109,6 @@ const UserGroupsList = (props: Props) => {
|
|||||||
onExited();
|
onExited();
|
||||||
}, [actions.openModal, onExited, backButtonAction]);
|
}, [actions.openModal, onExited, backButtonAction]);
|
||||||
|
|
||||||
const groupListOpenUp = (groupListItemIndex: number): boolean => {
|
|
||||||
if (groupListItemIndex === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Item = ({index, style}: ListChildComponentProps) => {
|
const Item = ({index, style}: ListChildComponentProps) => {
|
||||||
if (groups.length === 0 && searchTerm) {
|
if (groups.length === 0 && searchTerm) {
|
||||||
return (
|
return (
|
||||||
@@ -131,13 +125,14 @@ const UserGroupsList = (props: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<button
|
||||||
className='group-row'
|
className='group-row'
|
||||||
style={style}
|
style={style}
|
||||||
key={group.id}
|
key={group.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
goToViewGroupModal(group);
|
goToViewGroupModal(group);
|
||||||
}}
|
}}
|
||||||
|
aria-label={formatMessage({id: 'user_groups_list.groupAriaLabel', defaultMessage: '{group_name} group'}, {group_name: group.display_name})}
|
||||||
>
|
>
|
||||||
<span className='group-display-name'>
|
<span className='group-display-name'>
|
||||||
{
|
{
|
||||||
@@ -159,55 +154,68 @@ const UserGroupsList = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='group-action'>
|
<div className='group-action'>
|
||||||
<MenuWrapper
|
<Menu.Container
|
||||||
isDisabled={false}
|
menuButton={{
|
||||||
stopPropagationOnToggle={true}
|
id: `customWrapper-${group.id}`,
|
||||||
id={`customWrapper-${group.id}`}
|
class: 'btn btn-icon btn-xs',
|
||||||
|
children: <i className='icon icon-dots-vertical'/>,
|
||||||
|
'aria-label': formatMessage({id: 'user_groups_list.menuAriaLabel', defaultMessage: '{group_name} actions'}, {group_name: group.display_name}),
|
||||||
|
}}
|
||||||
|
menu={{
|
||||||
|
id: 'group-actions-menu',
|
||||||
|
onToggle,
|
||||||
|
'aria-label': formatMessage({id: 'user_groups_list.menuAriaLabel', defaultMessage: '{group_name} actions'}, {group_name: group.display_name}),
|
||||||
|
className: 'group-actions-menu',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<button className='btn btn-icon btn-xs'>
|
<Menu.Item
|
||||||
<i className='icon icon-dots-vertical'/>
|
id='view-group'
|
||||||
</button>
|
onClick={() => {
|
||||||
<Menu
|
goToViewGroupModal(group);
|
||||||
openLeft={true}
|
}}
|
||||||
openUp={groupListOpenUp(index)}
|
labels={
|
||||||
className={'group-actions-menu'}
|
<FormattedMessage
|
||||||
ariaLabel={formatMessage({id: 'admin.user_item.menuAriaLabel', defaultMessage: 'User Actions Menu'})}
|
id='user_groups_modal.viewGroup'
|
||||||
>
|
defaultMessage='View Group'
|
||||||
<Menu.Group>
|
|
||||||
<Menu.ItemAction
|
|
||||||
onClick={() => {
|
|
||||||
goToViewGroupModal(group);
|
|
||||||
}}
|
|
||||||
icon={<i className='icon-account-multiple-outline'/>}
|
|
||||||
text={formatMessage({id: 'user_groups_modal.viewGroup', defaultMessage: 'View Group'})}
|
|
||||||
disabled={false}
|
|
||||||
/>
|
/>
|
||||||
</Menu.Group>
|
}
|
||||||
<Menu.Group>
|
leadingElement={<i className='icon-account-multiple-outline'/>}
|
||||||
<Menu.ItemAction
|
/>
|
||||||
show={groupPermissionsMap[group.id].can_delete}
|
<Menu.Separator/>
|
||||||
onClick={() => {
|
{groupPermissionsMap[group.id].can_delete && (
|
||||||
archiveGroup(group.id);
|
<Menu.Item
|
||||||
}}
|
id='archive-group'
|
||||||
icon={<i className='icon-archive-outline'/>}
|
onClick={() => {
|
||||||
text={formatMessage({id: 'user_groups_modal.archiveGroup', defaultMessage: 'Archive Group'})}
|
archiveGroup(group.id);
|
||||||
disabled={false}
|
}}
|
||||||
isDangerous={true}
|
labels={
|
||||||
/>
|
<FormattedMessage
|
||||||
<Menu.ItemAction
|
id='user_groups_modal.archiveGroup'
|
||||||
show={groupPermissionsMap[group.id].can_restore}
|
defaultMessage='Archive Group'
|
||||||
onClick={() => {
|
/>
|
||||||
restoreGroup(group.id);
|
}
|
||||||
}}
|
leadingElement={<i className='icon-archive-outline'/>}
|
||||||
icon={<i className='icon-restore'/>}
|
isDestructive={true}
|
||||||
text={formatMessage({id: 'user_groups_modal.restoreGroup', defaultMessage: 'Restore Group'})}
|
/>
|
||||||
disabled={false}
|
)}
|
||||||
/>
|
{groupPermissionsMap[group.id].can_restore && (
|
||||||
</Menu.Group>
|
<Menu.Item
|
||||||
</Menu>
|
id='restore-group'
|
||||||
</MenuWrapper>
|
onClick={() => {
|
||||||
|
restoreGroup(group.id);
|
||||||
|
}}
|
||||||
|
labels={
|
||||||
|
<FormattedMessage
|
||||||
|
id='user_groups_modal.restoreGroup'
|
||||||
|
defaultMessage='Restore Group'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
leadingElement={<i className='icon-restore'/>}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Menu.Container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
|||||||
@@ -127,12 +127,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.GenericModal__header {
|
||||||
h1#createGroupsModalTitleWithBack,
|
display: flex;
|
||||||
h1#updateGroupsModalTitle,
|
|
||||||
h1#addUsersToGroupsModalLabel {
|
|
||||||
margin-left: 16px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +151,9 @@
|
|||||||
line-height: 38px;
|
line-height: 38px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Input {
|
.Input.form-control {
|
||||||
|
height: 34px;
|
||||||
|
border: none;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 38px;
|
line-height: 38px;
|
||||||
}
|
}
|
||||||
@@ -177,8 +175,11 @@
|
|||||||
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
|
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
|
||||||
|
|
||||||
#groupsFilterDropdown {
|
#groupsFilterDropdown {
|
||||||
|
display: inline-flex;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
background: none;
|
||||||
line-height: 12px;
|
line-height: 12px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -189,14 +190,11 @@
|
|||||||
background: rgba(var(--button-bg-rgb), 0.08);
|
background: rgba(var(--button-bg-rgb), 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
span {
|
||||||
|
color: var(--button-bg);
|
||||||
|
font-size: 12px;
|
||||||
line-height: 12px;
|
line-height: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
span {
|
|
||||||
color: var(--button-bg);
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 12px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,6 +240,9 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 14px 32px;
|
padding: 14px 32px;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
line-height: 38px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(var(--center-channel-color-rgb), 0.08);
|
background: rgba(var(--center-channel-color-rgb), 0.08);
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ describe('component/user_groups_modal', () => {
|
|||||||
setModalSearchTerm: jest.fn(),
|
setModalSearchTerm: jest.fn(),
|
||||||
getGroupsByUserIdPaginated: jest.fn(),
|
getGroupsByUserIdPaginated: jest.fn(),
|
||||||
searchGroups: jest.fn(),
|
searchGroups: jest.fn(),
|
||||||
|
openModal: jest.fn(),
|
||||||
},
|
},
|
||||||
|
canCreateCustomGroups: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
function getGroups(numberOfGroups: number) {
|
function getGroups(numberOfGroups: number) {
|
||||||
|
|||||||
@@ -2,25 +2,27 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||||
import {Modal} from 'react-bootstrap';
|
import {defineMessage, FormattedMessage} from 'react-intl';
|
||||||
import {defineMessage} from 'react-intl';
|
|
||||||
|
|
||||||
|
import {GenericModal} from '@mattermost/components';
|
||||||
import type {GetGroupsForUserParams, GetGroupsParams, Group, GroupSearchParams} from '@mattermost/types/groups';
|
import type {GetGroupsForUserParams, GetGroupsParams, Group, GroupSearchParams} from '@mattermost/types/groups';
|
||||||
|
|
||||||
import './user_groups_modal.scss';
|
import './user_groups_modal.scss';
|
||||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||||
|
|
||||||
|
import CreateUserGroupsModal from 'components/create_user_groups_modal';
|
||||||
import NoResultsIndicator from 'components/no_results_indicator';
|
import NoResultsIndicator from 'components/no_results_indicator';
|
||||||
import {NoResultsVariant} from 'components/no_results_indicator/types';
|
import {NoResultsVariant} from 'components/no_results_indicator/types';
|
||||||
import Input from 'components/widgets/inputs/input/input';
|
import Input from 'components/widgets/inputs/input/input';
|
||||||
|
|
||||||
import Constants from 'utils/constants';
|
import Constants, {ModalIdentifiers} from 'utils/constants';
|
||||||
|
|
||||||
|
import type {ModalData} from 'types/actions';
|
||||||
|
|
||||||
import ADLDAPUpsellBanner from './ad_ldap_upsell_banner';
|
import ADLDAPUpsellBanner from './ad_ldap_upsell_banner';
|
||||||
import {usePagingMeta} from './hooks';
|
import {usePagingMeta} from './hooks';
|
||||||
import UserGroupsFilter from './user_groups_filter/user_groups_filter';
|
import UserGroupsFilter from './user_groups_filter/user_groups_filter';
|
||||||
import UserGroupsList from './user_groups_list';
|
import UserGroupsList from './user_groups_list';
|
||||||
import UserGroupsModalHeader from './user_groups_modal_header';
|
|
||||||
|
|
||||||
const GROUPS_PER_PAGE = 60;
|
const GROUPS_PER_PAGE = 60;
|
||||||
|
|
||||||
@@ -32,6 +34,7 @@ export type Props = {
|
|||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
currentUserId: string;
|
currentUserId: string;
|
||||||
backButtonAction: () => void;
|
backButtonAction: () => void;
|
||||||
|
canCreateCustomGroups: boolean;
|
||||||
actions: {
|
actions: {
|
||||||
getGroups: (
|
getGroups: (
|
||||||
opts: GetGroupsParams,
|
opts: GetGroupsParams,
|
||||||
@@ -43,6 +46,7 @@ export type Props = {
|
|||||||
searchGroups: (
|
searchGroups: (
|
||||||
params: GroupSearchParams,
|
params: GroupSearchParams,
|
||||||
) => Promise<ActionResult>;
|
) => Promise<ActionResult>;
|
||||||
|
openModal: <P>(modalData: ModalData<P>) => void;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +57,7 @@ const UserGroupsModal = (props: Props) => {
|
|||||||
const [selectedFilter, setSelectedFilter] = useState('all');
|
const [selectedFilter, setSelectedFilter] = useState('all');
|
||||||
const [groupsFull, setGroupsFull] = useState(false);
|
const [groupsFull, setGroupsFull] = useState(false);
|
||||||
const [groups, setGroups] = useState(props.groups);
|
const [groups, setGroups] = useState(props.groups);
|
||||||
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
|
|
||||||
const [page, setPage] = usePagingMeta(selectedFilter);
|
const [page, setPage] = usePagingMeta(selectedFilter);
|
||||||
|
|
||||||
@@ -180,55 +185,105 @@ const UserGroupsModal = (props: Props) => {
|
|||||||
return NoResultsVariant.UserGroups;
|
return NoResultsVariant.UserGroups;
|
||||||
}, [selectedFilter]);
|
}, [selectedFilter]);
|
||||||
|
|
||||||
|
const goToCreateModal = useCallback(() => {
|
||||||
|
props.actions.openModal({
|
||||||
|
modalId: ModalIdentifiers.USER_GROUPS_CREATE,
|
||||||
|
dialogType: CreateUserGroupsModal,
|
||||||
|
dialogProps: {
|
||||||
|
backButtonCallback: props.backButtonAction,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
props.onExited();
|
||||||
|
}, [props.actions.openModal, props.backButtonAction, props.onExited]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<GenericModal
|
||||||
dialogClassName='a11y__modal user-groups-modal'
|
id='userGroupsModal'
|
||||||
|
ariaLabel='userGroupsModalLabel'
|
||||||
|
className='a11y__modal user-groups-modal'
|
||||||
show={show}
|
show={show}
|
||||||
onHide={doHide}
|
onHide={doHide}
|
||||||
onExited={props.onExited}
|
onExited={props.onExited}
|
||||||
role='none'
|
compassDesign={true}
|
||||||
aria-labelledby='userGroupsModalLabel'
|
modalHeaderText={
|
||||||
id='userGroupsModal'
|
<FormattedMessage
|
||||||
>
|
id='user_groups_modal.title'
|
||||||
<UserGroupsModalHeader
|
defaultMessage='User Groups'
|
||||||
onExited={props.onExited}
|
|
||||||
backButtonAction={props.backButtonAction}
|
|
||||||
/>
|
|
||||||
<Modal.Body>
|
|
||||||
<div className='user-groups-search'>
|
|
||||||
<Input
|
|
||||||
type='text'
|
|
||||||
placeholder={defineMessage({id: 'user_groups_modal.searchGroups', defaultMessage: 'Search Groups'})}
|
|
||||||
onChange={handleSearch}
|
|
||||||
value={props.searchTerm}
|
|
||||||
data-testid='searchInput'
|
|
||||||
className={'user-group-search-input'}
|
|
||||||
inputPrefix={inputPrefix}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<UserGroupsFilter
|
|
||||||
selectedFilter={selectedFilter}
|
|
||||||
getGroups={getGroups}
|
|
||||||
/>
|
/>
|
||||||
{(groups.length === 0 && !props.searchTerm) ? <>
|
}
|
||||||
<NoResultsIndicator
|
headerButton={
|
||||||
variant={noResultsType}
|
props.canCreateCustomGroups &&
|
||||||
|
<button
|
||||||
|
className='user-groups-create btn btn-secondary btn-sm'
|
||||||
|
onClick={goToCreateModal}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='user_groups_modal.createNew'
|
||||||
|
defaultMessage='Create Group'
|
||||||
/>
|
/>
|
||||||
<ADLDAPUpsellBanner/>
|
</button>
|
||||||
</> : <>
|
}
|
||||||
<UserGroupsList
|
bodyPadding={false}
|
||||||
groups={groups}
|
enforceFocus={!isMenuOpen}
|
||||||
searchTerm={props.searchTerm}
|
>
|
||||||
loading={loading}
|
<div className='user-groups-search'>
|
||||||
hasNextPage={!groupsFull}
|
<div
|
||||||
loadMoreGroups={loadMoreGroups}
|
className='sr-only'
|
||||||
onExited={props.onExited}
|
role='status'
|
||||||
backButtonAction={props.backButtonAction}
|
aria-live='polite'
|
||||||
/>
|
aria-atomic='true'
|
||||||
</>
|
>
|
||||||
}
|
{props.searchTerm && (
|
||||||
</Modal.Body>
|
groups.length > 0 ? (
|
||||||
</Modal>
|
<FormattedMessage
|
||||||
|
id='user_groups_modal.searchResults'
|
||||||
|
defaultMessage='{count} groups found'
|
||||||
|
values={{
|
||||||
|
count: groups.length,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormattedMessage
|
||||||
|
id='user_groups_modal.noSearchResults'
|
||||||
|
defaultMessage='No groups found'
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type='text'
|
||||||
|
placeholder={defineMessage({id: 'user_groups_modal.searchGroups', defaultMessage: 'Search Groups'})}
|
||||||
|
onChange={handleSearch}
|
||||||
|
value={props.searchTerm}
|
||||||
|
data-testid='searchInput'
|
||||||
|
className={'user-group-search-input'}
|
||||||
|
inputPrefix={inputPrefix}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<UserGroupsFilter
|
||||||
|
selectedFilter={selectedFilter}
|
||||||
|
getGroups={getGroups}
|
||||||
|
onToggle={setIsMenuOpen}
|
||||||
|
/>
|
||||||
|
{(groups.length === 0 && !props.searchTerm) ? <>
|
||||||
|
<NoResultsIndicator
|
||||||
|
variant={noResultsType}
|
||||||
|
/>
|
||||||
|
<ADLDAPUpsellBanner/>
|
||||||
|
</> : <>
|
||||||
|
<UserGroupsList
|
||||||
|
groups={groups}
|
||||||
|
searchTerm={props.searchTerm}
|
||||||
|
loading={loading}
|
||||||
|
hasNextPage={!groupsFull}
|
||||||
|
loadMoreGroups={loadMoreGroups}
|
||||||
|
onExited={props.onExited}
|
||||||
|
backButtonAction={props.backButtonAction}
|
||||||
|
onToggle={setIsMenuOpen}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</GenericModal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`component/user_groups_modal should match snapshot with groups 1`] = `
|
|
||||||
<ModalHeader
|
|
||||||
bsClass="modal-header"
|
|
||||||
closeButton={true}
|
|
||||||
closeLabel="Close"
|
|
||||||
>
|
|
||||||
<ModalTitle
|
|
||||||
bsClass="modal-title"
|
|
||||||
componentClass="h1"
|
|
||||||
id="userGroupsModalLabel"
|
|
||||||
>
|
|
||||||
<MemoizedFormattedMessage
|
|
||||||
defaultMessage="User Groups"
|
|
||||||
id="user_groups_modal.title"
|
|
||||||
/>
|
|
||||||
</ModalTitle>
|
|
||||||
</ModalHeader>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`component/user_groups_modal should match snapshot without groups 1`] = `
|
|
||||||
<ModalHeader
|
|
||||||
bsClass="modal-header"
|
|
||||||
closeButton={true}
|
|
||||||
closeLabel="Close"
|
|
||||||
>
|
|
||||||
<ModalTitle
|
|
||||||
bsClass="modal-title"
|
|
||||||
componentClass="h1"
|
|
||||||
id="userGroupsModalLabel"
|
|
||||||
>
|
|
||||||
<MemoizedFormattedMessage
|
|
||||||
defaultMessage="User Groups"
|
|
||||||
id="user_groups_modal.title"
|
|
||||||
/>
|
|
||||||
</ModalTitle>
|
|
||||||
<button
|
|
||||||
className="user-groups-create btn btn-secondary btn-sm"
|
|
||||||
onClick={[Function]}
|
|
||||||
>
|
|
||||||
<MemoizedFormattedMessage
|
|
||||||
defaultMessage="Create Group"
|
|
||||||
id="user_groups_modal.createNew"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</ModalHeader>
|
|
||||||
`;
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import {connect} from 'react-redux';
|
|
||||||
import {bindActionCreators} from 'redux';
|
|
||||||
import type {Dispatch} from 'redux';
|
|
||||||
|
|
||||||
import {Permissions} from 'mattermost-redux/constants';
|
|
||||||
import {haveISystemPermission} from 'mattermost-redux/selectors/entities/roles';
|
|
||||||
|
|
||||||
import {openModal} from 'actions/views/modals';
|
|
||||||
|
|
||||||
import type {GlobalState} from 'types/store';
|
|
||||||
|
|
||||||
import UserGroupsModalHeader from './user_groups_modal_header';
|
|
||||||
|
|
||||||
function mapStateToProps(state: GlobalState) {
|
|
||||||
const canCreateCustomGroups = haveISystemPermission(state, {permission: Permissions.CREATE_CUSTOM_GROUP});
|
|
||||||
|
|
||||||
return {
|
|
||||||
canCreateCustomGroups,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch: Dispatch) {
|
|
||||||
return {
|
|
||||||
actions: bindActionCreators({
|
|
||||||
openModal,
|
|
||||||
}, dispatch),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(UserGroupsModalHeader);
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import {shallow} from 'enzyme';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import UserGroupsModalHeader from './user_groups_modal_header';
|
|
||||||
|
|
||||||
describe('component/user_groups_modal', () => {
|
|
||||||
const baseProps = {
|
|
||||||
onExited: jest.fn(),
|
|
||||||
backButtonAction: jest.fn(),
|
|
||||||
canCreateCustomGroups: true,
|
|
||||||
actions: {
|
|
||||||
openModal: jest.fn(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
test('should match snapshot without groups', () => {
|
|
||||||
const wrapper = shallow(
|
|
||||||
<UserGroupsModalHeader
|
|
||||||
{...baseProps}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should match snapshot with groups', () => {
|
|
||||||
const wrapper = shallow(
|
|
||||||
<UserGroupsModalHeader
|
|
||||||
{...baseProps}
|
|
||||||
canCreateCustomGroups={false}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
import React, {useCallback} from 'react';
|
|
||||||
import {Modal} from 'react-bootstrap';
|
|
||||||
import {FormattedMessage} from 'react-intl';
|
|
||||||
|
|
||||||
import CreateUserGroupsModal from 'components/create_user_groups_modal';
|
|
||||||
|
|
||||||
import {ModalIdentifiers} from 'utils/constants';
|
|
||||||
|
|
||||||
import type {ModalData} from 'types/actions';
|
|
||||||
|
|
||||||
export type Props = {
|
|
||||||
canCreateCustomGroups: boolean;
|
|
||||||
onExited: () => void;
|
|
||||||
backButtonAction: () => void;
|
|
||||||
actions: {
|
|
||||||
openModal: <P>(modalData: ModalData<P>) => void;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const UserGroupsModalHeader = (props: Props) => {
|
|
||||||
const goToCreateModal = useCallback(() => {
|
|
||||||
props.actions.openModal({
|
|
||||||
modalId: ModalIdentifiers.USER_GROUPS_CREATE,
|
|
||||||
dialogType: CreateUserGroupsModal,
|
|
||||||
dialogProps: {
|
|
||||||
backButtonCallback: props.backButtonAction,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
props.onExited();
|
|
||||||
}, [props.actions.openModal, props.backButtonAction, props.onExited]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal.Header closeButton={true}>
|
|
||||||
<Modal.Title
|
|
||||||
componentClass='h1'
|
|
||||||
id='userGroupsModalLabel'
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id='user_groups_modal.title'
|
|
||||||
defaultMessage='User Groups'
|
|
||||||
/>
|
|
||||||
</Modal.Title>
|
|
||||||
{
|
|
||||||
props.canCreateCustomGroups &&
|
|
||||||
<button
|
|
||||||
className='user-groups-create btn btn-secondary btn-sm'
|
|
||||||
onClick={goToCreateModal}
|
|
||||||
>
|
|
||||||
<FormattedMessage
|
|
||||||
id='user_groups_modal.createNew'
|
|
||||||
defaultMessage='Create Group'
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</Modal.Header>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default React.memo(UserGroupsModalHeader);
|
|
||||||
@@ -9,7 +9,7 @@ exports[`component/view_user_group_modal should match snapshot 1`] = `
|
|||||||
bsClass="modal"
|
bsClass="modal"
|
||||||
dialogClassName="a11y__modal view-user-groups-modal"
|
dialogClassName="a11y__modal view-user-groups-modal"
|
||||||
dialogComponentClass={[Function]}
|
dialogComponentClass={[Function]}
|
||||||
enforceFocus={true}
|
enforceFocus={false}
|
||||||
keyboard={true}
|
keyboard={true}
|
||||||
manager={
|
manager={
|
||||||
ModalManager {
|
ModalManager {
|
||||||
@@ -30,96 +30,98 @@ exports[`component/view_user_group_modal should match snapshot 1`] = `
|
|||||||
role="none"
|
role="none"
|
||||||
show={true}
|
show={true}
|
||||||
>
|
>
|
||||||
<Connect(Component)
|
<div>
|
||||||
backButtonAction={[MockFunction]}
|
<Connect(Component)
|
||||||
backButtonCallback={[MockFunction]}
|
backButtonAction={[MockFunction]}
|
||||||
decrementMemberCount={[Function]}
|
backButtonCallback={[MockFunction]}
|
||||||
groupId="groupid123"
|
decrementMemberCount={[Function]}
|
||||||
incrementMemberCount={[Function]}
|
groupId="groupid123"
|
||||||
onExited={[MockFunction]}
|
incrementMemberCount={[Function]}
|
||||||
/>
|
onExited={[MockFunction]}
|
||||||
<ModalBody
|
/>
|
||||||
bsClass="modal-body"
|
<ModalBody
|
||||||
componentClass="div"
|
bsClass="modal-body"
|
||||||
>
|
componentClass="div"
|
||||||
<div
|
|
||||||
className="group-mention-name"
|
|
||||||
>
|
>
|
||||||
<span
|
<div
|
||||||
className="group-name"
|
className="group-mention-name"
|
||||||
>
|
>
|
||||||
@group
|
<span
|
||||||
</span>
|
className="group-name"
|
||||||
</div>
|
>
|
||||||
<div
|
@group
|
||||||
className="user-groups-search"
|
</span>
|
||||||
>
|
</div>
|
||||||
<ForwardRef
|
<div
|
||||||
className="user-group-search-input"
|
className="user-groups-search"
|
||||||
data-testid="searchInput"
|
>
|
||||||
inputPrefix={
|
<ForwardRef
|
||||||
<i
|
className="user-group-search-input"
|
||||||
className="icon icon-magnify"
|
data-testid="searchInput"
|
||||||
/>
|
inputPrefix={
|
||||||
}
|
<i
|
||||||
onChange={[Function]}
|
className="icon icon-magnify"
|
||||||
placeholder={
|
/>
|
||||||
Object {
|
|
||||||
"defaultMessage": "Search group members",
|
|
||||||
"id": "search_bar.searchGroupMembers",
|
|
||||||
}
|
}
|
||||||
}
|
onChange={[Function]}
|
||||||
type="text"
|
placeholder={
|
||||||
value=""
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="user-groups-modal__content group-member-list"
|
|
||||||
onScroll={[Function]}
|
|
||||||
>
|
|
||||||
<h2
|
|
||||||
className="group-member-count"
|
|
||||||
>
|
|
||||||
<MemoizedFormattedMessage
|
|
||||||
defaultMessage="{member_count} {member_count, plural, one {Member} other {Members}}"
|
|
||||||
id="view_user_group_modal.memberCount"
|
|
||||||
values={
|
|
||||||
Object {
|
Object {
|
||||||
"member_count": 6,
|
"defaultMessage": "Search group members",
|
||||||
|
"id": "search_bar.searchGroupMembers",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="user-groups-modal__content group-member-list"
|
||||||
|
onScroll={[Function]}
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
className="group-member-count"
|
||||||
|
>
|
||||||
|
<MemoizedFormattedMessage
|
||||||
|
defaultMessage="{member_count} {member_count, plural, one {Member} other {Members}}"
|
||||||
|
id="view_user_group_modal.memberCount"
|
||||||
|
values={
|
||||||
|
Object {
|
||||||
|
"member_count": 6,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
<Connect(Component)
|
||||||
|
decrementMemberCount={[Function]}
|
||||||
|
groupId="groupid123"
|
||||||
|
key="user-1"
|
||||||
|
user={
|
||||||
|
Object {
|
||||||
|
"delete_at": 0,
|
||||||
|
"first_name": "user",
|
||||||
|
"id": "user-1",
|
||||||
|
"last_name": "one",
|
||||||
|
"username": "user1",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</h2>
|
<Connect(Component)
|
||||||
<Connect(Component)
|
decrementMemberCount={[Function]}
|
||||||
decrementMemberCount={[Function]}
|
groupId="groupid123"
|
||||||
groupId="groupid123"
|
key="user-2"
|
||||||
key="user-1"
|
user={
|
||||||
user={
|
Object {
|
||||||
Object {
|
"delete_at": 0,
|
||||||
"delete_at": 0,
|
"first_name": "user",
|
||||||
"first_name": "user",
|
"id": "user-2",
|
||||||
"id": "user-1",
|
"last_name": "otwo",
|
||||||
"last_name": "one",
|
"username": "user2",
|
||||||
"username": "user1",
|
}
|
||||||
}
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<LoadingScreen />
|
||||||
<Connect(Component)
|
</div>
|
||||||
decrementMemberCount={[Function]}
|
</ModalBody>
|
||||||
groupId="groupid123"
|
</div>
|
||||||
key="user-2"
|
|
||||||
user={
|
|
||||||
Object {
|
|
||||||
"delete_at": 0,
|
|
||||||
"first_name": "user",
|
|
||||||
"id": "user-2",
|
|
||||||
"last_name": "otwo",
|
|
||||||
"username": "user2",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<LoadingScreen />
|
|
||||||
</div>
|
|
||||||
</ModalBody>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -2,18 +2,16 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {useCallback} from 'react';
|
import React, {useCallback} from 'react';
|
||||||
import {useIntl} from 'react-intl';
|
import {useIntl, FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
import type {Group} from '@mattermost/types/groups';
|
import type {Group} from '@mattermost/types/groups';
|
||||||
|
|
||||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||||
|
|
||||||
|
import * as Menu from 'components/menu';
|
||||||
import UpdateUserGroupModal from 'components/update_user_group_modal';
|
import UpdateUserGroupModal from 'components/update_user_group_modal';
|
||||||
import Menu from 'components/widgets/menu/menu';
|
|
||||||
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
|
||||||
|
|
||||||
import {ModalIdentifiers} from 'utils/constants';
|
import {ModalIdentifiers} from 'utils/constants';
|
||||||
import * as Utils from 'utils/utils';
|
|
||||||
|
|
||||||
import type {ModalData} from 'types/actions';
|
import type {ModalData} from 'types/actions';
|
||||||
|
|
||||||
@@ -86,50 +84,70 @@ const ViewUserGroupHeaderSubMenu = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='details-action'>
|
<div className='details-action'>
|
||||||
<MenuWrapper
|
<Menu.Container
|
||||||
isDisabled={false}
|
menuButton={{
|
||||||
stopPropagationOnToggle={false}
|
id: `detailsCustomWrapper-${group.id}`,
|
||||||
id={`detailsCustomWrapper-${group.id}`}
|
class: 'btn btn-icon',
|
||||||
|
children: (<i className='icon icon-dots-vertical'/>),
|
||||||
|
'aria-label': formatMessage({id: 'view_user_group_header_sub_menu.menuAriaLabel', defaultMessage: 'User group actions'}),
|
||||||
|
}}
|
||||||
|
menu={{
|
||||||
|
id: 'details-group-actions-menu',
|
||||||
|
'aria-labelledby': `detailsCustomWrapper-${group.id}`,
|
||||||
|
className: 'group-actions-menu',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<button className='btn btn-icon'>
|
{props.permissionToEditGroup && (
|
||||||
<i
|
<Menu.Item
|
||||||
className='icon icon-dots-vertical'
|
id='edit-details'
|
||||||
aria-label={formatMessage({id: 'user_groups_modal.goBackLabel', defaultMessage: 'Back'})}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<Menu
|
|
||||||
openLeft={false}
|
|
||||||
openUp={false}
|
|
||||||
ariaLabel={Utils.localizeMessage({id: 'admin.user_item.menuAriaLabel', defaultMessage: 'User Actions Menu'})}
|
|
||||||
>
|
|
||||||
<Menu.ItemAction
|
|
||||||
show={props.permissionToEditGroup}
|
|
||||||
onClick={goToEditGroupModal}
|
onClick={goToEditGroupModal}
|
||||||
text={Utils.localizeMessage({id: 'user_groups_modal.editDetails', defaultMessage: 'Edit Details'})}
|
labels={
|
||||||
disabled={false}
|
<FormattedMessage
|
||||||
|
id='user_groups_modal.editDetails'
|
||||||
|
defaultMessage='Edit Details'
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Menu.ItemAction
|
)}
|
||||||
show={props.permissionToJoinGroup && !isGroupMember}
|
{props.permissionToJoinGroup && !isGroupMember && (
|
||||||
|
<Menu.Item
|
||||||
|
id='join-group'
|
||||||
onClick={joinGroup}
|
onClick={joinGroup}
|
||||||
text={Utils.localizeMessage({id: 'user_groups_modal.joinGroup', defaultMessage: 'Join Group'})}
|
labels={
|
||||||
disabled={false}
|
<FormattedMessage
|
||||||
|
id='user_groups_modal.joinGroup'
|
||||||
|
defaultMessage='Join Group'
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Menu.ItemAction
|
)}
|
||||||
show={props.permissionToLeaveGroup && isGroupMember}
|
{props.permissionToLeaveGroup && isGroupMember && (
|
||||||
|
<Menu.Item
|
||||||
|
id='leave-group'
|
||||||
onClick={leaveGroup}
|
onClick={leaveGroup}
|
||||||
text={Utils.localizeMessage({id: 'user_groups_modal.leaveGroup', defaultMessage: 'Leave Group'})}
|
labels={
|
||||||
disabled={false}
|
<FormattedMessage
|
||||||
isDangerous={true}
|
id='user_groups_modal.leaveGroup'
|
||||||
|
defaultMessage='Leave Group'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
isDestructive={true}
|
||||||
/>
|
/>
|
||||||
<Menu.ItemAction
|
)}
|
||||||
show={props.permissionToArchiveGroup}
|
{props.permissionToArchiveGroup && (
|
||||||
|
<Menu.Item
|
||||||
|
id='archive-group'
|
||||||
onClick={archiveGroup}
|
onClick={archiveGroup}
|
||||||
text={Utils.localizeMessage({id: 'user_groups_modal.archiveGroup', defaultMessage: 'Archive Group'})}
|
labels={
|
||||||
disabled={false}
|
<FormattedMessage
|
||||||
isDangerous={true}
|
id='user_groups_modal.archiveGroup'
|
||||||
|
defaultMessage='Archive Group'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
isDestructive={true}
|
||||||
/>
|
/>
|
||||||
</Menu>
|
)}
|
||||||
</MenuWrapper>
|
</Menu.Container>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -98,16 +98,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
button.remove-group-member {
|
button.remove-group-member {
|
||||||
display: none;
|
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover, &:focus-within {
|
||||||
background: rgba(var(--center-channel-color-rgb), 0.08);
|
background: rgba(var(--center-channel-color-rgb), 0.08);
|
||||||
|
|
||||||
button.remove-group-member {
|
button.remove-group-member {
|
||||||
display: block;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ describe('component/view_user_group_modal', () => {
|
|||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should match snapshot, search user1', () => {
|
test('should match snapshot, search user1', async () => {
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
<ViewUserGroupModal
|
<ViewUserGroupModal
|
||||||
{...baseProps}
|
{...baseProps}
|
||||||
@@ -83,21 +83,14 @@ describe('component/view_user_group_modal', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const instance = wrapper.instance() as ViewUserGroupModal;
|
// Simulate search input change
|
||||||
|
const searchInput = wrapper.find('ForwardRef[data-testid="searchInput"]');
|
||||||
const e = {
|
searchInput.simulate('change', {target: {value: ''}});
|
||||||
target: {
|
|
||||||
value: '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
instance.handleSearch(e as React.ChangeEvent<HTMLInputElement>);
|
|
||||||
expect(baseProps.actions.setModalSearchTerm).toHaveBeenCalledTimes(1);
|
expect(baseProps.actions.setModalSearchTerm).toHaveBeenCalledTimes(1);
|
||||||
expect(baseProps.actions.setModalSearchTerm).toBeCalledWith('');
|
expect(baseProps.actions.setModalSearchTerm).toBeCalledWith('');
|
||||||
|
|
||||||
e.target.value = 'user1';
|
searchInput.simulate('change', {target: {value: 'user1'}});
|
||||||
instance.handleSearch(e as React.ChangeEvent<HTMLInputElement>);
|
|
||||||
expect(wrapper.state('loading')).toEqual(true);
|
|
||||||
expect(baseProps.actions.setModalSearchTerm).toHaveBeenCalledTimes(2);
|
expect(baseProps.actions.setModalSearchTerm).toHaveBeenCalledTimes(2);
|
||||||
expect(baseProps.actions.setModalSearchTerm).toBeCalledWith(e.target.value);
|
expect(baseProps.actions.setModalSearchTerm).toBeCalledWith('user1');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {createRef} from 'react';
|
import React, {useRef, useState, useEffect, useCallback} from 'react';
|
||||||
import type {RefObject} from 'react';
|
|
||||||
import {Modal} from 'react-bootstrap';
|
import {Modal} from 'react-bootstrap';
|
||||||
import {defineMessage, FormattedMessage} from 'react-intl';
|
import {defineMessage, FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
|
import {useFocusTrap} from '@mattermost/components/src/hooks/useFocusTrap';
|
||||||
import type {Group} from '@mattermost/types/groups';
|
import type {Group} from '@mattermost/types/groups';
|
||||||
import {GroupSource, PluginGroupSourcePrefix} from '@mattermost/types/groups';
|
import {GroupSource, PluginGroupSourcePrefix} from '@mattermost/types/groups';
|
||||||
import type {UserProfile} from '@mattermost/types/users';
|
import type {UserProfile} from '@mattermost/types/users';
|
||||||
@@ -43,133 +43,119 @@ export type Props = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
const ViewUserGroupModal: React.FC<Props> = ({
|
||||||
page: number;
|
onExited,
|
||||||
loading: boolean;
|
searchTerm,
|
||||||
show: boolean;
|
groupId,
|
||||||
selectedFilter: string;
|
group,
|
||||||
memberCount: number;
|
users,
|
||||||
}
|
backButtonCallback,
|
||||||
|
backButtonAction,
|
||||||
|
actions,
|
||||||
|
}) => {
|
||||||
|
const divScrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const searchTimeoutIdRef = useRef<number>(0);
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [show, setShow] = useState(true);
|
||||||
|
const [memberCount, setMemberCount] = useState(group?.member_count || 0);
|
||||||
|
|
||||||
export default class ViewUserGroupModal extends React.PureComponent<Props, State> {
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
private divScrollRef: RefObject<HTMLDivElement>;
|
useFocusTrap(show, modalRef);
|
||||||
private searchTimeoutId: number;
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
const incrementMemberCount = useCallback(() => {
|
||||||
super(props);
|
setMemberCount((prev) => prev + 1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
this.divScrollRef = createRef();
|
const decrementMemberCount = useCallback(() => {
|
||||||
this.searchTimeoutId = 0;
|
setMemberCount((prev) => prev - 1);
|
||||||
this.state = {
|
}, []);
|
||||||
page: 0,
|
|
||||||
loading: true,
|
|
||||||
show: true,
|
|
||||||
selectedFilter: 'all',
|
|
||||||
memberCount: props.group?.member_count || 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
incrementMemberCount = () => {
|
const doHide = useCallback(() => {
|
||||||
this.setState({memberCount: this.state.memberCount + 1});
|
setShow(false);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
decrementMemberCount = () => {
|
const startLoad = useCallback(() => {
|
||||||
this.setState({memberCount: this.state.memberCount - 1});
|
setLoading(true);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
doHide = () => {
|
const loadComplete = useCallback(() => {
|
||||||
this.setState({show: false});
|
setLoading(false);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
async componentDidMount() {
|
const handleSearch = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const {
|
|
||||||
groupId,
|
|
||||||
actions,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
actions.getGroup(groupId, true),
|
|
||||||
actions.getUsersInGroup(groupId, 0, USERS_PER_PAGE),
|
|
||||||
]);
|
|
||||||
this.loadComplete();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.props.actions.setModalSearchTerm('');
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
|
||||||
if (prevProps.searchTerm !== this.props.searchTerm) {
|
|
||||||
clearTimeout(this.searchTimeoutId);
|
|
||||||
const searchTerm = this.props.searchTerm;
|
|
||||||
|
|
||||||
if (searchTerm === '') {
|
|
||||||
this.loadComplete();
|
|
||||||
this.searchTimeoutId = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchTimeoutId = window.setTimeout(
|
|
||||||
async () => {
|
|
||||||
await prevProps.actions.searchProfiles(searchTerm, {in_group_id: this.props.groupId});
|
|
||||||
},
|
|
||||||
Constants.SEARCH_TIMEOUT_MILLISECONDS,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.searchTimeoutId = searchTimeoutId;
|
|
||||||
}
|
|
||||||
if (prevProps.group?.member_count !== this.props.group?.member_count) {
|
|
||||||
this.setMemberCount(this.props.group?.member_count || 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setMemberCount = (count: number) => {
|
|
||||||
this.setState({memberCount: count});
|
|
||||||
};
|
|
||||||
|
|
||||||
startLoad = () => {
|
|
||||||
this.setState({loading: true});
|
|
||||||
};
|
|
||||||
|
|
||||||
loadComplete = () => {
|
|
||||||
this.setState({loading: false});
|
|
||||||
};
|
|
||||||
|
|
||||||
handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const term = e.target.value;
|
const term = e.target.value;
|
||||||
this.props.actions.setModalSearchTerm(term);
|
actions.setModalSearchTerm(term);
|
||||||
};
|
}, [actions]);
|
||||||
|
|
||||||
getGroupMembers = debounce(
|
const getGroupMembers = useCallback(
|
||||||
async () => {
|
debounce(
|
||||||
const {actions, groupId} = this.props;
|
async () => {
|
||||||
const {page} = this.state;
|
const newPage = page + 1;
|
||||||
const newPage = page + 1;
|
setPage(newPage);
|
||||||
|
startLoad();
|
||||||
this.setState({page: newPage});
|
await actions.getUsersInGroup(groupId, newPage, USERS_PER_PAGE);
|
||||||
|
loadComplete();
|
||||||
this.startLoad();
|
},
|
||||||
await actions.getUsersInGroup(groupId, newPage, USERS_PER_PAGE);
|
200,
|
||||||
this.loadComplete();
|
false,
|
||||||
},
|
() => {},
|
||||||
200,
|
),
|
||||||
false,
|
[page, groupId, actions, startLoad, loadComplete],
|
||||||
(): void => {},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
onScroll = () => {
|
const onScroll = useCallback(() => {
|
||||||
const scrollHeight = this.divScrollRef.current?.scrollHeight || 0;
|
const scrollHeight = divScrollRef.current?.scrollHeight || 0;
|
||||||
const scrollTop = this.divScrollRef.current?.scrollTop || 0;
|
const scrollTop = divScrollRef.current?.scrollTop || 0;
|
||||||
const clientHeight = this.divScrollRef.current?.clientHeight || 0;
|
const clientHeight = divScrollRef.current?.clientHeight || 0;
|
||||||
|
|
||||||
if (((scrollTop + clientHeight + 30) >= scrollHeight && this.props.group) && (this.props.users.length !== this.props.group.member_count && this.state.loading === false)) {
|
if (((scrollTop + clientHeight + 30) >= scrollHeight && group) && (users.length !== group.member_count && !loading)) {
|
||||||
this.getGroupMembers();
|
getGroupMembers();
|
||||||
}
|
}
|
||||||
};
|
}, [group, users.length, loading, getGroupMembers]);
|
||||||
|
|
||||||
mentionName = () => {
|
useEffect(() => {
|
||||||
const {group} = this.props;
|
const fetchData = async () => {
|
||||||
|
await Promise.all([
|
||||||
|
actions.getGroup(groupId, true),
|
||||||
|
actions.getUsersInGroup(groupId, 0, USERS_PER_PAGE),
|
||||||
|
]);
|
||||||
|
loadComplete();
|
||||||
|
};
|
||||||
|
fetchData();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
actions.setModalSearchTerm('');
|
||||||
|
};
|
||||||
|
}, [groupId, actions, loadComplete]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (group?.member_count !== undefined && group.member_count !== memberCount) {
|
||||||
|
setMemberCount(group.member_count);
|
||||||
|
}
|
||||||
|
}, [group?.member_count]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchTerm === '') {
|
||||||
|
loadComplete();
|
||||||
|
searchTimeoutIdRef.current = 0;
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(searchTimeoutIdRef.current);
|
||||||
|
searchTimeoutIdRef.current = window.setTimeout(
|
||||||
|
async () => {
|
||||||
|
await actions.searchProfiles(searchTerm, {in_group_id: groupId});
|
||||||
|
},
|
||||||
|
Constants.SEARCH_TIMEOUT_MILLISECONDS,
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(searchTimeoutIdRef.current);
|
||||||
|
};
|
||||||
|
}, [searchTerm, groupId, actions, loadComplete]);
|
||||||
|
|
||||||
|
const mentionName = () => {
|
||||||
if (group) {
|
if (group) {
|
||||||
return (
|
return (
|
||||||
<div className='group-mention-name'>
|
<div className='group-mention-name'>
|
||||||
@@ -198,29 +184,28 @@ export default class ViewUserGroupModal extends React.PureComponent<Props, State
|
|||||||
return (<></>);
|
return (<></>);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
const {groupId, group, users, onExited} = this.props;
|
<Modal
|
||||||
|
dialogClassName='a11y__modal view-user-groups-modal'
|
||||||
return (
|
show={show}
|
||||||
<Modal
|
onHide={doHide}
|
||||||
dialogClassName='a11y__modal view-user-groups-modal'
|
onExited={onExited}
|
||||||
show={this.state.show}
|
role='none'
|
||||||
onHide={this.doHide}
|
aria-labelledby='viewUserGroupModalLabel'
|
||||||
onExited={onExited}
|
enforceFocus={false}
|
||||||
role='none'
|
>
|
||||||
aria-labelledby='viewUserGroupModalLabel'
|
<div ref={modalRef}>
|
||||||
>
|
|
||||||
<ViewUserGroupModalHeader
|
<ViewUserGroupModalHeader
|
||||||
onExited={onExited}
|
onExited={onExited}
|
||||||
groupId={groupId}
|
groupId={groupId}
|
||||||
backButtonCallback={this.props.backButtonCallback}
|
backButtonCallback={backButtonCallback}
|
||||||
backButtonAction={this.props.backButtonAction}
|
backButtonAction={backButtonAction}
|
||||||
incrementMemberCount={this.incrementMemberCount}
|
incrementMemberCount={incrementMemberCount}
|
||||||
decrementMemberCount={this.decrementMemberCount}
|
decrementMemberCount={decrementMemberCount}
|
||||||
/>
|
/>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
{this.mentionName()}
|
{mentionName()}
|
||||||
{((users.length === 0 && !this.props.searchTerm && !this.state.loading) || !group) ? (
|
{((users.length === 0 && !searchTerm && !loading) || !group) ? (
|
||||||
<NoResultsIndicator
|
<NoResultsIndicator
|
||||||
variant={NoResultsVariant.UserGroupMembers}
|
variant={NoResultsVariant.UserGroupMembers}
|
||||||
/>
|
/>
|
||||||
@@ -230,8 +215,8 @@ export default class ViewUserGroupModal extends React.PureComponent<Props, State
|
|||||||
<Input
|
<Input
|
||||||
type='text'
|
type='text'
|
||||||
placeholder={defineMessage({id: 'search_bar.searchGroupMembers', defaultMessage: 'Search group members'})}
|
placeholder={defineMessage({id: 'search_bar.searchGroupMembers', defaultMessage: 'Search group members'})}
|
||||||
onChange={this.handleSearch}
|
onChange={handleSearch}
|
||||||
value={this.props.searchTerm}
|
value={searchTerm}
|
||||||
data-testid='searchInput'
|
data-testid='searchInput'
|
||||||
className={'user-group-search-input'}
|
className={'user-group-search-input'}
|
||||||
inputPrefix={<i className={'icon icon-magnify'}/>}
|
inputPrefix={<i className={'icon icon-magnify'}/>}
|
||||||
@@ -239,8 +224,8 @@ export default class ViewUserGroupModal extends React.PureComponent<Props, State
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className='user-groups-modal__content group-member-list'
|
className='user-groups-modal__content group-member-list'
|
||||||
onScroll={this.onScroll}
|
onScroll={onScroll}
|
||||||
ref={this.divScrollRef}
|
ref={divScrollRef}
|
||||||
>
|
>
|
||||||
{(users.length !== 0) &&
|
{(users.length !== 0) &&
|
||||||
<h2 className='group-member-count'>
|
<h2 className='group-member-count'>
|
||||||
@@ -248,36 +233,36 @@ export default class ViewUserGroupModal extends React.PureComponent<Props, State
|
|||||||
id='view_user_group_modal.memberCount'
|
id='view_user_group_modal.memberCount'
|
||||||
defaultMessage='{member_count} {member_count, plural, one {Member} other {Members}}'
|
defaultMessage='{member_count} {member_count, plural, one {Member} other {Members}}'
|
||||||
values={{
|
values={{
|
||||||
member_count: this.state.memberCount,
|
member_count: memberCount,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</h2>
|
</h2>
|
||||||
}
|
}
|
||||||
{(users.length === 0 && this.props.searchTerm) &&
|
{(users.length === 0 && searchTerm) &&
|
||||||
<NoResultsIndicator
|
<NoResultsIndicator
|
||||||
variant={NoResultsVariant.Search}
|
variant={NoResultsVariant.Search}
|
||||||
titleValues={{channelName: `${this.props.searchTerm}`}}
|
titleValues={{channelName: `${searchTerm}`}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{users.map((user) => {
|
{users.map((user) => (
|
||||||
return (
|
<ViewUserGroupListItem
|
||||||
<ViewUserGroupListItem
|
groupId={groupId}
|
||||||
groupId={groupId}
|
user={user}
|
||||||
user={user}
|
decrementMemberCount={decrementMemberCount}
|
||||||
decrementMemberCount={this.decrementMemberCount}
|
key={user.id}
|
||||||
key={user.id}
|
/>
|
||||||
/>
|
))}
|
||||||
);
|
|
||||||
})}
|
|
||||||
{
|
{
|
||||||
this.state.loading &&
|
loading &&
|
||||||
<LoadingScreen/>
|
<LoadingScreen/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
</Modal>
|
</div>
|
||||||
);
|
</Modal>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default ViewUserGroupModal;
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ const ViewUserGroupModalHeader = ({
|
|||||||
}, [backButtonCallback, onExited]);
|
}, [backButtonCallback, onExited]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal.Header closeButton={true}>
|
<Modal.Header>
|
||||||
<div className='d-flex align-items-center'>
|
<div className='d-flex align-items-center'>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
@@ -179,6 +179,20 @@ const ViewUserGroupModalHeader = ({
|
|||||||
{restoreGroupButton()}
|
{restoreGroupButton()}
|
||||||
{subMenuButton()}
|
{subMenuButton()}
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='close'
|
||||||
|
onClick={onExited}
|
||||||
|
aria-label={formatMessage({id: 'generic.close', defaultMessage: 'Close'})}
|
||||||
|
>
|
||||||
|
<span aria-hidden='true'>{'×'}</span>
|
||||||
|
<span className='sr-only'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='generic.close'
|
||||||
|
defaultMessage='Close'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5716,6 +5716,8 @@
|
|||||||
"user_group_popover.memberCount": "{member_count} {member_count, plural, one {Member} other {Members}}",
|
"user_group_popover.memberCount": "{member_count} {member_count, plural, one {Member} other {Members}}",
|
||||||
"user_group_popover.openGroupModal": "View full group info",
|
"user_group_popover.openGroupModal": "View full group info",
|
||||||
"user_group_popover.searchGroupMembers": "Search members",
|
"user_group_popover.searchGroupMembers": "Search members",
|
||||||
|
"user_groups_list.groupAriaLabel": "{group_name} group",
|
||||||
|
"user_groups_list.menuAriaLabel": "{group_name} actions",
|
||||||
"user_groups_modal.addPeople": "Add people",
|
"user_groups_modal.addPeople": "Add people",
|
||||||
"user_groups_modal.addPeopleTitle": "Add people to {group}",
|
"user_groups_modal.addPeopleTitle": "Add people to {group}",
|
||||||
"user_groups_modal.allGroups": "All Groups",
|
"user_groups_modal.allGroups": "All Groups",
|
||||||
@@ -5726,7 +5728,7 @@
|
|||||||
"user_groups_modal.createTitle": "Create Group",
|
"user_groups_modal.createTitle": "Create Group",
|
||||||
"user_groups_modal.editDetails": "Edit Details",
|
"user_groups_modal.editDetails": "Edit Details",
|
||||||
"user_groups_modal.editGroupTitle": "Edit Group Details",
|
"user_groups_modal.editGroupTitle": "Edit Group Details",
|
||||||
"user_groups_modal.filterAriaLabel": "Groups Filter Menu",
|
"user_groups_modal.filterAriaLabel": "Groups Filter",
|
||||||
"user_groups_modal.goBackLabel": "Back",
|
"user_groups_modal.goBackLabel": "Back",
|
||||||
"user_groups_modal.joinGroup": "Join Group",
|
"user_groups_modal.joinGroup": "Join Group",
|
||||||
"user_groups_modal.leaveGroup": "Leave Group",
|
"user_groups_modal.leaveGroup": "Leave Group",
|
||||||
@@ -5740,8 +5742,10 @@
|
|||||||
"user_groups_modal.myGroups": "My Groups",
|
"user_groups_modal.myGroups": "My Groups",
|
||||||
"user_groups_modal.name": "Name",
|
"user_groups_modal.name": "Name",
|
||||||
"user_groups_modal.nameIsEmpty": "Name is a required field.",
|
"user_groups_modal.nameIsEmpty": "Name is a required field.",
|
||||||
|
"user_groups_modal.noSearchResults": "No groups found",
|
||||||
"user_groups_modal.restoreGroup": "Restore Group",
|
"user_groups_modal.restoreGroup": "Restore Group",
|
||||||
"user_groups_modal.searchGroups": "Search Groups",
|
"user_groups_modal.searchGroups": "Search Groups",
|
||||||
|
"user_groups_modal.searchResults": "{count} groups found",
|
||||||
"user_groups_modal.showAllGroups": "Show: All Groups",
|
"user_groups_modal.showAllGroups": "Show: All Groups",
|
||||||
"user_groups_modal.showArchivedGroups": "Show: Archived Groups",
|
"user_groups_modal.showArchivedGroups": "Show: Archived Groups",
|
||||||
"user_groups_modal.showMyGroups": "Show: My Groups",
|
"user_groups_modal.showMyGroups": "Show: My Groups",
|
||||||
@@ -6237,6 +6241,7 @@
|
|||||||
"view_image.zoom_in": "Zoom In",
|
"view_image.zoom_in": "Zoom In",
|
||||||
"view_image.zoom_out": "Zoom Out",
|
"view_image.zoom_out": "Zoom Out",
|
||||||
"view_image.zoom_reset": "Reset Zoom",
|
"view_image.zoom_reset": "Reset Zoom",
|
||||||
|
"view_user_group_header_sub_menu.menuAriaLabel": "User group actions",
|
||||||
"view_user_group_modal.ldapSynced": "AD/LDAP SYNCED",
|
"view_user_group_modal.ldapSynced": "AD/LDAP SYNCED",
|
||||||
"view_user_group_modal.memberCount": "{member_count} {member_count, plural, one {Member} other {Members}}",
|
"view_user_group_modal.memberCount": "{member_count} {member_count, plural, one {Member} other {Members}}",
|
||||||
"view_user_group_modal.pluginSynced": "Plugin SYNCED",
|
"view_user_group_modal.pluginSynced": "Plugin SYNCED",
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ function isElementVisible(element: HTMLElement): boolean {
|
|||||||
if (
|
if (
|
||||||
style.display === 'none' ||
|
style.display === 'none' ||
|
||||||
style.visibility === 'hidden' ||
|
style.visibility === 'hidden' ||
|
||||||
style.opacity === '0' ||
|
style.pointerEvents === 'none' ||
|
||||||
currentElement.hasAttribute('hidden')
|
currentElement.hasAttribute('hidden')
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user