Mono repo -> Master (#22553)
Combines the following repositories into one: https://github.com/mattermost/mattermost-server https://github.com/mattermost/mattermost-webapp https://github.com/mattermost/focalboard https://github.com/mattermost/mattermost-plugin-playbooks
Этот коммит содержится в:
@@ -0,0 +1,336 @@
|
||||
// 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]}
|
||||
enforceFocus={true}
|
||||
id="userGroupsModal"
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onExited={[MockFunction]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="dialog"
|
||||
show={true}
|
||||
>
|
||||
<Connect(Component)
|
||||
backButtonAction={[MockFunction]}
|
||||
onExited={[MockFunction]}
|
||||
/>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
componentClass="div"
|
||||
>
|
||||
<div
|
||||
className="user-groups-search"
|
||||
>
|
||||
<ForwardRef
|
||||
className="user-group-search-input"
|
||||
data-testid="searchInput"
|
||||
inputPrefix={
|
||||
<i
|
||||
className="icon icon-magnify"
|
||||
/>
|
||||
}
|
||||
onChange={[Function]}
|
||||
placeholder="Search Groups"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="more-modal__dropdown"
|
||||
>
|
||||
<MenuWrapper
|
||||
animationComponent={[Function]}
|
||||
className=""
|
||||
id="groupsFilterDropdown"
|
||||
>
|
||||
<a>
|
||||
<span>
|
||||
Show: All Groups
|
||||
</span>
|
||||
<span
|
||||
className="icon icon-chevron-down"
|
||||
/>
|
||||
</a>
|
||||
<Menu
|
||||
ariaLabel="Groups Filter Menu"
|
||||
openLeft={false}
|
||||
>
|
||||
<MenuItemAction
|
||||
buttonClass="groups-filter-btn"
|
||||
id="groupsDropdownAll"
|
||||
onClick={[Function]}
|
||||
rightDecorator={
|
||||
<i
|
||||
className="icon icon-check"
|
||||
/>
|
||||
}
|
||||
show={true}
|
||||
text="All Groups"
|
||||
/>
|
||||
<MenuItemAction
|
||||
buttonClass="groups-filter-btn"
|
||||
id="groupsDropdownMy"
|
||||
onClick={[Function]}
|
||||
rightDecorator={false}
|
||||
show={true}
|
||||
text="My Groups"
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
</div>
|
||||
<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,
|
||||
},
|
||||
]
|
||||
}
|
||||
loading={true}
|
||||
onExited={[MockFunction]}
|
||||
onScroll={[Function]}
|
||||
searchTerm=""
|
||||
/>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
`;
|
||||
|
||||
exports[`component/user_groups_modal should match snapshot with groups, myGroups selected 1`] = `
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-labelledby="userGroupsModalLabel"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
dialogClassName="a11y__modal user-groups-modal"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
id="userGroupsModal"
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onExited={[MockFunction]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="dialog"
|
||||
show={true}
|
||||
>
|
||||
<Connect(Component)
|
||||
backButtonAction={[MockFunction]}
|
||||
onExited={[MockFunction]}
|
||||
/>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
componentClass="div"
|
||||
>
|
||||
<div
|
||||
className="user-groups-search"
|
||||
>
|
||||
<ForwardRef
|
||||
className="user-group-search-input"
|
||||
data-testid="searchInput"
|
||||
inputPrefix={
|
||||
<i
|
||||
className="icon icon-magnify"
|
||||
/>
|
||||
}
|
||||
onChange={[Function]}
|
||||
placeholder="Search Groups"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="more-modal__dropdown"
|
||||
>
|
||||
<MenuWrapper
|
||||
animationComponent={[Function]}
|
||||
className=""
|
||||
id="groupsFilterDropdown"
|
||||
>
|
||||
<a>
|
||||
<span>
|
||||
Show: My Groups
|
||||
</span>
|
||||
<span
|
||||
className="icon icon-chevron-down"
|
||||
/>
|
||||
</a>
|
||||
<Menu
|
||||
ariaLabel="Groups Filter Menu"
|
||||
openLeft={false}
|
||||
>
|
||||
<MenuItemAction
|
||||
buttonClass="groups-filter-btn"
|
||||
id="groupsDropdownAll"
|
||||
onClick={[Function]}
|
||||
rightDecorator={false}
|
||||
show={true}
|
||||
text="All Groups"
|
||||
/>
|
||||
<MenuItemAction
|
||||
buttonClass="groups-filter-btn"
|
||||
id="groupsDropdownMy"
|
||||
onClick={[Function]}
|
||||
rightDecorator={
|
||||
<i
|
||||
className="icon icon-check"
|
||||
/>
|
||||
}
|
||||
show={true}
|
||||
text="My Groups"
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
</div>
|
||||
<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,
|
||||
},
|
||||
]
|
||||
}
|
||||
loading={true}
|
||||
onExited={[MockFunction]}
|
||||
onScroll={[Function]}
|
||||
searchTerm=""
|
||||
/>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
`;
|
||||
|
||||
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]}
|
||||
enforceFocus={true}
|
||||
id="userGroupsModal"
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onExited={[MockFunction]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="dialog"
|
||||
show={true}
|
||||
>
|
||||
<Connect(Component)
|
||||
backButtonAction={[MockFunction]}
|
||||
onExited={[MockFunction]}
|
||||
/>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
componentClass="div"
|
||||
>
|
||||
<NoResultsIndicator
|
||||
variant="UserGroups"
|
||||
/>
|
||||
<ADLDAPUpsellBanner />
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
`;
|
||||
@@ -0,0 +1,233 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {mount, ReactWrapper} from 'enzyme';
|
||||
import * as reactRedux from 'react-redux';
|
||||
import {act} from '@testing-library/react';
|
||||
|
||||
import mockStore from 'tests/test_store';
|
||||
|
||||
import {CloudProducts, LicenseSkus} from 'utils/constants';
|
||||
|
||||
import ADLDAPUpsellBanner from './ad_ldap_upsell_banner';
|
||||
|
||||
const actImmediate = (wrapper: ReactWrapper) =>
|
||||
act(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
setImmediate(() => {
|
||||
wrapper.update();
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
describe('component/user_groups_modal/ad_ldap_upsell_banner', () => {
|
||||
const useDispatchMock = jest.spyOn(reactRedux, 'useDispatch');
|
||||
const useSelectorMock = jest.spyOn(reactRedux, 'useSelector');
|
||||
|
||||
beforeEach(() => {
|
||||
useDispatchMock.mockClear();
|
||||
useSelectorMock.mockClear();
|
||||
});
|
||||
|
||||
const initState = {
|
||||
entities: {
|
||||
general: {
|
||||
license: {
|
||||
Cloud: 'false',
|
||||
SkuShortName: LicenseSkus.Professional,
|
||||
ExpiresAt: 100000000,
|
||||
},
|
||||
config: {},
|
||||
},
|
||||
cloud: {},
|
||||
admin: {
|
||||
prevTrialLicense: {
|
||||
IsLicensed: 'false',
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'user1',
|
||||
profiles: {
|
||||
user1: {
|
||||
id: 'user1',
|
||||
roles: 'system_admin',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test('should display for admin users on professional with option to start trial if no self-hosted trial before', () => {
|
||||
const store = mockStore(initState);
|
||||
const dummyDispatch = jest.fn();
|
||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
|
||||
const wrapper = mount(
|
||||
<reactRedux.Provider store={store}>
|
||||
<ADLDAPUpsellBanner/>
|
||||
</reactRedux.Provider>,
|
||||
);
|
||||
|
||||
expect(wrapper.find('#ad_ldap_upsell_banner')).toHaveLength(1);
|
||||
expect(wrapper.find('.ad-ldap-banner-btn').text()).toEqual('Try free for 30 days');
|
||||
});
|
||||
|
||||
test('should display for admin users on professional with option to start trial if no cloud trial before', async () => {
|
||||
const state = JSON.parse(JSON.stringify(initState));
|
||||
state.entities.admin = {};
|
||||
state.entities.general.license = {
|
||||
Cloud: 'true',
|
||||
ExpiresAt: 100000000,
|
||||
};
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_professional',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 0,
|
||||
},
|
||||
products: {
|
||||
prod_professional: {
|
||||
id: 'prod_professional',
|
||||
sku: CloudProducts.PROFESSIONAL,
|
||||
},
|
||||
},
|
||||
};
|
||||
const store = mockStore(state);
|
||||
const dummyDispatch = jest.fn();
|
||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
|
||||
const wrapper = mount(
|
||||
<reactRedux.Provider store={store}>
|
||||
<ADLDAPUpsellBanner/>
|
||||
</reactRedux.Provider>,
|
||||
);
|
||||
|
||||
await actImmediate(wrapper);
|
||||
|
||||
expect(wrapper.find('#ad_ldap_upsell_banner')).toHaveLength(1);
|
||||
expect(wrapper.find('.ad-ldap-banner-btn').text()).toEqual('Try free for 30 days');
|
||||
});
|
||||
|
||||
test('should display for admin users on professional with option to contact sales if self-hosted trialed before', () => {
|
||||
const state = JSON.parse(JSON.stringify(initState));
|
||||
state.entities.admin.prevTrialLicense.IsLicensed = 'true';
|
||||
const store = mockStore(state);
|
||||
const dummyDispatch = jest.fn();
|
||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
|
||||
const wrapper = mount(
|
||||
<reactRedux.Provider store={store}>
|
||||
<ADLDAPUpsellBanner/>
|
||||
</reactRedux.Provider>,
|
||||
);
|
||||
|
||||
expect(wrapper.find('#ad_ldap_upsell_banner')).toHaveLength(1);
|
||||
expect(wrapper.find('.ad-ldap-banner-btn').text()).toEqual('Contact sales to use');
|
||||
});
|
||||
|
||||
test('should display for admin users on professional with option to contact sales if cloud trialed before', async () => {
|
||||
const state = JSON.parse(JSON.stringify(initState));
|
||||
state.entities.admin = {};
|
||||
state.entities.general.license = {
|
||||
Cloud: 'true',
|
||||
ExpiresAt: 100000000,
|
||||
};
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_professional',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 1,
|
||||
},
|
||||
products: {
|
||||
prod_professional: {
|
||||
id: 'prod_professional',
|
||||
sku: CloudProducts.PROFESSIONAL,
|
||||
},
|
||||
},
|
||||
};
|
||||
const store = mockStore(state);
|
||||
const dummyDispatch = jest.fn();
|
||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
|
||||
const wrapper = mount(
|
||||
<reactRedux.Provider store={store}>
|
||||
<ADLDAPUpsellBanner/>
|
||||
</reactRedux.Provider>,
|
||||
);
|
||||
|
||||
await actImmediate(wrapper);
|
||||
|
||||
expect(wrapper.find('#ad_ldap_upsell_banner')).toHaveLength(1);
|
||||
expect(wrapper.find('.ad-ldap-banner-btn').text()).toEqual('Contact sales to use');
|
||||
});
|
||||
|
||||
test('should not display for non admin users', () => {
|
||||
const state = JSON.parse(JSON.stringify(initState));
|
||||
state.entities.users.profiles.user1.roles = 'system_user';
|
||||
const store = mockStore(state);
|
||||
const dummyDispatch = jest.fn();
|
||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
|
||||
const wrapper = mount(
|
||||
<reactRedux.Provider store={store}>
|
||||
<ADLDAPUpsellBanner/>
|
||||
</reactRedux.Provider>,
|
||||
);
|
||||
|
||||
expect(wrapper.find('#ad_ldap_upsell_banner')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should not display for non self-hosted professional users', () => {
|
||||
const state = JSON.parse(JSON.stringify(initState));
|
||||
state.entities.general.license.SkuShortName = LicenseSkus.Enterprise;
|
||||
const store = mockStore(state);
|
||||
const dummyDispatch = jest.fn();
|
||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
|
||||
const wrapper = mount(
|
||||
<reactRedux.Provider store={store}>
|
||||
<ADLDAPUpsellBanner/>
|
||||
</reactRedux.Provider>,
|
||||
);
|
||||
|
||||
expect(wrapper.find('#ad_ldap_upsell_banner')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should not display for non cloud professional users', async () => {
|
||||
const state = JSON.parse(JSON.stringify(initState));
|
||||
state.entities.admin = {};
|
||||
state.entities.general.license = {
|
||||
Cloud: 'true',
|
||||
ExpiresAt: 100000000,
|
||||
};
|
||||
state.entities.cloud = {
|
||||
subscription: {
|
||||
product_id: 'prod_starter',
|
||||
is_free_trial: 'false',
|
||||
trial_end_at: 1,
|
||||
},
|
||||
products: {
|
||||
prod_starter: {
|
||||
id: 'prod_starter',
|
||||
sku: CloudProducts.STARTER,
|
||||
},
|
||||
},
|
||||
};
|
||||
const store = mockStore(state);
|
||||
const dummyDispatch = jest.fn();
|
||||
useDispatchMock.mockReturnValue(dummyDispatch);
|
||||
|
||||
const wrapper = mount(
|
||||
<reactRedux.Provider store={store}>
|
||||
<ADLDAPUpsellBanner/>
|
||||
</reactRedux.Provider>,
|
||||
);
|
||||
|
||||
await actImmediate(wrapper);
|
||||
|
||||
expect(wrapper.find('#ad_ldap_upsell_banner')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,157 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import moment from 'moment';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import StartTrialBtn from 'components/learn_more_trial_modal/start_trial_btn';
|
||||
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
|
||||
|
||||
import {isAdmin} from 'mattermost-redux/utils/user_utils';
|
||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getPrevTrialLicense} from 'mattermost-redux/actions/admin';
|
||||
import {getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {checkHadPriorTrial, isCurrentLicenseCloud, getSubscriptionProduct as selectSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud';
|
||||
import {getBrowserTimezone} from 'utils/timezone';
|
||||
|
||||
import {CloudProducts, LicenseLinks, LicenseSkus} from 'utils/constants';
|
||||
|
||||
import CloudStartTrialButton from 'components/cloud_start_trial/cloud_start_trial_btn';
|
||||
|
||||
import {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
function ADLDAPUpsellBanner() {
|
||||
const [show, setShow] = useState(true);
|
||||
const [confirmed, setConfirmed] = useState(false);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const {formatMessage} = useIntl();
|
||||
const openSalesLink = useOpenSalesLink();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getPrevTrialLicense());
|
||||
}, []);
|
||||
|
||||
const isAdminUser = isAdmin(useSelector(getCurrentUser).roles);
|
||||
const isCloud = useSelector(isCurrentLicenseCloud);
|
||||
const currentLicense = useSelector(getLicense);
|
||||
const prevTrialLicense = useSelector((state: GlobalState) => state.entities.admin.prevTrialLicense);
|
||||
const product = useSelector(selectSubscriptionProduct);
|
||||
|
||||
const prevCloudTrialed = useSelector(checkHadPriorTrial);
|
||||
const prevSelfHostedTrialed = prevTrialLicense?.IsLicensed === 'true';
|
||||
const prevTrialed = prevSelfHostedTrialed || prevCloudTrialed;
|
||||
|
||||
const isSelfHostedProfessional = currentLicense?.SkuShortName === LicenseSkus.Professional;
|
||||
const isCloudProfessional = product?.sku === CloudProducts.PROFESSIONAL;
|
||||
const isProfessional = isSelfHostedProfessional || isCloudProfessional;
|
||||
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentLicenseEndDate = new Date(parseInt(currentLicense?.ExpiresAt, 10));
|
||||
|
||||
const openLink = () => {
|
||||
if (isCloud) {
|
||||
openSalesLink();
|
||||
} else {
|
||||
window.open(LicenseLinks.CONTACT_SALES, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
const confirmBanner = (
|
||||
<div className='ad_ldap_upsell_confirm'>
|
||||
<div className='upsell-confirm-backdrop'/>
|
||||
<div className='upsell-confirm-foreground'>
|
||||
<p className='title'>{formatMessage({id: 'adldap_upsell_banner.confirm.title', defaultMessage: 'Your trial has started!'})}</p>
|
||||
<p className='subtitle'>{formatMessage({id: 'adldap_upsell_banner.confirm.license_trial', defaultMessage: 'Welcome to your Mattermost Enterprise trial! It expires on {endDate}. You now have access to high-security Enterprise features, for free.'}, {endDate: moment(currentLicenseEndDate).tz(getBrowserTimezone()).format('MMMM Do YYYY')})}</p>
|
||||
<div className='btns-container'>
|
||||
<button
|
||||
className='confrim-btn learn-more'
|
||||
onClick={openLink}
|
||||
>
|
||||
{formatMessage({id: 'adldap_upsell_banner.confirm.learn_more', defaultMessage: 'Learn more'})}
|
||||
</button>
|
||||
<button
|
||||
className='confrim-btn continue'
|
||||
onClick={() => setShow(false)}
|
||||
>
|
||||
{formatMessage({id: 'adldap_upsell_banner.confirm.continue', defaultMessage: 'Continue'})}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (confirmed) {
|
||||
return confirmBanner;
|
||||
}
|
||||
|
||||
if (!isAdminUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isProfessional) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let btn = (
|
||||
<StartTrialBtn
|
||||
btnClass='ad-ldap-banner-btn'
|
||||
message={formatMessage({id: 'adldap_upsell_banner.trial_btn', defaultMessage: 'Try free for 30 days'})}
|
||||
telemetryId={'start_self-hosted_trial_from_adldap_upsell_banner'}
|
||||
renderAsButton={true}
|
||||
onClick={() => setConfirmed(true)}
|
||||
/>);
|
||||
|
||||
if (isCloud) {
|
||||
btn = (
|
||||
<CloudStartTrialButton
|
||||
extraClass='ad-ldap-banner-btn'
|
||||
message={formatMessage({id: 'adldap_upsell_banner.trial_btn', defaultMessage: 'Try free for 30 days'})}
|
||||
telemetryId={'start_cloud_trial_from_adldap_upsell_banner'}
|
||||
onClick={() => setConfirmed(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (prevTrialed) {
|
||||
btn = (
|
||||
<button
|
||||
className='ad-ldap-banner-btn'
|
||||
onClick={openLink}
|
||||
>
|
||||
{formatMessage({id: 'adldap_upsell_banner.sales_btn', defaultMessage: 'Contact sales to use'})}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
id='ad_ldap_upsell_banner'
|
||||
className='ad_ldap_upsell_banner'
|
||||
>
|
||||
<div className='message'>
|
||||
<i className='icon icon-information-outline'/>
|
||||
{formatMessage({id: 'adldap_upsell_banner.banner_message', defaultMessage: 'AD/LDAP group sync creates groups faster'})}
|
||||
</div>
|
||||
<div className='btn-container'>
|
||||
{btn}
|
||||
<button
|
||||
type='button'
|
||||
aria-label='Close'
|
||||
className='banner-close'
|
||||
onClick={() => setShow(false)}
|
||||
>
|
||||
<span aria-hidden='true'>{'×'}</span>
|
||||
<span className='sr-only'>{'Close'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ADLDAPUpsellBanner;
|
||||
74
webapp/channels/src/components/user_groups_modal/index.ts
Обычный файл
74
webapp/channels/src/components/user_groups_modal/index.ts
Обычный файл
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators, Dispatch, ActionCreatorsMapObject} from 'redux';
|
||||
|
||||
import {ActionFunc, GenericAction} from 'mattermost-redux/types/actions';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getAllAssociatedGroupsForReference, getMyAllowReferencedGroups, searchAllowReferencedGroups, searchMyAllowReferencedGroups} from 'mattermost-redux/selectors/entities/groups';
|
||||
import {getGroups, getGroupsByUserIdPaginated, searchGroups} from 'mattermost-redux/actions/groups';
|
||||
import {Group, GroupSearachParams} from '@mattermost/types/groups';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
import {isModalOpen} from 'selectors/views/modals';
|
||||
import {setModalSearchTerm} from 'actions/views/search';
|
||||
|
||||
import UserGroupsModal from './user_groups_modal';
|
||||
|
||||
type Actions = {
|
||||
getGroups: (
|
||||
filterAllowReference?: boolean,
|
||||
page?: number,
|
||||
perPage?: number,
|
||||
includeMemberCount?: boolean
|
||||
) => Promise<{data: Group[]}>;
|
||||
setModalSearchTerm: (term: string) => void;
|
||||
getGroupsByUserIdPaginated: (
|
||||
userId: string,
|
||||
filterAllowReference?: boolean,
|
||||
page?: number,
|
||||
perPage?: number,
|
||||
includeMemberCount?: boolean
|
||||
) => Promise<{data: Group[]}>;
|
||||
searchGroups: (
|
||||
params: GroupSearachParams,
|
||||
) => Promise<{data: Group[]}>;
|
||||
};
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const searchTerm = state.views.search.modalSearch;
|
||||
|
||||
let groups: Group[] = [];
|
||||
let myGroups: Group[] = [];
|
||||
if (searchTerm) {
|
||||
groups = searchAllowReferencedGroups(state, searchTerm);
|
||||
myGroups = searchMyAllowReferencedGroups(state, searchTerm);
|
||||
} else {
|
||||
groups = getAllAssociatedGroupsForReference(state);
|
||||
myGroups = getMyAllowReferencedGroups(state);
|
||||
}
|
||||
|
||||
return {
|
||||
showModal: isModalOpen(state, ModalIdentifiers.USER_GROUPS),
|
||||
groups,
|
||||
searchTerm,
|
||||
myGroups,
|
||||
currentUserId: getCurrentUserId(state),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc | GenericAction>, Actions>({
|
||||
getGroups,
|
||||
setModalSearchTerm,
|
||||
getGroupsByUserIdPaginated,
|
||||
searchGroups,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(UserGroupsModal);
|
||||
@@ -0,0 +1,278 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`component/user_groups_modal should match snapshot with groups 1`] = `
|
||||
<div
|
||||
className="user-groups-modal__content user-groups-list"
|
||||
onScroll={[MockFunction]}
|
||||
style={
|
||||
Object {
|
||||
"overflow": "overlay",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="group-row"
|
||||
key="group0"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="group-display-name"
|
||||
>
|
||||
Group 0
|
||||
</span>
|
||||
<span
|
||||
className="group-name"
|
||||
>
|
||||
@
|
||||
group0
|
||||
</span>
|
||||
<div
|
||||
className="group-member-count"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="{member_count} {member_count, plural, one {member} other {members}}"
|
||||
id="user_groups_modal.memberCount"
|
||||
values={
|
||||
Object {
|
||||
"member_count": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="group-action"
|
||||
>
|
||||
<MenuWrapper
|
||||
animationComponent={[Function]}
|
||||
className=""
|
||||
id="customWrapper-group0"
|
||||
isDisabled={false}
|
||||
stopPropagationOnToggle={true}
|
||||
>
|
||||
<button
|
||||
className="action-wrapper"
|
||||
>
|
||||
<i
|
||||
className="icon icon-dots-vertical"
|
||||
/>
|
||||
</button>
|
||||
<Menu
|
||||
ariaLabel="User Actions Menu"
|
||||
className="group-actions-menu"
|
||||
openLeft={true}
|
||||
openUp={false}
|
||||
>
|
||||
<MenuGroup>
|
||||
<MenuItemAction
|
||||
disabled={false}
|
||||
icon={
|
||||
<i
|
||||
className="icon-account-multiple-outline"
|
||||
/>
|
||||
}
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="View Group"
|
||||
/>
|
||||
</MenuGroup>
|
||||
<MenuGroup>
|
||||
<MenuItemAction
|
||||
disabled={false}
|
||||
icon={
|
||||
<i
|
||||
className="icon-archive-outline"
|
||||
/>
|
||||
}
|
||||
isDangerous={true}
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Archive Group"
|
||||
/>
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="group-row"
|
||||
key="group1"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="group-display-name"
|
||||
>
|
||||
Group 1
|
||||
</span>
|
||||
<span
|
||||
className="group-name"
|
||||
>
|
||||
@
|
||||
group1
|
||||
</span>
|
||||
<div
|
||||
className="group-member-count"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="{member_count} {member_count, plural, one {member} other {members}}"
|
||||
id="user_groups_modal.memberCount"
|
||||
values={
|
||||
Object {
|
||||
"member_count": 2,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="group-action"
|
||||
>
|
||||
<MenuWrapper
|
||||
animationComponent={[Function]}
|
||||
className=""
|
||||
id="customWrapper-group1"
|
||||
isDisabled={false}
|
||||
stopPropagationOnToggle={true}
|
||||
>
|
||||
<button
|
||||
className="action-wrapper"
|
||||
>
|
||||
<i
|
||||
className="icon icon-dots-vertical"
|
||||
/>
|
||||
</button>
|
||||
<Menu
|
||||
ariaLabel="User Actions Menu"
|
||||
className="group-actions-menu"
|
||||
openLeft={true}
|
||||
openUp={true}
|
||||
>
|
||||
<MenuGroup>
|
||||
<MenuItemAction
|
||||
disabled={false}
|
||||
icon={
|
||||
<i
|
||||
className="icon-account-multiple-outline"
|
||||
/>
|
||||
}
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="View Group"
|
||||
/>
|
||||
</MenuGroup>
|
||||
<MenuGroup>
|
||||
<MenuItemAction
|
||||
disabled={false}
|
||||
icon={
|
||||
<i
|
||||
className="icon-archive-outline"
|
||||
/>
|
||||
}
|
||||
isDangerous={true}
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Archive Group"
|
||||
/>
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="group-row"
|
||||
key="group2"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<span
|
||||
className="group-display-name"
|
||||
>
|
||||
Group 2
|
||||
</span>
|
||||
<span
|
||||
className="group-name"
|
||||
>
|
||||
@
|
||||
group2
|
||||
</span>
|
||||
<div
|
||||
className="group-member-count"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="{member_count} {member_count, plural, one {member} other {members}}"
|
||||
id="user_groups_modal.memberCount"
|
||||
values={
|
||||
Object {
|
||||
"member_count": 3,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="group-action"
|
||||
>
|
||||
<MenuWrapper
|
||||
animationComponent={[Function]}
|
||||
className=""
|
||||
id="customWrapper-group2"
|
||||
isDisabled={false}
|
||||
stopPropagationOnToggle={true}
|
||||
>
|
||||
<button
|
||||
className="action-wrapper"
|
||||
>
|
||||
<i
|
||||
className="icon icon-dots-vertical"
|
||||
/>
|
||||
</button>
|
||||
<Menu
|
||||
ariaLabel="User Actions Menu"
|
||||
className="group-actions-menu"
|
||||
openLeft={true}
|
||||
openUp={true}
|
||||
>
|
||||
<MenuGroup>
|
||||
<MenuItemAction
|
||||
disabled={false}
|
||||
icon={
|
||||
<i
|
||||
className="icon-account-multiple-outline"
|
||||
/>
|
||||
}
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="View Group"
|
||||
/>
|
||||
</MenuGroup>
|
||||
<MenuGroup>
|
||||
<MenuItemAction
|
||||
disabled={false}
|
||||
icon={
|
||||
<i
|
||||
className="icon-archive-outline"
|
||||
/>
|
||||
}
|
||||
isDangerous={true}
|
||||
onClick={[Function]}
|
||||
show={true}
|
||||
text="Archive Group"
|
||||
/>
|
||||
</MenuGroup>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
</div>
|
||||
</div>
|
||||
<ADLDAPUpsellBanner />
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`component/user_groups_modal should match snapshot without groups 1`] = `
|
||||
<div
|
||||
className="user-groups-modal__content user-groups-list"
|
||||
onScroll={[MockFunction]}
|
||||
style={
|
||||
Object {
|
||||
"overflow": "overlay",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ADLDAPUpsellBanner />
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators, Dispatch, ActionCreatorsMapObject} from 'redux';
|
||||
|
||||
import {ActionFunc, ActionResult, GenericAction} from 'mattermost-redux/types/actions';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
import {archiveGroup} from 'mattermost-redux/actions/groups';
|
||||
import {ModalData} from 'types/actions';
|
||||
import {openModal} from 'actions/views/modals';
|
||||
import {getGroupListPermissions} from 'mattermost-redux/selectors/entities/roles';
|
||||
|
||||
import UserGroupsList from './user_groups_list';
|
||||
|
||||
type Actions = {
|
||||
openModal: <P>(modalData: ModalData<P>) => void;
|
||||
archiveGroup: (groupId: string) => Promise<ActionResult>;
|
||||
};
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const groupPermissionsMap = getGroupListPermissions(state);
|
||||
return {
|
||||
groupPermissionsMap,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc | GenericAction>, Actions>({
|
||||
openModal,
|
||||
archiveGroup,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps, null, {forwardRef: true})(UserGroupsList);
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import {Group, GroupPermissions} from '@mattermost/types/groups';
|
||||
|
||||
import UserGroupsList from './user_groups_list';
|
||||
|
||||
describe('component/user_groups_modal', () => {
|
||||
const baseProps = {
|
||||
onExited: jest.fn(),
|
||||
onScroll: jest.fn(),
|
||||
groups: [],
|
||||
searchTerm: '',
|
||||
backButtonAction: jest.fn(),
|
||||
groupPermissionsMap: {},
|
||||
loading: false,
|
||||
actions: {
|
||||
openModal: jest.fn(),
|
||||
archiveGroup: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
function getGroups(numberOfGroups: number) {
|
||||
const groups: Group[] = [];
|
||||
for (let i = 0; i < numberOfGroups; i++) {
|
||||
groups.push({
|
||||
id: `group${i}`,
|
||||
name: `group${i}`,
|
||||
display_name: `Group ${i}`,
|
||||
description: `Group ${i} description`,
|
||||
source: 'custom',
|
||||
remote_id: null,
|
||||
create_at: 1637349374137,
|
||||
update_at: 1637349374137,
|
||||
delete_at: 0,
|
||||
has_syncables: false,
|
||||
member_count: i + 1,
|
||||
allow_reference: true,
|
||||
scheme_admin: false,
|
||||
});
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
function getPermissions(groups: Group[]) {
|
||||
const groupPermissionsMap: Record<string, GroupPermissions> = {};
|
||||
groups.forEach((g) => {
|
||||
groupPermissionsMap[g.id] = {
|
||||
can_delete: true,
|
||||
can_manage_members: true,
|
||||
};
|
||||
});
|
||||
|
||||
return groupPermissionsMap;
|
||||
}
|
||||
|
||||
test('should match snapshot without groups', () => {
|
||||
const wrapper = shallow(
|
||||
<UserGroupsList
|
||||
{...baseProps}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with groups', () => {
|
||||
const groups = getGroups(3);
|
||||
const permissions = getPermissions(groups);
|
||||
|
||||
const wrapper = shallow(
|
||||
<UserGroupsList
|
||||
{...baseProps}
|
||||
groups={groups}
|
||||
groupPermissionsMap={permissions}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import NoResultsIndicator from 'components/no_results_indicator';
|
||||
import {NoResultsVariant} from 'components/no_results_indicator/types';
|
||||
import LoadingScreen from 'components/loading_screen';
|
||||
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
|
||||
import * as Utils from 'utils/utils';
|
||||
import {ActionResult} from 'mattermost-redux/types/actions';
|
||||
import {ModalData} from 'types/actions';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
import ViewUserGroupModal from 'components/view_user_group_modal';
|
||||
|
||||
import {Group, GroupPermissions} from '@mattermost/types/groups';
|
||||
import ADLDAPUpsellBanner from '../ad_ldap_upsell_banner';
|
||||
|
||||
export type Props = {
|
||||
groups: Group[];
|
||||
searchTerm: string;
|
||||
loading: boolean;
|
||||
groupPermissionsMap: Record<string, GroupPermissions>;
|
||||
onScroll: () => void;
|
||||
onExited: () => void;
|
||||
backButtonAction: () => void;
|
||||
actions: {
|
||||
archiveGroup: (groupId: string) => Promise<ActionResult>;
|
||||
openModal: <P>(modalData: ModalData<P>) => void;
|
||||
};
|
||||
}
|
||||
|
||||
const UserGroupsList = React.forwardRef((props: Props, ref?: React.Ref<HTMLDivElement>) => {
|
||||
const {
|
||||
groups,
|
||||
searchTerm,
|
||||
loading,
|
||||
groupPermissionsMap,
|
||||
onScroll,
|
||||
backButtonAction,
|
||||
onExited,
|
||||
actions,
|
||||
} = props;
|
||||
|
||||
const [overflowState, setOverflowState] = useState('overlay');
|
||||
|
||||
useEffect(() => {
|
||||
if (groups.length === 1) {
|
||||
setOverflowState('visible');
|
||||
}
|
||||
}, [groups]);
|
||||
|
||||
const archiveGroup = useCallback(async (groupId: string) => {
|
||||
await actions.archiveGroup(groupId);
|
||||
}, [actions.archiveGroup]);
|
||||
|
||||
const goToViewGroupModal = useCallback((group: Group) => {
|
||||
actions.openModal({
|
||||
modalId: ModalIdentifiers.VIEW_USER_GROUP,
|
||||
dialogType: ViewUserGroupModal,
|
||||
dialogProps: {
|
||||
groupId: group.id,
|
||||
backButtonCallback: backButtonAction,
|
||||
backButtonAction: () => {
|
||||
goToViewGroupModal(group);
|
||||
},
|
||||
},
|
||||
});
|
||||
onExited();
|
||||
}, [actions.openModal, onExited, backButtonAction]);
|
||||
|
||||
const groupListOpenUp = (groupListItemIndex: number): boolean => {
|
||||
if (groups.length > 1 && groupListItemIndex === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className='user-groups-modal__content user-groups-list'
|
||||
onScroll={onScroll}
|
||||
ref={ref}
|
||||
style={{overflow: overflowState}}
|
||||
>
|
||||
{(groups.length === 0 && searchTerm) &&
|
||||
<NoResultsIndicator
|
||||
variant={NoResultsVariant.ChannelSearch}
|
||||
titleValues={{channelName: `"${searchTerm}"`}}
|
||||
/>
|
||||
}
|
||||
{groups.map((group, i) => {
|
||||
return (
|
||||
<div
|
||||
className='group-row'
|
||||
key={group.id}
|
||||
onClick={() => {
|
||||
goToViewGroupModal(group);
|
||||
}}
|
||||
>
|
||||
<span className='group-display-name'>
|
||||
{group.display_name}
|
||||
</span>
|
||||
<span className='group-name'>
|
||||
{'@'}{group.name}
|
||||
</span>
|
||||
<div className='group-member-count'>
|
||||
<FormattedMessage
|
||||
id='user_groups_modal.memberCount'
|
||||
defaultMessage='{member_count} {member_count, plural, one {member} other {members}}'
|
||||
values={{
|
||||
member_count: group.member_count,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='group-action'>
|
||||
<MenuWrapper
|
||||
isDisabled={false}
|
||||
stopPropagationOnToggle={true}
|
||||
id={`customWrapper-${group.id}`}
|
||||
>
|
||||
<button className='action-wrapper'>
|
||||
<i className='icon icon-dots-vertical'/>
|
||||
</button>
|
||||
<Menu
|
||||
openLeft={true}
|
||||
openUp={groupListOpenUp(i)}
|
||||
className={'group-actions-menu'}
|
||||
ariaLabel={Utils.localizeMessage('admin.user_item.menuAriaLabel', 'User Actions Menu')}
|
||||
>
|
||||
<Menu.Group>
|
||||
<Menu.ItemAction
|
||||
onClick={() => {
|
||||
goToViewGroupModal(group);
|
||||
}}
|
||||
icon={<i className='icon-account-multiple-outline'/>}
|
||||
text={Utils.localizeMessage('user_groups_modal.viewGroup', 'View Group')}
|
||||
disabled={false}
|
||||
/>
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
<Menu.ItemAction
|
||||
show={groupPermissionsMap[group.id].can_delete}
|
||||
onClick={() => {
|
||||
archiveGroup(group.id);
|
||||
}}
|
||||
icon={<i className='icon-archive-outline'/>}
|
||||
text={Utils.localizeMessage('user_groups_modal.archiveGroup', 'Archive Group')}
|
||||
disabled={false}
|
||||
isDangerous={true}
|
||||
/>
|
||||
</Menu.Group>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{
|
||||
(loading) &&
|
||||
<LoadingScreen/>
|
||||
}
|
||||
<ADLDAPUpsellBanner/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default React.memo(UserGroupsList);
|
||||
@@ -0,0 +1,444 @@
|
||||
@charset 'UTF-8';
|
||||
|
||||
.user-groups-modal-create,
|
||||
.user-groups-modal-update,
|
||||
.view-user-groups-modal,
|
||||
.user-groups-modal {
|
||||
&.modal-dialog {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border-radius: 8px;
|
||||
|
||||
.ad_ldap_upsell_banner {
|
||||
position: sticky;
|
||||
z-index: 4;
|
||||
right: 12px;
|
||||
bottom: 6px;
|
||||
left: 12px;
|
||||
display: flex;
|
||||
height: 44px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 8px;
|
||||
margin: 0 12px;
|
||||
background-color: var(--denim-sidebar-active-border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.12);
|
||||
color: var(--button-color);
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
display: flex;
|
||||
gap: 13px;
|
||||
}
|
||||
|
||||
.banner-close {
|
||||
height: 4rem;
|
||||
border: none;
|
||||
margin-top: 0;
|
||||
background: none;
|
||||
font-size: 25px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.ad-ldap-banner-btn {
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--button-color);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.ad_ldap_upsell_confirm {
|
||||
position: sticky;
|
||||
z-index: 4;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 0 0 8px 8px;
|
||||
|
||||
.upsell-confirm-backdrop {
|
||||
position: fixed;
|
||||
z-index: 5;
|
||||
bottom: 164px;
|
||||
width: 100%;
|
||||
height: calc(100% - 164px);
|
||||
background: rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
|
||||
.upsell-confirm-foreground {
|
||||
height: 148px;
|
||||
padding: 16px 24px;
|
||||
background: var(--button-color);
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #000;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--center-channel-color);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.btns-container {
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
gap: 10px;
|
||||
|
||||
.confrim-btn {
|
||||
width: 100px;
|
||||
height: 32px;
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
background: rgba(28, 88, 217, 0.08);
|
||||
border-radius: 4px;
|
||||
color: var(--denim-button-bg);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 10px;
|
||||
}
|
||||
|
||||
.learn-more {
|
||||
background: rgba(var(--denim-button-bg-rgb), 0.08);
|
||||
color: var(--denim-button-bg);
|
||||
}
|
||||
|
||||
.continue {
|
||||
background: var(--denim-button-bg);
|
||||
color: var(--button-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
h1#createGroupsModalTitleWithBack,
|
||||
h1#updateGroupsModalTitle,
|
||||
h1#addUsersToGroupsModalLabel {
|
||||
margin-left: 44px;
|
||||
}
|
||||
|
||||
button.user-groups-create {
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--button-bg);
|
||||
margin: auto;
|
||||
margin-right: 46px;
|
||||
background-color: var(--center-channel-bg);
|
||||
border-radius: 4px;
|
||||
color: var(--button-bg);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(var(--button-bg-rgb), 0.08);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: rgba(var(--button-bg-rgb), 0.16);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: inset 0 0 0 2px var(--sidebar-text-active-border);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
overflow: hidden;
|
||||
max-height: 100%;
|
||||
padding: 0;
|
||||
|
||||
.no-results__wrapper {
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.user-groups-search {
|
||||
position: relative;
|
||||
margin: 6px 24px 8px;
|
||||
|
||||
.Input_container {
|
||||
.Input_wrapper {
|
||||
margin: 6px 0;
|
||||
line-height: 38px;
|
||||
}
|
||||
|
||||
.Input {
|
||||
font-size: 14px;
|
||||
line-height: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
.fa-search {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 16px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
font-size: 16px;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.more-modal__dropdown {
|
||||
width: 100%;
|
||||
padding: 0 24px 24px;
|
||||
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
|
||||
|
||||
#groupsFilterDropdown {
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
line-height: 12px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(var(--button-bg-rgb), 0.08);
|
||||
}
|
||||
|
||||
a {
|
||||
line-height: 12px;
|
||||
|
||||
span {
|
||||
color: var(--button-bg);
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.groups-filter-btn span {
|
||||
width: 100%;
|
||||
|
||||
i {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
color: var(--button-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-groups-modal__content {
|
||||
overflow: auto;
|
||||
|
||||
&.user-groups-list {
|
||||
position: relative;
|
||||
max-height: 450px;
|
||||
padding-top: 24px;
|
||||
padding-bottom: 16px;
|
||||
overflow-y: scroll; // for Firefox and browsers that doesn't support overflow-y:overlay property
|
||||
overflow-y: overlay;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border: 8px rgba(0, 0, 0, 0) solid;
|
||||
background: rgba(var(--center-channel-color-rgb), 0.24);
|
||||
background-clip: padding-box;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.group-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 14px 32px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba(var(--button-bg-rgb), 0.08);
|
||||
}
|
||||
|
||||
.group-display-name,
|
||||
.group-name {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
max-width: 200px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.group-name {
|
||||
padding-left: 8px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
}
|
||||
|
||||
.group-member-count {
|
||||
margin-right: 8px;
|
||||
margin-left: auto;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.group-action {
|
||||
width: 24px;
|
||||
|
||||
.MenuWrapper {
|
||||
width: 24px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.group-actions-menu {
|
||||
span.MenuItem__primary-text {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
|
||||
&.leave-group-action {
|
||||
color: var(--dnd-indicator);
|
||||
}
|
||||
|
||||
span.icon {
|
||||
display: inline-flex;
|
||||
width: auto;
|
||||
height: auto;
|
||||
align-self: center;
|
||||
padding-right: 12px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||
font-size: 18px;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group-action button.action-wrapper {
|
||||
display: flex;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 4px 6px;
|
||||
border: none;
|
||||
margin-left: auto;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
background: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
border-radius: 4px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.72);
|
||||
fill: rgba(var(--center-channel-color-rgb), 0.72);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: rgba(var(--button-bg-rgb), 0.08);
|
||||
color: var(--button-bg);
|
||||
}
|
||||
|
||||
i.icon {
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .group-action button.action-wrapper i {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-screen {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
height: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
.group-mention-input-wrapper,
|
||||
.group-name-input-wrapper {
|
||||
padding: 6px 24px 12px;
|
||||
|
||||
.Input_container {
|
||||
:focus {
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
outline-style: none;
|
||||
}
|
||||
|
||||
.Input_wrapper {
|
||||
margin: 6px 0;
|
||||
line-height: 38px;
|
||||
}
|
||||
|
||||
.Input {
|
||||
font-size: 14px;
|
||||
line-height: 38px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group-mention-input-wrapper {
|
||||
padding: 12px 24px 24px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
padding: 0 24px;
|
||||
color: inherit;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.group-add-user {
|
||||
.multi-select__wrapper {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.multi-select__footer {
|
||||
padding-top: 24px;
|
||||
|
||||
button {
|
||||
height: 40px;
|
||||
padding: 0 20px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
button[disabled] {
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
color: rgba(var(--center-channel-color-rgb), 0.32);
|
||||
}
|
||||
|
||||
button.multiselect-back {
|
||||
padding: 13px 20px;
|
||||
margin-right: 8px;
|
||||
background: rgba(var(--button-bg-rgb), 0.08);
|
||||
color: rgb(var(--button-bg-rgb));
|
||||
line-height: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import {Group} from '@mattermost/types/groups';
|
||||
|
||||
import UserGroupsModal from './user_groups_modal';
|
||||
|
||||
describe('component/user_groups_modal', () => {
|
||||
const baseProps = {
|
||||
onExited: jest.fn(),
|
||||
groups: [],
|
||||
myGroups: [],
|
||||
searchTerm: '',
|
||||
currentUserId: '',
|
||||
backButtonAction: jest.fn(),
|
||||
actions: {
|
||||
getGroups: jest.fn(),
|
||||
setModalSearchTerm: jest.fn(),
|
||||
getGroupsByUserIdPaginated: jest.fn(),
|
||||
searchGroups: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
function getGroups(numberOfGroups: number) {
|
||||
const groups: Group[] = [];
|
||||
for (let i = 0; i < numberOfGroups; i++) {
|
||||
groups.push({
|
||||
id: `group${i}`,
|
||||
name: `group${i}`,
|
||||
display_name: `Group ${i}`,
|
||||
description: `Group ${i} description`,
|
||||
source: 'custom',
|
||||
remote_id: null,
|
||||
create_at: 1637349374137,
|
||||
update_at: 1637349374137,
|
||||
delete_at: 0,
|
||||
has_syncables: false,
|
||||
member_count: i + 1,
|
||||
allow_reference: true,
|
||||
scheme_admin: false,
|
||||
});
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
test('should match snapshot without groups', () => {
|
||||
const wrapper = shallow(
|
||||
<UserGroupsModal
|
||||
{...baseProps}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with groups', () => {
|
||||
const groups = getGroups(3);
|
||||
const myGroups = getGroups(1);
|
||||
|
||||
const wrapper = shallow(
|
||||
<UserGroupsModal
|
||||
{...baseProps}
|
||||
groups={groups}
|
||||
myGroups={myGroups}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with groups, myGroups selected', () => {
|
||||
const groups = getGroups(3);
|
||||
const myGroups = getGroups(1);
|
||||
|
||||
const wrapper = shallow(
|
||||
<UserGroupsModal
|
||||
{...baseProps}
|
||||
groups={groups}
|
||||
myGroups={myGroups}
|
||||
/>,
|
||||
);
|
||||
|
||||
wrapper.setState({selectedFilter: 'my'});
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with groups, search group1', () => {
|
||||
const groups = getGroups(3);
|
||||
const myGroups = getGroups(1);
|
||||
|
||||
const wrapper = shallow(
|
||||
<UserGroupsModal
|
||||
{...baseProps}
|
||||
groups={groups}
|
||||
myGroups={myGroups}
|
||||
searchTerm='group1'
|
||||
/>,
|
||||
);
|
||||
|
||||
const instance = wrapper.instance() as UserGroupsModal;
|
||||
|
||||
const e = {
|
||||
target: {
|
||||
value: '',
|
||||
},
|
||||
};
|
||||
instance.handleSearch(e as React.ChangeEvent<HTMLInputElement>);
|
||||
expect(baseProps.actions.setModalSearchTerm).toHaveBeenCalledTimes(1);
|
||||
expect(baseProps.actions.setModalSearchTerm).toBeCalledWith('');
|
||||
|
||||
e.target.value = 'group1';
|
||||
instance.handleSearch(e as React.ChangeEvent<HTMLInputElement>);
|
||||
expect(wrapper.state('loading')).toEqual(true);
|
||||
expect(baseProps.actions.setModalSearchTerm).toHaveBeenCalledTimes(2);
|
||||
expect(baseProps.actions.setModalSearchTerm).toBeCalledWith(e.target.value);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,296 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {createRef, RefObject} from 'react';
|
||||
|
||||
import {Modal} from 'react-bootstrap';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
|
||||
import * as Utils from 'utils/utils';
|
||||
import {Group, GroupSearachParams} from '@mattermost/types/groups';
|
||||
|
||||
import './user_groups_modal.scss';
|
||||
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
import {debounce} from 'mattermost-redux/actions/helpers';
|
||||
import Input from 'components/widgets/inputs/input/input';
|
||||
import NoResultsIndicator from 'components/no_results_indicator';
|
||||
import {NoResultsVariant} from 'components/no_results_indicator/types';
|
||||
|
||||
import UserGroupsList from './user_groups_list';
|
||||
import UserGroupsModalHeader from './user_groups_modal_header';
|
||||
import ADLDAPUpsellBanner from './ad_ldap_upsell_banner';
|
||||
|
||||
const GROUPS_PER_PAGE = 60;
|
||||
|
||||
export type Props = {
|
||||
onExited: () => void;
|
||||
groups: Group[];
|
||||
myGroups: Group[];
|
||||
searchTerm: string;
|
||||
currentUserId: string;
|
||||
backButtonAction: () => void;
|
||||
actions: {
|
||||
getGroups: (
|
||||
filterAllowReference?: boolean,
|
||||
page?: number,
|
||||
perPage?: number,
|
||||
includeMemberCount?: boolean
|
||||
) => Promise<{data: Group[]}>;
|
||||
setModalSearchTerm: (term: string) => void;
|
||||
getGroupsByUserIdPaginated: (
|
||||
userId: string,
|
||||
filterAllowReference?: boolean,
|
||||
page?: number,
|
||||
perPage?: number,
|
||||
includeMemberCount?: boolean
|
||||
) => Promise<{data: Group[]}>;
|
||||
searchGroups: (
|
||||
params: GroupSearachParams,
|
||||
) => Promise<{data: Group[]}>;
|
||||
};
|
||||
}
|
||||
|
||||
type State = {
|
||||
page: number;
|
||||
myGroupsPage: number;
|
||||
loading: boolean;
|
||||
show: boolean;
|
||||
selectedFilter: string;
|
||||
allGroupsFull: boolean;
|
||||
myGroupsFull: boolean;
|
||||
}
|
||||
|
||||
export default class UserGroupsModal extends React.PureComponent<Props, State> {
|
||||
divScrollRef: RefObject<HTMLDivElement>;
|
||||
private searchTimeoutId: number
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.divScrollRef = createRef();
|
||||
this.searchTimeoutId = 0;
|
||||
|
||||
this.state = {
|
||||
page: 0,
|
||||
myGroupsPage: 0,
|
||||
loading: true,
|
||||
show: true,
|
||||
selectedFilter: 'all',
|
||||
allGroupsFull: false,
|
||||
myGroupsFull: false,
|
||||
};
|
||||
}
|
||||
|
||||
doHide = () => {
|
||||
this.setState({show: false});
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const {
|
||||
actions,
|
||||
} = this.props;
|
||||
await Promise.all([
|
||||
actions.getGroups(false, this.state.page, GROUPS_PER_PAGE, true),
|
||||
actions.getGroupsByUserIdPaginated(this.props.currentUserId, false, this.state.myGroupsPage, GROUPS_PER_PAGE, true),
|
||||
]);
|
||||
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 () => {
|
||||
const params: GroupSearachParams = {
|
||||
q: searchTerm,
|
||||
filter_allow_reference: true,
|
||||
page: this.state.page,
|
||||
per_page: GROUPS_PER_PAGE,
|
||||
include_member_count: true,
|
||||
};
|
||||
if (this.state.selectedFilter === 'all') {
|
||||
await prevProps.actions.searchGroups(params);
|
||||
} else {
|
||||
params.user_id = this.props.currentUserId;
|
||||
await prevProps.actions.searchGroups(params);
|
||||
}
|
||||
},
|
||||
Constants.SEARCH_TIMEOUT_MILLISECONDS,
|
||||
);
|
||||
|
||||
this.searchTimeoutId = searchTimeoutId;
|
||||
}
|
||||
}
|
||||
|
||||
startLoad = () => {
|
||||
this.setState({loading: true});
|
||||
}
|
||||
|
||||
loadComplete = () => {
|
||||
this.setState({loading: false});
|
||||
}
|
||||
|
||||
handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const term = e.target.value;
|
||||
this.props.actions.setModalSearchTerm(term);
|
||||
}
|
||||
|
||||
scrollGetGroups = debounce(
|
||||
async () => {
|
||||
const {page} = this.state;
|
||||
const newPage = page + 1;
|
||||
|
||||
this.setState({page: newPage});
|
||||
this.getGroups(newPage);
|
||||
},
|
||||
500,
|
||||
false,
|
||||
(): void => {},
|
||||
);
|
||||
scrollGetMyGroups = debounce(
|
||||
async () => {
|
||||
const {myGroupsPage} = this.state;
|
||||
const newPage = myGroupsPage + 1;
|
||||
|
||||
this.setState({myGroupsPage: newPage});
|
||||
this.getMyGroups(newPage);
|
||||
},
|
||||
500,
|
||||
false,
|
||||
(): void => {},
|
||||
);
|
||||
|
||||
onScroll = () => {
|
||||
const scrollHeight = this.divScrollRef.current?.scrollHeight || 0;
|
||||
const scrollTop = this.divScrollRef.current?.scrollTop || 0;
|
||||
const clientHeight = this.divScrollRef.current?.clientHeight || 0;
|
||||
|
||||
if ((scrollTop + clientHeight + 30) >= scrollHeight) {
|
||||
if (this.state.selectedFilter === 'all' && this.state.loading === false && !this.state.allGroupsFull) {
|
||||
this.scrollGetGroups();
|
||||
}
|
||||
if (this.state.selectedFilter !== 'all' && this.props.myGroups.length % GROUPS_PER_PAGE === 0 && this.state.loading === false) {
|
||||
this.scrollGetMyGroups();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getMyGroups = async (page: number) => {
|
||||
const {actions} = this.props;
|
||||
|
||||
this.startLoad();
|
||||
const data = await actions.getGroupsByUserIdPaginated(this.props.currentUserId, false, page, GROUPS_PER_PAGE, true);
|
||||
if (data.data.length === 0) {
|
||||
this.setState({myGroupsFull: true});
|
||||
}
|
||||
this.loadComplete();
|
||||
this.setState({selectedFilter: 'my'});
|
||||
}
|
||||
|
||||
getGroups = async (page: number) => {
|
||||
const {actions} = this.props;
|
||||
|
||||
this.startLoad();
|
||||
const data = await actions.getGroups(false, page, GROUPS_PER_PAGE, true);
|
||||
if (data.data.length === 0) {
|
||||
this.setState({allGroupsFull: true});
|
||||
}
|
||||
this.loadComplete();
|
||||
this.setState({selectedFilter: 'all'});
|
||||
}
|
||||
|
||||
render() {
|
||||
const groups = this.state.selectedFilter === 'all' ? this.props.groups : this.props.myGroups;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
dialogClassName='a11y__modal user-groups-modal'
|
||||
show={this.state.show}
|
||||
onHide={this.doHide}
|
||||
onExited={this.props.onExited}
|
||||
role='dialog'
|
||||
aria-labelledby='userGroupsModalLabel'
|
||||
id='userGroupsModal'
|
||||
>
|
||||
<UserGroupsModalHeader
|
||||
onExited={this.props.onExited}
|
||||
backButtonAction={this.props.backButtonAction}
|
||||
/>
|
||||
<Modal.Body>
|
||||
{(groups.length === 0 && !this.props.searchTerm) ? <>
|
||||
<NoResultsIndicator
|
||||
variant={NoResultsVariant.UserGroups}
|
||||
/>
|
||||
<ADLDAPUpsellBanner/>
|
||||
</> : <>
|
||||
<div className='user-groups-search'>
|
||||
<Input
|
||||
type='text'
|
||||
placeholder={Utils.localizeMessage('user_groups_modal.searchGroups', 'Search Groups')}
|
||||
onChange={this.handleSearch}
|
||||
value={this.props.searchTerm}
|
||||
data-testid='searchInput'
|
||||
className={'user-group-search-input'}
|
||||
inputPrefix={<i className={'icon icon-magnify'}/>}
|
||||
/>
|
||||
</div>
|
||||
<div className='more-modal__dropdown'>
|
||||
<MenuWrapper id='groupsFilterDropdown'>
|
||||
<a>
|
||||
<span>{this.state.selectedFilter === 'all' ? Utils.localizeMessage('user_groups_modal.showAllGroups', 'Show: All Groups') : Utils.localizeMessage('user_groups_modal.showMyGroups', 'Show: My Groups')}</span>
|
||||
<span className='icon icon-chevron-down'/>
|
||||
</a>
|
||||
<Menu
|
||||
openLeft={false}
|
||||
ariaLabel={Utils.localizeMessage('user_groups_modal.filterAriaLabel', 'Groups Filter Menu')}
|
||||
>
|
||||
<Menu.ItemAction
|
||||
id='groupsDropdownAll'
|
||||
buttonClass='groups-filter-btn'
|
||||
onClick={() => {
|
||||
this.getGroups(0);
|
||||
}}
|
||||
text={Utils.localizeMessage('user_groups_modal.allGroups', 'All Groups')}
|
||||
rightDecorator={this.state.selectedFilter === 'all' && <i className='icon icon-check'/>}
|
||||
/>
|
||||
<Menu.ItemAction
|
||||
id='groupsDropdownMy'
|
||||
buttonClass='groups-filter-btn'
|
||||
onClick={() => {
|
||||
this.getMyGroups(0);
|
||||
}}
|
||||
text={Utils.localizeMessage('user_groups_modal.myGroups', 'My Groups')}
|
||||
rightDecorator={this.state.selectedFilter !== 'all' && <i className='icon icon-check'/>}
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
</div>
|
||||
<UserGroupsList
|
||||
groups={groups}
|
||||
searchTerm={this.props.searchTerm}
|
||||
loading={this.state.loading}
|
||||
onScroll={this.onScroll}
|
||||
ref={this.divScrollRef}
|
||||
onExited={this.props.onExited}
|
||||
backButtonAction={this.props.backButtonAction}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// 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-md btn-primary"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Create Group"
|
||||
id="user_groups_modal.createNew"
|
||||
/>
|
||||
</button>
|
||||
</ModalHeader>
|
||||
`;
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators, Dispatch, ActionCreatorsMapObject} from 'redux';
|
||||
|
||||
import {ActionFunc, GenericAction} from 'mattermost-redux/types/actions';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
import {ModalData} from 'types/actions';
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
import {openModal} from 'actions/views/modals';
|
||||
import {haveISystemPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||
|
||||
import UserGroupsModalHeader from './user_groups_modal_header';
|
||||
|
||||
type Actions = {
|
||||
openModal: <P>(modalData: ModalData<P>) => void;
|
||||
};
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const canCreateCustomGroups = haveISystemPermission(state, {permission: Permissions.CREATE_CUSTOM_GROUP});
|
||||
|
||||
return {
|
||||
canCreateCustomGroups,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc | GenericAction>, Actions>({
|
||||
openModal,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(UserGroupsModalHeader);
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
// 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 {ModalData} from 'types/actions';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
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-md btn-primary'
|
||||
onClick={goToCreateModal}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='user_groups_modal.createNew'
|
||||
defaultMessage='Create Group'
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
</Modal.Header>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(UserGroupsModalHeader);
|
||||
Ссылка в новой задаче
Block a user