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