MM-50252_Refresh UI of the marketplace to improve first impressions (#22698)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
69657be9f2
Коммит
22bc41effa
@@ -35,7 +35,7 @@ import KeyboardShortcutsModal from 'components/keyboard_shortcuts/keyboard_short
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
import {t} from 'utils/i18n';
|
||||
import MarketplaceModal from 'components/plugin_marketplace';
|
||||
import MarketplaceModal from 'components/plugin_marketplace/marketplace_modal';
|
||||
import WorkTemplateModal from 'components/work_templates';
|
||||
import {haveICurrentTeamPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
|
||||
@@ -59,7 +59,7 @@ exports[`components/GenericModal should match snapshot for base case 1`] = `
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
className="GenericModal__body"
|
||||
className="GenericModal__body padding"
|
||||
/>
|
||||
</ModalBody>
|
||||
</div>
|
||||
@@ -126,11 +126,12 @@ exports[`components/GenericModal should match snapshot with both buttons 1`] = `
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
className="GenericModal__body"
|
||||
className="GenericModal__body padding"
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter
|
||||
bsClass="modal-footer"
|
||||
className=""
|
||||
componentClass="div"
|
||||
>
|
||||
<button
|
||||
@@ -218,11 +219,12 @@ exports[`components/GenericModal should match snapshot with disabled confirm but
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
className="GenericModal__body"
|
||||
className="GenericModal__body padding"
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter
|
||||
bsClass="modal-footer"
|
||||
className=""
|
||||
componentClass="div"
|
||||
>
|
||||
<button
|
||||
|
||||
@@ -19,8 +19,7 @@ import {Locations, Constants, ModalIdentifiers} from 'utils/constants';
|
||||
import Permissions from 'mattermost-redux/constants/permissions';
|
||||
import {ActionsTutorialTip} from 'components/actions_menu/actions_menu_tutorial_tip';
|
||||
import {ModalData} from 'types/actions';
|
||||
import MarketplaceModal from 'components/plugin_marketplace';
|
||||
import {OpenedFromType} from 'components/plugin_marketplace/marketplace_modal';
|
||||
import MarketplaceModal, {OpenedFromType} from 'components/plugin_marketplace/marketplace_modal';
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
import * as PostUtils from 'utils/post_utils';
|
||||
import * as Utils from 'utils/utils';
|
||||
|
||||
@@ -10,7 +10,7 @@ import Icon from '@mattermost/compass-components/foundations/icon';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import MarketplaceModal from 'components/plugin_marketplace';
|
||||
import MarketplaceModal from 'components/plugin_marketplace/marketplace_modal';
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
|
||||
import {Constants, ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
@@ -10,7 +10,7 @@ import {uniqWith} from 'lodash';
|
||||
import {Post} from '@mattermost/types/posts';
|
||||
import {MarketplacePlugin} from '@mattermost/types/marketplace';
|
||||
|
||||
import MarketplaceModal from 'components/plugin_marketplace';
|
||||
import MarketplaceModal from 'components/plugin_marketplace/marketplace_modal';
|
||||
import Markdown from 'components/markdown';
|
||||
|
||||
import {getUsers} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
@@ -4,6 +4,7 @@ exports[`components/drafts/draft_actions/delete_draft_modal should have called o
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={true}
|
||||
autoCloseOnConfirmButton={true}
|
||||
bodyPadding={true}
|
||||
compassDesign={true}
|
||||
confirmButtonText="Yes, delete"
|
||||
enforceFocus={true}
|
||||
@@ -45,6 +46,7 @@ exports[`components/drafts/draft_actions/delete_draft_modal should have called o
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={true}
|
||||
autoCloseOnConfirmButton={true}
|
||||
bodyPadding={true}
|
||||
compassDesign={true}
|
||||
confirmButtonText="Yes, delete"
|
||||
enforceFocus={true}
|
||||
|
||||
@@ -4,6 +4,7 @@ exports[`components/drafts/draft_actions/send_draft_modal should have called onC
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={true}
|
||||
autoCloseOnConfirmButton={true}
|
||||
bodyPadding={true}
|
||||
compassDesign={true}
|
||||
confirmButtonText="Yes, send now"
|
||||
enforceFocus={true}
|
||||
@@ -44,6 +45,7 @@ exports[`components/drafts/draft_actions/send_draft_modal should have called onE
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={true}
|
||||
autoCloseOnConfirmButton={true}
|
||||
bodyPadding={true}
|
||||
compassDesign={true}
|
||||
confirmButtonText="Yes, send now"
|
||||
enforceFocus={true}
|
||||
|
||||
@@ -104,6 +104,10 @@
|
||||
padding: 12px 24px 24px 24px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
|
||||
&.divider {
|
||||
border-top: 1px solid rgba(63, 67, 80, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
&.GenericModal__compassDesign {
|
||||
@@ -112,10 +116,11 @@
|
||||
border-radius: 12px;
|
||||
|
||||
.modal-header {
|
||||
flex-direction: column;
|
||||
padding: 26px 32px 7px;
|
||||
|
||||
.close {
|
||||
top: 20px;
|
||||
top: 22px;
|
||||
right: 18px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@@ -133,7 +138,13 @@
|
||||
}
|
||||
|
||||
.GenericModal__header {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0;
|
||||
margin-right: 32px;
|
||||
border-top-left-radius: 12px;
|
||||
border-top-right-radius: 12px;
|
||||
|
||||
@@ -145,10 +156,12 @@
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 15px 32px 0;
|
||||
|
||||
.GenericModal__body {
|
||||
padding: 0;
|
||||
|
||||
&.padding {
|
||||
padding: 15px 32px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,5 +11,8 @@ import './generic_modal.scss';
|
||||
// find them there, so we've had to redefine them here
|
||||
t('generic_modal.cancel');
|
||||
t('generic_modal.confirm');
|
||||
t('footer_pagination.count');
|
||||
t('footer_pagination.prev');
|
||||
t('footer_pagination.next');
|
||||
|
||||
export default GenericModal;
|
||||
|
||||
@@ -152,14 +152,7 @@ exports[`components/global/product_switcher_menu should match snapshot with id 1
|
||||
"openedFrom": "product_menu",
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
dialogType={[Function]}
|
||||
icon={
|
||||
<Icon
|
||||
glyph="view-grid-plus-outline"
|
||||
@@ -360,14 +353,7 @@ exports[`components/global/product_switcher_menu should match snapshot with most
|
||||
"openedFrom": "product_menu",
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
dialogType={[Function]}
|
||||
icon={
|
||||
<Icon
|
||||
glyph="view-grid-plus-outline"
|
||||
@@ -688,14 +674,7 @@ exports[`components/global/product_switcher_menu should show integrations should
|
||||
"openedFrom": "product_menu",
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
dialogType={[Function]}
|
||||
icon={
|
||||
<Icon
|
||||
glyph="view-grid-plus-outline"
|
||||
|
||||
@@ -11,7 +11,7 @@ import {Permissions} from 'mattermost-redux/constants';
|
||||
import AboutBuildModal from 'components/about_build_modal';
|
||||
import SystemPermissionGate from 'components/permissions_gates/system_permission_gate';
|
||||
import TeamPermissionGate from 'components/permissions_gates/team_permission_gate';
|
||||
import MarketplaceModal from 'components/plugin_marketplace';
|
||||
import MarketplaceModal from 'components/plugin_marketplace/marketplace_modal';
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
import RestrictedIndicator from 'components/widgets/menu/menu_items/restricted_indicator';
|
||||
import {VisitSystemConsoleTour} from 'components/onboarding_tasks';
|
||||
|
||||
@@ -4,6 +4,7 @@ exports[`components/new_channel_modal should match snapshot 1`] = `
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={true}
|
||||
autoCloseOnConfirmButton={false}
|
||||
bodyPadding={true}
|
||||
cancelButtonText="Cancel"
|
||||
className="new-channel-modal"
|
||||
compassDesign={true}
|
||||
|
||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -1,58 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators, Dispatch, ActionCreatorsMapObject} from 'redux';
|
||||
|
||||
import {GenericAction, ActionFunc} from 'mattermost-redux/types/actions';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
import {getListing, getInstalledListing} from 'selectors/views/marketplace';
|
||||
import {setFirstAdminVisitMarketplaceStatus} from 'mattermost-redux/actions/general';
|
||||
import {getPluginStatuses} from 'mattermost-redux/actions/admin';
|
||||
import {getFirstAdminVisitMarketplaceStatus} from 'mattermost-redux/selectors/entities/general';
|
||||
|
||||
import {makeAsyncComponent} from 'components/async_load';
|
||||
|
||||
import {isModalOpen} from 'selectors/views/modals';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
import {getSiteURL} from 'utils/url';
|
||||
|
||||
import {closeModal} from 'actions/views/modals';
|
||||
import {fetchListing, filterListing} from 'actions/marketplace';
|
||||
|
||||
const MarketplaceModal = makeAsyncComponent('MarketplaceModal', React.lazy(() => import('./marketplace_modal')));
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
return {
|
||||
show: isModalOpen(state, ModalIdentifiers.PLUGIN_MARKETPLACE),
|
||||
listing: getListing(state),
|
||||
installedListing: getInstalledListing(state),
|
||||
siteURL: getSiteURL(),
|
||||
pluginStatuses: state.entities.admin.pluginStatuses,
|
||||
firstAdminVisitMarketplaceStatus: getFirstAdminVisitMarketplaceStatus(state),
|
||||
};
|
||||
}
|
||||
|
||||
type Actions = {
|
||||
closeModal(): void;
|
||||
fetchListing(localOnly?: boolean): Promise<{error?: Error}>;
|
||||
filterListing(filter: string): Promise<{error?: Error}>;
|
||||
setFirstAdminVisitMarketplaceStatus(): Promise<void>;
|
||||
getPluginStatuses(): Promise<void>;
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
||||
return {
|
||||
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc | GenericAction>, Actions>({
|
||||
closeModal: () => closeModal(ModalIdentifiers.PLUGIN_MARKETPLACE),
|
||||
fetchListing,
|
||||
filterListing,
|
||||
setFirstAdminVisitMarketplaceStatus,
|
||||
getPluginStatuses,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(MarketplaceModal);
|
||||
@@ -11,7 +11,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render 1`] = `
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="app-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -50,7 +50,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render installed a
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-outline"
|
||||
className="app-installed"
|
||||
disabled={true}
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
@@ -83,7 +83,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render with empty
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="app-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -123,7 +123,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render with icon 1
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="app-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -164,7 +164,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render with no hom
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="app-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -202,7 +202,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render with no plu
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="app-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -240,7 +240,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render with one la
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="app-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -288,7 +288,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render with server
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="app-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -328,7 +328,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render with two la
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="app-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -381,7 +381,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem when installing 1`] = `
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="app-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
|
||||
@@ -50,7 +50,7 @@ export default class MarketplaceItemApp extends React.PureComponent <Marketplace
|
||||
if (this.props.installed && !this.props.installing && !this.props.error) {
|
||||
return (
|
||||
<button
|
||||
className='btn btn-outline'
|
||||
className='app-installed'
|
||||
disabled={true}
|
||||
>
|
||||
<FormattedMessage
|
||||
@@ -81,7 +81,7 @@ export default class MarketplaceItemApp extends React.PureComponent <Marketplace
|
||||
return (
|
||||
<button
|
||||
onClick={this.onInstall}
|
||||
className='btn btn-primary'
|
||||
className='app-install always-show-enabled'
|
||||
disabled={this.props.installing}
|
||||
>
|
||||
<LoadingWrapper
|
||||
|
||||
@@ -11,7 +11,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render 1`] = `
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="plugin-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -78,7 +78,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render installe
|
||||
to="/admin_console/plugins/plugin_id"
|
||||
>
|
||||
<button
|
||||
className="btn btn-outline"
|
||||
className="plugin-configure"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
@@ -137,7 +137,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with emp
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="plugin-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -202,7 +202,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with no
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="plugin-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -265,7 +265,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with no
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="plugin-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -328,7 +328,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with no
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="plugin-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -390,7 +390,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with one
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="plugin-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -463,7 +463,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with plu
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="plugin-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -540,7 +540,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with ser
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="plugin-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -605,7 +605,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with two
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="plugin-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -686,7 +686,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with upd
|
||||
to="/admin_console/plugins/plugin_id"
|
||||
>
|
||||
<button
|
||||
className="btn btn-outline"
|
||||
className="plugin-configure"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
@@ -751,7 +751,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with upd
|
||||
to="/admin_console/plugins/plugin_id"
|
||||
>
|
||||
<button
|
||||
className="btn btn-outline"
|
||||
className="plugin-configure"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
|
||||
@@ -294,7 +294,7 @@ export default class MarketplaceItemPlugin extends React.PureComponent <Marketpl
|
||||
>
|
||||
<button
|
||||
onClick={this.onConfigure}
|
||||
className='btn btn-outline'
|
||||
className='plugin-configure'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='marketplace_modal.list.configure'
|
||||
@@ -325,7 +325,7 @@ export default class MarketplaceItemPlugin extends React.PureComponent <Marketpl
|
||||
return (
|
||||
<button
|
||||
onClick={this.onInstall}
|
||||
className='btn btn-primary'
|
||||
className='plugin-install always-show-enabled'
|
||||
disabled={this.props.installing}
|
||||
>
|
||||
<LoadingWrapper
|
||||
|
||||
@@ -1,150 +1,14 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/marketplace/marketplace_list should render with multiple plugins 1`] = `
|
||||
exports[`components/marketplace/marketplace_list should render default 1`] = `
|
||||
<div
|
||||
className="more-modal__list"
|
||||
className="no_plugins"
|
||||
>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
<PluginIcon
|
||||
className="icon__plugin"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(NavigationRow)
|
||||
maximumPerPage={15}
|
||||
onNextPageButtonClick={[Function]}
|
||||
onPreviousPageButtonClick={[Function]}
|
||||
page={0}
|
||||
total={17}
|
||||
<div
|
||||
className="no_plugins__message"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -8,8 +8,7 @@ import {AuthorType, MarketplacePlugin, ReleaseStage} from '@mattermost/types/mar
|
||||
|
||||
import MarketplaceItem from '../marketplace_item/marketplace_item_plugin';
|
||||
|
||||
import MarketplaceList from './marketplace_list';
|
||||
import NavigationRow from './navigation_row';
|
||||
import MarketplaceList, {ITEMS_PER_PAGE} from './marketplace_list';
|
||||
|
||||
describe('components/marketplace/marketplace_list', () => {
|
||||
const samplePlugin: MarketplacePlugin = {
|
||||
@@ -28,8 +27,20 @@ describe('components/marketplace/marketplace_list', () => {
|
||||
installed_version: '',
|
||||
};
|
||||
|
||||
it('should render with multiple plugins', () => {
|
||||
const wrapper = shallow<MarketplaceList>(
|
||||
it('should render default', () => {
|
||||
const wrapper = shallow(
|
||||
<MarketplaceList
|
||||
listing={[]}
|
||||
page={0}
|
||||
noResultsMessage=''
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render page with ITEMS_PER_PAGE plugins', () => {
|
||||
const wrapper = shallow(
|
||||
<MarketplaceList
|
||||
listing={[
|
||||
samplePlugin, samplePlugin, samplePlugin, samplePlugin, samplePlugin,
|
||||
@@ -37,28 +48,29 @@ describe('components/marketplace/marketplace_list', () => {
|
||||
samplePlugin, samplePlugin, samplePlugin, samplePlugin, samplePlugin,
|
||||
samplePlugin, samplePlugin,
|
||||
]}
|
||||
page={0}
|
||||
noResultsMessage=''
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
expect(wrapper.state().page).toEqual(0);
|
||||
expect(wrapper.find(MarketplaceItem)).toHaveLength(15);
|
||||
expect(wrapper.find(NavigationRow)).toHaveLength(1);
|
||||
expect(wrapper.find(NavigationRow).props().page).toEqual(0);
|
||||
expect(wrapper.find(NavigationRow).props().total).toEqual(17);
|
||||
expect(wrapper.find(NavigationRow).props().maximumPerPage).toEqual(15);
|
||||
expect(wrapper.find(MarketplaceItem)).toHaveLength(ITEMS_PER_PAGE);
|
||||
});
|
||||
|
||||
it('should set page to 0 when list of plugins changed', () => {
|
||||
const wrapper = shallow<MarketplaceList>(
|
||||
it('should render no results', () => {
|
||||
const wrapper = shallow(
|
||||
<MarketplaceList
|
||||
listing={[samplePlugin, samplePlugin]}
|
||||
listing={[]}
|
||||
page={0}
|
||||
noResultsMessage='No plugins available'
|
||||
noResultsAction={{
|
||||
label: 'action',
|
||||
onClick: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
wrapper.setState({page: 10});
|
||||
wrapper.setProps({listing: [samplePlugin]});
|
||||
|
||||
expect(wrapper.state().page).toEqual(0);
|
||||
expect(wrapper.find('.icon__plugin').length).toEqual(1);
|
||||
expect(wrapper.find('.no_plugins__message').length).toEqual(1);
|
||||
expect(wrapper.find('.no_plugins__action').length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,107 +1,116 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import type {MarketplaceApp, MarketplacePlugin} from '@mattermost/types/marketplace';
|
||||
import {isPlugin, getName} from 'mattermost-redux/utils/marketplace';
|
||||
|
||||
import PluginIcon from 'components/widgets/icons/plugin_icon';
|
||||
|
||||
import MarketplaceItemPlugin from '../marketplace_item/marketplace_item_plugin';
|
||||
import MarketplaceItemApp from '../marketplace_item/marketplace_item_app';
|
||||
|
||||
import NavigationRow from './navigation_row';
|
||||
|
||||
const ITEMS_PER_PAGE = 15;
|
||||
export const ITEMS_PER_PAGE = 15;
|
||||
|
||||
type MarketplaceListProps = {
|
||||
listing: Array<MarketplacePlugin | MarketplaceApp>;
|
||||
};
|
||||
|
||||
type MarketplaceListState = {
|
||||
page: number;
|
||||
noResultsMessage: string;
|
||||
noResultsAction?: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
filter?: string;
|
||||
listRef?: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export default class MarketplaceList extends React.PureComponent <MarketplaceListProps, MarketplaceListState> {
|
||||
static getDerivedStateFromProps(props: MarketplaceListProps, state: MarketplaceListState): MarketplaceListState | null {
|
||||
if (state.page > 0 && props.listing.length < ITEMS_PER_PAGE) {
|
||||
return {page: 0};
|
||||
const MarketplaceList = ({
|
||||
listing,
|
||||
page,
|
||||
noResultsMessage,
|
||||
noResultsAction,
|
||||
filter,
|
||||
listRef,
|
||||
}: MarketplaceListProps) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const pageItems = useMemo(() => {
|
||||
if (listing.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
constructor(props: MarketplaceListProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
page: 0,
|
||||
};
|
||||
}
|
||||
|
||||
nextPage = (): void => {
|
||||
this.setState((state) => ({
|
||||
page: state.page + 1,
|
||||
}));
|
||||
};
|
||||
|
||||
previousPage = (): void => {
|
||||
this.setState((state) => ({
|
||||
page: state.page - 1,
|
||||
}));
|
||||
};
|
||||
|
||||
render(): JSX.Element {
|
||||
const pageStart = this.state.page * ITEMS_PER_PAGE;
|
||||
const pageStart = page * ITEMS_PER_PAGE;
|
||||
const pageEnd = pageStart + ITEMS_PER_PAGE;
|
||||
|
||||
this.props.listing.sort((a, b) => {
|
||||
return getName(a).localeCompare(getName(b));
|
||||
});
|
||||
return [...listing].
|
||||
sort((a, b) => getName(a).localeCompare(getName(b))).
|
||||
slice(pageStart, pageEnd).
|
||||
map((i) => (
|
||||
isPlugin(i) ? (
|
||||
<MarketplaceItemPlugin
|
||||
key={i.manifest.id}
|
||||
id={i.manifest.id}
|
||||
name={i.manifest.name}
|
||||
description={i.manifest.description}
|
||||
version={i.manifest.version}
|
||||
homepageUrl={i.homepage_url}
|
||||
releaseNotesUrl={i.release_notes_url}
|
||||
labels={i.labels}
|
||||
iconData={i.icon_data}
|
||||
installedVersion={i.installed_version}
|
||||
/>
|
||||
) : (
|
||||
<MarketplaceItemApp
|
||||
key={i.manifest.app_id}
|
||||
id={i.manifest.app_id}
|
||||
name={i.manifest.display_name}
|
||||
description={i.manifest.description}
|
||||
homepageUrl={i.manifest.homepage_url}
|
||||
iconURL={i.icon_url}
|
||||
installed={i.installed}
|
||||
labels={i.labels}
|
||||
/>
|
||||
)
|
||||
));
|
||||
}, [listing, page]);
|
||||
|
||||
const itemsToDisplay = this.props.listing.slice(pageStart, pageEnd);
|
||||
const getNoResultsMessage = useCallback(() => (
|
||||
filter ? (
|
||||
formatMessage(
|
||||
{id: 'marketplace_modal_list.no_plugins_filter', defaultMessage: 'No results for "{filter}"'},
|
||||
{filter},
|
||||
)
|
||||
) : (
|
||||
noResultsMessage
|
||||
)
|
||||
), [filter, noResultsMessage]);
|
||||
|
||||
return (
|
||||
<div className='more-modal__list'>
|
||||
{itemsToDisplay.map((i) => {
|
||||
if (isPlugin(i)) {
|
||||
return (
|
||||
<MarketplaceItemPlugin
|
||||
key={i.manifest.id}
|
||||
id={i.manifest.id}
|
||||
name={i.manifest.name}
|
||||
description={i.manifest.description}
|
||||
version={i.manifest.version}
|
||||
homepageUrl={i.homepage_url}
|
||||
releaseNotesUrl={i.release_notes_url}
|
||||
labels={i.labels}
|
||||
iconData={i.icon_data}
|
||||
installedVersion={i.installed_version}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MarketplaceItemApp
|
||||
key={i.manifest.app_id}
|
||||
id={i.manifest.app_id}
|
||||
name={i.manifest.display_name}
|
||||
description={i.manifest.description}
|
||||
homepageUrl={i.manifest.homepage_url}
|
||||
iconURL={i.icon_url}
|
||||
installed={i.installed}
|
||||
labels={i.labels}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
<NavigationRow
|
||||
page={this.state.page}
|
||||
total={this.props.listing.length}
|
||||
maximumPerPage={ITEMS_PER_PAGE}
|
||||
onNextPageButtonClick={this.nextPage}
|
||||
onPreviousPageButtonClick={this.previousPage}
|
||||
/>
|
||||
return (listing.length === 0 ? (
|
||||
<div className='no_plugins'>
|
||||
<PluginIcon className='icon__plugin'/>
|
||||
<div className='no_plugins__message'>
|
||||
{getNoResultsMessage()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
{noResultsAction && (
|
||||
<button
|
||||
className='no_plugins__action'
|
||||
onClick={noResultsAction.onClick}
|
||||
data-testid='Install-Plugins-button'
|
||||
>
|
||||
{noResultsAction.label}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
ref={listRef}
|
||||
className='more-modal__list'
|
||||
>
|
||||
{pageItems}
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
export default MarketplaceList;
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/marketplace/navigation_row should not render any buttons 1`] = `
|
||||
<div
|
||||
className="navigation-row"
|
||||
>
|
||||
<div
|
||||
className="col-xs-2"
|
||||
/>
|
||||
<div
|
||||
className="col-xs-8 count"
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.6)",
|
||||
}
|
||||
}
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="{startCount, number} - {endCount, number} {total, plural, one {plugin} other {plugins}} of {total, number} total"
|
||||
id="marketplace_list.count_total_page"
|
||||
values={
|
||||
Object {
|
||||
"endCount": 15,
|
||||
"startCount": 1,
|
||||
"total": 15,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="col-xs-2"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/marketplace/navigation_row should render next and previous buttons 1`] = `
|
||||
<div
|
||||
className="navigation-row"
|
||||
>
|
||||
<div
|
||||
className="col-xs-2"
|
||||
>
|
||||
<NavigationButton
|
||||
defaultMessage="Previous"
|
||||
messageId="more_channels.prev"
|
||||
onClick={[MockFunction]}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="col-xs-8 count"
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.6)",
|
||||
}
|
||||
}
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="{startCount, number} - {endCount, number} {total, plural, one {plugin} other {plugins}} of {total, number} total"
|
||||
id="marketplace_list.count_total_page"
|
||||
values={
|
||||
Object {
|
||||
"endCount": 30,
|
||||
"startCount": 16,
|
||||
"total": 32,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="col-xs-2"
|
||||
>
|
||||
<NavigationButton
|
||||
defaultMessage="Next"
|
||||
messageId="more_channels.next"
|
||||
onClick={[MockFunction]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/marketplace/navigation_row should render only next button 1`] = `
|
||||
<div
|
||||
className="navigation-row"
|
||||
>
|
||||
<div
|
||||
className="col-xs-2"
|
||||
/>
|
||||
<div
|
||||
className="col-xs-8 count"
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.6)",
|
||||
}
|
||||
}
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="{startCount, number} - {endCount, number} {total, plural, one {plugin} other {plugins}} of {total, number} total"
|
||||
id="marketplace_list.count_total_page"
|
||||
values={
|
||||
Object {
|
||||
"endCount": 15,
|
||||
"startCount": 1,
|
||||
"total": 32,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="col-xs-2"
|
||||
>
|
||||
<NavigationButton
|
||||
defaultMessage="Next"
|
||||
messageId="more_channels.next"
|
||||
onClick={[MockFunction]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/marketplace/navigation_row should render only previous button 1`] = `
|
||||
<div
|
||||
className="navigation-row"
|
||||
>
|
||||
<div
|
||||
className="col-xs-2"
|
||||
>
|
||||
<NavigationButton
|
||||
defaultMessage="Previous"
|
||||
messageId="more_channels.prev"
|
||||
onClick={[MockFunction]}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="col-xs-8 count"
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.6)",
|
||||
}
|
||||
}
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="{startCount, number} - {endCount, number} {total, plural, one {plugin} other {plugins}} of {total, number} total"
|
||||
id="marketplace_list.count_total_page"
|
||||
values={
|
||||
Object {
|
||||
"endCount": 32,
|
||||
"startCount": 31,
|
||||
"total": 32,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="col-xs-2"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,17 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
import NavigationRow from './navigation_row';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(NavigationRow);
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
type NavigationButtonProps = {
|
||||
onClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
messageId: string;
|
||||
defaultMessage: string;
|
||||
};
|
||||
|
||||
export default class NavigationButton extends React.PureComponent <NavigationButtonProps> {
|
||||
onClick = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>): void => {
|
||||
event.preventDefault();
|
||||
this.props.onClick(event);
|
||||
};
|
||||
|
||||
render(): JSX.Element {
|
||||
const {onClick, messageId, defaultMessage} = this.props;
|
||||
return (
|
||||
<button
|
||||
className='btn btn-link'
|
||||
onClick={onClick}
|
||||
>
|
||||
<FormattedMessage
|
||||
id={messageId}
|
||||
defaultMessage={defaultMessage}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import NavigationRow, {NavigationRowProps} from './navigation_row';
|
||||
|
||||
describe('components/marketplace/navigation_row', () => {
|
||||
const baseProps: NavigationRowProps = {
|
||||
page: 0,
|
||||
total: 32,
|
||||
maximumPerPage: 15,
|
||||
onNextPageButtonClick: jest.fn(),
|
||||
onPreviousPageButtonClick: jest.fn(),
|
||||
theme: {centerChannelColor: '#fff'} as Theme,
|
||||
};
|
||||
|
||||
it('should render only next button', () => {
|
||||
const wrapper = shallow<NavigationRow>(
|
||||
<NavigationRow {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find('NavigationButton')).toHaveLength(1);
|
||||
|
||||
wrapper.find('NavigationButton').simulate('click', {preventDefault: jest.fn});
|
||||
|
||||
expect(wrapper.instance().props.onNextPageButtonClick).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.instance().props.onPreviousPageButtonClick).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should render next and previous buttons', () => {
|
||||
const props = {...baseProps, page: 1};
|
||||
const wrapper = shallow<NavigationRow>(
|
||||
<NavigationRow{...props}/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find('NavigationButton')).toHaveLength(2);
|
||||
|
||||
wrapper.find('NavigationButton').at(0).simulate('click', {preventDefault: jest.fn});
|
||||
wrapper.find('NavigationButton').at(1).simulate('click', {preventDefault: jest.fn});
|
||||
|
||||
expect(wrapper.instance().props.onNextPageButtonClick).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.instance().props.onPreviousPageButtonClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should render only previous button', () => {
|
||||
const props = {...baseProps, page: 2};
|
||||
const wrapper = shallow<NavigationRow>(
|
||||
<NavigationRow{...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find('NavigationButton')).toHaveLength(1);
|
||||
|
||||
wrapper.find('NavigationButton').simulate('click', {preventDefault: jest.fn});
|
||||
|
||||
expect(wrapper.instance().props.onNextPageButtonClick).toHaveBeenCalledTimes(0);
|
||||
expect(wrapper.instance().props.onPreviousPageButtonClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not render any buttons', () => {
|
||||
const props = {...baseProps, page: 0, total: 15};
|
||||
const wrapper = shallow<NavigationRow>(
|
||||
<NavigationRow{...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find('NavigationButton')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -1,87 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {changeOpacity, makeStyleFromTheme} from 'mattermost-redux/utils/theme_utils';
|
||||
|
||||
import NavigationButton from './navigation_button';
|
||||
|
||||
export type NavigationRowProps = {
|
||||
page: number;
|
||||
total: number;
|
||||
maximumPerPage: number;
|
||||
onNextPageButtonClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
onPreviousPageButtonClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
theme: Theme;
|
||||
};
|
||||
|
||||
export default class NavigationRow extends React.PureComponent <NavigationRowProps> {
|
||||
canShowNextButton = (): boolean => {
|
||||
const {page, maximumPerPage, total} = this.props;
|
||||
const totalPages = Math.trunc((total - 1) / maximumPerPage);
|
||||
|
||||
return totalPages > page;
|
||||
};
|
||||
|
||||
renderCount = (): JSX.Element => {
|
||||
const {page, total, maximumPerPage} = this.props;
|
||||
const startCount = page * maximumPerPage;
|
||||
const endCount = Math.min(startCount + maximumPerPage, total);
|
||||
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='marketplace_list.count_total_page'
|
||||
defaultMessage='{startCount, number} - {endCount, number} {total, plural, one {plugin} other {plugins}} of {total, number} total'
|
||||
values={{
|
||||
startCount: startCount + 1,
|
||||
endCount,
|
||||
total,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render(): JSX.Element {
|
||||
const style = getStyle(this.props.theme);
|
||||
|
||||
return (
|
||||
<div className='navigation-row'>
|
||||
<div className='col-xs-2'>
|
||||
{(this.props.page > 0) && (
|
||||
<NavigationButton
|
||||
onClick={this.props.onPreviousPageButtonClick}
|
||||
messageId={'more_channels.prev'}
|
||||
defaultMessage={'Previous'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className='col-xs-8 count'
|
||||
style={style.count}
|
||||
>
|
||||
{this.renderCount()}
|
||||
</div>
|
||||
<div className='col-xs-2'>
|
||||
{this.canShowNextButton() && (
|
||||
<NavigationButton
|
||||
onClick={this.props.onNextPageButtonClick}
|
||||
messageId={'more_channels.next'}
|
||||
defaultMessage={'Next'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyle = makeStyleFromTheme((theme) => {
|
||||
return {
|
||||
count: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -1,21 +1,11 @@
|
||||
@import 'utils/variables';
|
||||
@import 'utils/mixins';
|
||||
|
||||
.modal-marketplace {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 50px;
|
||||
color: var(--center-channel-color);
|
||||
font-size: 16px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
.marketplace-modal {
|
||||
width: 800px;
|
||||
|
||||
div.navigation-row {
|
||||
overflow: unset;
|
||||
margin-top: 10px;
|
||||
|
||||
div {
|
||||
@@ -29,7 +19,7 @@
|
||||
}
|
||||
|
||||
div.count {
|
||||
padding-top: 8px;
|
||||
padding: 9px 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,33 +35,47 @@
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
font-size: 14px;
|
||||
padding: 0 32px;
|
||||
margin: 0 0 8px;
|
||||
|
||||
> li {
|
||||
> a {
|
||||
padding: 10px 16px;
|
||||
li {
|
||||
margin-right: 0;
|
||||
|
||||
a {
|
||||
padding: 13px 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: none;
|
||||
color: var(--center-channel-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.active > a {
|
||||
color: var(--center-channel-color);
|
||||
&.active {
|
||||
border-bottom: 2px solid var(--denim-button-bg);
|
||||
|
||||
a {
|
||||
color: var(--denim-button-bg);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 8px 0 24px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: 300;
|
||||
|
||||
@@ -81,8 +85,14 @@
|
||||
}
|
||||
|
||||
.more-modal__list {
|
||||
height: 390px;
|
||||
margin: 0 6px 8px 12px;
|
||||
overflow-y: scroll;
|
||||
|
||||
.more-modal__row {
|
||||
align-items: normal;
|
||||
min-height: 80px;
|
||||
padding: 16px 20px;
|
||||
border-bottom: none;
|
||||
|
||||
.marketplace__tag {
|
||||
margin-left: 6px;
|
||||
@@ -94,25 +104,91 @@
|
||||
margin: 10px 10px 0 0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.more-modal__details {
|
||||
padding-left: 16px;
|
||||
|
||||
.more-modal__row--link {
|
||||
color: var(--center-channel-color);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.more-modal__description {
|
||||
margin: 2px 0 0;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.more-modal__actions {
|
||||
padding-left: 16px;
|
||||
margin: 0;
|
||||
|
||||
.plugin-configure,
|
||||
.app-installed {
|
||||
@include secondary-button;
|
||||
@include button-medium;
|
||||
}
|
||||
|
||||
.plugin-install,
|
||||
.app-install {
|
||||
@include primary-button;
|
||||
@include button-medium;
|
||||
}
|
||||
|
||||
a {
|
||||
&:hover,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.more-modal__description {
|
||||
margin: 2px 0 0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.icon__plugin {
|
||||
display: flex;
|
||||
height: 48px;
|
||||
flex: 0 0 48px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: $white;
|
||||
border-radius: 50%;
|
||||
|
||||
padding-bottom: 80px;
|
||||
svg,
|
||||
img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: var(--button-bg);
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search_input {
|
||||
width: 720px;
|
||||
height: 40px;
|
||||
flex: 1;
|
||||
margin-top: 28px;
|
||||
margin-right: 16px;
|
||||
margin-bottom: 20px;
|
||||
margin-left: 16px;
|
||||
box-shadow: none;
|
||||
.marketplace-modal-search {
|
||||
padding: 24px 0 0;
|
||||
|
||||
.search_input {
|
||||
width: 100%;
|
||||
border: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
@@ -130,8 +206,9 @@
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
width: 720px;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
@@ -160,67 +237,41 @@
|
||||
}
|
||||
}
|
||||
|
||||
.icon__plugin {
|
||||
.no_plugins {
|
||||
display: flex;
|
||||
height: 42px;
|
||||
flex: 0 0 42px;
|
||||
height: 390px;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 4px;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 8px;
|
||||
|
||||
svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
fill: var(--button-bg);
|
||||
&__message {
|
||||
margin-top: 20px;
|
||||
color: var(--center-channel-color);
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon__plugin--background {
|
||||
padding: 6px;
|
||||
background-color: $white;
|
||||
&__action {
|
||||
@include primary-button;
|
||||
@include button-medium;
|
||||
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.no_plugins_div {
|
||||
text-align: center;
|
||||
.icon__plugin {
|
||||
svg {
|
||||
fill: var(--button-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item_error {
|
||||
background-color: rgba(var(--error-text-rgb), 0.08);
|
||||
background-color: rgba(var(--error-text-color-rgb), 0.08);
|
||||
}
|
||||
|
||||
.error_text {
|
||||
color: var(--error-text);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.error-bar {
|
||||
position: fixed;
|
||||
z-index: 8;
|
||||
top: 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
min-height: $announcement-bar-height;
|
||||
max-height: $announcement-bar-height;
|
||||
padding: 5px 30px;
|
||||
background-color: var(--center-channel-bg);
|
||||
color: var(--error-text);
|
||||
|
||||
.error-bar__content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(var(--error-text-rgb), 0.12);
|
||||
.loading {
|
||||
height: 390px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,19 +5,21 @@ import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import {AuthorType, MarketplacePlugin, ReleaseStage} from '@mattermost/types/marketplace';
|
||||
import type {PluginStatusRedux} from '@mattermost/types/plugins';
|
||||
|
||||
import {trackEvent} from 'actions/telemetry_actions.jsx';
|
||||
import {ActionFunc} from 'mattermost-redux/types/actions';
|
||||
|
||||
import MarketplaceModal, {AllListing, InstalledListing, MarketplaceModalProps} from './marketplace_modal';
|
||||
import {GlobalState} from 'types/store';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
jest.mock('actions/telemetry_actions.jsx', () => {
|
||||
const original = jest.requireActual('actions/telemetry_actions.jsx');
|
||||
return {
|
||||
...original,
|
||||
trackEvent: jest.fn(),
|
||||
};
|
||||
});
|
||||
import MarketplaceModal, {OpenedFromType} from './marketplace_modal';
|
||||
|
||||
let mockState: GlobalState;
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux') as typeof import('react-redux'),
|
||||
useSelector: (selector: (state: typeof mockState) => unknown) => selector(mockState),
|
||||
useDispatch: jest.fn(() => (action: ActionFunc) => action),
|
||||
}));
|
||||
|
||||
describe('components/marketplace/', () => {
|
||||
const samplePlugin: MarketplacePlugin = {
|
||||
@@ -52,161 +54,106 @@ describe('components/marketplace/', () => {
|
||||
installed_version: '1.0.3',
|
||||
};
|
||||
|
||||
describe('AllListing', () => {
|
||||
it('should render with no plugins', () => {
|
||||
const wrapper = shallow(
|
||||
<AllListing listing={[]}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
const defaultProps = {
|
||||
openedFrom: 'actions_menu' as OpenedFromType,
|
||||
};
|
||||
|
||||
it('should render with one plugin', () => {
|
||||
const wrapper = shallow(
|
||||
<AllListing listing={[samplePlugin]}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render with plugins', () => {
|
||||
const wrapper = shallow(
|
||||
<AllListing listing={[samplePlugin, sampleInstalledPlugin]}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('InstalledPlugins', () => {
|
||||
const baseProps = {
|
||||
changeTab: jest.fn(),
|
||||
};
|
||||
|
||||
it('should render with no plugins', () => {
|
||||
const wrapper = shallow(
|
||||
<InstalledListing
|
||||
{...baseProps}
|
||||
installedItems={[]}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render with one plugin', () => {
|
||||
const wrapper = shallow(
|
||||
<InstalledListing
|
||||
{...baseProps}
|
||||
installedItems={[sampleInstalledPlugin]}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render with multiple plugins', () => {
|
||||
const wrapper = shallow(
|
||||
<InstalledListing
|
||||
{...baseProps}
|
||||
installedItems={[sampleInstalledPlugin, sampleInstalledPlugin]}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MarketplaceModal', () => {
|
||||
const baseProps: MarketplaceModalProps = {
|
||||
show: true,
|
||||
listing: [samplePlugin],
|
||||
installedListing: [],
|
||||
pluginStatuses: {},
|
||||
siteURL: 'http://example.com',
|
||||
firstAdminVisitMarketplaceStatus: false,
|
||||
openedFrom: 'actions_menu',
|
||||
actions: {
|
||||
closeModal: jest.fn(),
|
||||
fetchListing: jest.fn(() => {
|
||||
return Promise.resolve({});
|
||||
}),
|
||||
filterListing: jest.fn(() => {
|
||||
return Promise.resolve({});
|
||||
}),
|
||||
setFirstAdminVisitMarketplaceStatus: jest.fn(),
|
||||
getPluginStatuses: jest.fn(),
|
||||
beforeEach(() => {
|
||||
mockState = {
|
||||
views: {
|
||||
modals: {
|
||||
modalState: {
|
||||
[ModalIdentifiers.PLUGIN_MARKETPLACE]: {
|
||||
open: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
marketplace: {
|
||||
plugins: [],
|
||||
apps: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
entities: {
|
||||
general: {
|
||||
firstAdminCompleteSetup: false,
|
||||
},
|
||||
admin: {
|
||||
pluginStatuses: {},
|
||||
},
|
||||
},
|
||||
} as unknown as GlobalState;
|
||||
});
|
||||
|
||||
test('should render with no plugins installed', () => {
|
||||
const wrapper = shallow<MarketplaceModal>(
|
||||
<MarketplaceModal {...baseProps}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('should render default', () => {
|
||||
const wrapper = shallow(
|
||||
<MarketplaceModal {...defaultProps}/>,
|
||||
);
|
||||
|
||||
test('should render with plugins installed', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
plugins: [
|
||||
...baseProps.listing,
|
||||
sampleInstalledPlugin,
|
||||
],
|
||||
installedListing: [
|
||||
sampleInstalledPlugin,
|
||||
],
|
||||
};
|
||||
expect(wrapper.shallow()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
const wrapper = shallow<MarketplaceModal>(
|
||||
<MarketplaceModal {...props}/>,
|
||||
);
|
||||
test('should render with no plugins available', () => {
|
||||
const setState = jest.fn();
|
||||
const useStateSpy = jest.spyOn(React, 'useState');
|
||||
useStateSpy.mockImplementationOnce(() => [false, setState]);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
const wrapper = shallow(
|
||||
<MarketplaceModal {...defaultProps}/>,
|
||||
);
|
||||
|
||||
test('should fetch plugins when plugin status is changed', () => {
|
||||
const fetchListing = baseProps.actions.fetchListing;
|
||||
const wrapper = shallow<MarketplaceModal>(<MarketplaceModal {...baseProps}/>);
|
||||
wrapper.update();
|
||||
|
||||
expect(fetchListing).toBeCalledTimes(1);
|
||||
wrapper.setProps({...baseProps});
|
||||
expect(fetchListing).toBeCalledTimes(1);
|
||||
expect(wrapper.shallow()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
const status = {
|
||||
id: 'test',
|
||||
} as PluginStatusRedux;
|
||||
wrapper.setProps({...baseProps, pluginStatuses: {test: status}});
|
||||
expect(fetchListing).toBeCalledTimes(2);
|
||||
});
|
||||
test('should render with plugins available', () => {
|
||||
const setState = jest.fn();
|
||||
const useStateSpy = jest.spyOn(React, 'useState');
|
||||
useStateSpy.mockImplementationOnce(() => [false, setState]);
|
||||
|
||||
test('should render with error banner', () => {
|
||||
const wrapper = shallow<MarketplaceModal>(
|
||||
<MarketplaceModal {...baseProps}/>,
|
||||
);
|
||||
mockState.views.marketplace.plugins = [
|
||||
samplePlugin,
|
||||
];
|
||||
|
||||
wrapper.setState({serverError: {name: 'some.error', message: 'Error test'}});
|
||||
const wrapper = shallow(
|
||||
<MarketplaceModal {...defaultProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
test('Should call for track event when searching', () => {
|
||||
const wrapper = shallow<MarketplaceModal>(
|
||||
<MarketplaceModal {...baseProps}/>,
|
||||
);
|
||||
expect(wrapper.shallow()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
wrapper.setState({filter: 'nps'});
|
||||
wrapper.instance().doSearch();
|
||||
test('should render with plugins installed', () => {
|
||||
const setState = jest.fn();
|
||||
const useStateSpy = jest.spyOn(React, 'useState');
|
||||
useStateSpy.mockImplementationOnce(() => [false, setState]);
|
||||
|
||||
expect(trackEvent).toHaveBeenCalledWith('plugins', 'ui_marketplace_opened', {from: 'actions_menu'});
|
||||
expect(trackEvent).toHaveBeenCalledWith('plugins', 'ui_marketplace_search', {filter: 'nps'});
|
||||
});
|
||||
mockState.views.marketplace.plugins = [
|
||||
samplePlugin,
|
||||
sampleInstalledPlugin,
|
||||
];
|
||||
|
||||
test('Should call for opened track event on mount', () => {
|
||||
const openedFrom = 'actions_menu';
|
||||
const wrapper = shallow(
|
||||
<MarketplaceModal {...defaultProps}/>,
|
||||
);
|
||||
|
||||
shallow<MarketplaceModal>(
|
||||
<MarketplaceModal
|
||||
{...baseProps}
|
||||
openedFrom={openedFrom}
|
||||
/>,
|
||||
);
|
||||
wrapper.update();
|
||||
|
||||
expect(trackEvent).toHaveBeenCalledWith('plugins', 'ui_marketplace_opened', {from: openedFrom});
|
||||
});
|
||||
expect(wrapper.shallow()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render with error banner', () => {
|
||||
const setState = jest.fn();
|
||||
const useStateSpy = jest.spyOn(React, 'useState');
|
||||
useStateSpy.mockImplementation(() => [true, setState]);
|
||||
|
||||
const wrapper = shallow(
|
||||
<MarketplaceModal {...defaultProps}/>,
|
||||
);
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.shallow()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,283 +1,269 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import debounce from 'lodash/debounce';
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {Tabs, Tab, SelectCallback} from 'react-bootstrap';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import {Link} from 'react-router-dom';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
import {PluginStatusRedux} from '@mattermost/types/plugins';
|
||||
import type {MarketplaceApp, MarketplacePlugin} from '@mattermost/types/marketplace';
|
||||
import {MagnifyIcon} from '@mattermost/compass-icons/components';
|
||||
|
||||
import FullScreenModal from 'components/widgets/modals/full_screen_modal';
|
||||
import RootPortal from 'components/root_portal';
|
||||
import QuickInput from 'components/quick_input';
|
||||
import LocalizedInput from 'components/localized_input/localized_input';
|
||||
import PluginIcon from 'components/widgets/icons/plugin_icon';
|
||||
import LoadingScreen from 'components/loading_screen';
|
||||
import FormattedMarkdownMessage from 'components/formatted_markdown_message';
|
||||
import {FooterPagination} from '@mattermost/components';
|
||||
import {getPluginStatuses} from 'mattermost-redux/actions/admin';
|
||||
import {setFirstAdminVisitMarketplaceStatus} from 'mattermost-redux/actions/general';
|
||||
import {getFirstAdminVisitMarketplaceStatus} from 'mattermost-redux/selectors/entities/general';
|
||||
import {ActionResult} from 'mattermost-redux/types/actions';
|
||||
|
||||
import {fetchListing, filterListing} from 'actions/marketplace';
|
||||
import {trackEvent} from 'actions/telemetry_actions.jsx';
|
||||
import {t} from 'utils/i18n';
|
||||
import {localizeMessage} from 'utils/utils';
|
||||
import {closeModal} from 'actions/views/modals';
|
||||
|
||||
import GenericModal from 'components/generic_modal';
|
||||
import LoadingScreen from 'components/loading_screen';
|
||||
import Input, {SIZE} from 'components/widgets/inputs/input/input';
|
||||
|
||||
import {getListing, getInstalledListing} from 'selectors/views/marketplace';
|
||||
import {isModalOpen} from 'selectors/views/modals';
|
||||
import {GlobalState} from 'types/store';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
import './marketplace_modal.scss';
|
||||
import MarketplaceList from './marketplace_list/marketplace_list';
|
||||
|
||||
import MarketplaceList, {ITEMS_PER_PAGE} from './marketplace_list/marketplace_list';
|
||||
|
||||
const MarketplaceTabs = {
|
||||
ALL_LISTING: 'allListing',
|
||||
ALL_LISTING: 'all',
|
||||
INSTALLED_LISTING: 'installed',
|
||||
};
|
||||
|
||||
const SEARCH_TIMEOUT_MILLISECONDS = 200;
|
||||
|
||||
const linkConsole = (msg: string) => (
|
||||
<Link to='/admin_console/plugins/plugin_management'>
|
||||
{msg}
|
||||
</Link>
|
||||
);
|
||||
|
||||
export type OpenedFromType = 'actions_menu' | 'app_bar' | 'channel_header' | 'command' | 'open_plugin_install_post' | 'product_menu';
|
||||
|
||||
type AllListingProps = {
|
||||
listing: Array<MarketplacePlugin | MarketplaceApp>;
|
||||
};
|
||||
|
||||
// AllListing renders the contents of the all listing tab.
|
||||
export const AllListing = ({listing}: AllListingProps): JSX.Element => {
|
||||
if (listing.length === 0) {
|
||||
return (
|
||||
<div className='no_plugins_div'>
|
||||
<br/>
|
||||
<PluginIcon className='icon__plugin'/>
|
||||
<div className='mt-3 light'>
|
||||
<FormattedMessage
|
||||
id='marketplace_modal.no_plugins'
|
||||
defaultMessage='There are no plugins available at this time.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <MarketplaceList listing={listing}/>;
|
||||
};
|
||||
|
||||
type InstalledListingProps = {
|
||||
installedItems: Array<MarketplacePlugin | MarketplaceApp>;
|
||||
changeTab: SelectCallback;
|
||||
};
|
||||
|
||||
// InstalledListing renders the contents of the installed listing tab.
|
||||
export const InstalledListing = ({installedItems, changeTab}: InstalledListingProps): JSX.Element => {
|
||||
if (installedItems.length === 0) {
|
||||
return (
|
||||
<div className='no_plugins_div'>
|
||||
<br/>
|
||||
<PluginIcon className='icon__plugin'/>
|
||||
<div className='mt-3 light'>
|
||||
<FormattedMessage
|
||||
id='marketplace_modal.no_plugins_installed'
|
||||
defaultMessage='You do not have any plugins installed.'
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className='mt-5 style--none color--link'
|
||||
onClick={() => changeTab(MarketplaceTabs.ALL_LISTING)}
|
||||
data-testid='Install-Plugins-button'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='marketplace_modal.install_plugins'
|
||||
defaultMessage='Install Plugins'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <MarketplaceList listing={installedItems}/>;
|
||||
};
|
||||
|
||||
export type MarketplaceModalProps = {
|
||||
show: boolean;
|
||||
listing: Array<MarketplacePlugin | MarketplaceApp>;
|
||||
installedListing: Array<MarketplacePlugin | MarketplaceApp>;
|
||||
siteURL: string;
|
||||
pluginStatuses?: Record<string, PluginStatusRedux>;
|
||||
firstAdminVisitMarketplaceStatus: boolean;
|
||||
type MarketplaceModalProps = {
|
||||
openedFrom: OpenedFromType;
|
||||
actions: {
|
||||
closeModal: () => void;
|
||||
fetchListing(localOnly?: boolean): Promise<{error?: Error}>;
|
||||
filterListing(filter: string): Promise<{error?: Error}>;
|
||||
setFirstAdminVisitMarketplaceStatus(): Promise<void>;
|
||||
getPluginStatuses(): Promise<void>;
|
||||
};
|
||||
};
|
||||
|
||||
type MarketplaceModalState = {
|
||||
tabKey: unknown;
|
||||
loading: boolean;
|
||||
serverError?: Error;
|
||||
filter: string;
|
||||
};
|
||||
|
||||
// MarketplaceModal is the marketplace modal.
|
||||
export default class MarketplaceModal extends React.PureComponent<MarketplaceModalProps, MarketplaceModalState> {
|
||||
private filterRef: React.RefObject<HTMLInputElement>;
|
||||
|
||||
constructor(props: MarketplaceModalProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
tabKey: MarketplaceTabs.ALL_LISTING,
|
||||
loading: true,
|
||||
serverError: undefined,
|
||||
filter: '',
|
||||
};
|
||||
|
||||
this.filterRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
trackEvent('plugins', 'ui_marketplace_opened', {from: this.props.openedFrom});
|
||||
|
||||
this.fetchListing();
|
||||
this.props.actions.getPluginStatuses();
|
||||
if (!this.props.firstAdminVisitMarketplaceStatus) {
|
||||
trackEvent('plugins', 'ui_first_admin_visit_marketplace_status');
|
||||
|
||||
this.props.actions.setFirstAdminVisitMarketplaceStatus();
|
||||
}
|
||||
|
||||
this.filterRef.current?.focus();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: MarketplaceModalProps): void {
|
||||
// Automatically refresh the component when a plugin is installed or uninstalled.
|
||||
if (this.props.pluginStatuses !== prevProps.pluginStatuses) {
|
||||
this.fetchListing();
|
||||
}
|
||||
}
|
||||
|
||||
fetchListing = async (): Promise<void> => {
|
||||
const {error} = await this.props.actions.fetchListing();
|
||||
this.setState({loading: false, serverError: error});
|
||||
}
|
||||
|
||||
close = (): void => {
|
||||
trackEvent('plugins', 'ui_marketplace_closed');
|
||||
this.props.actions.closeModal();
|
||||
}
|
||||
|
||||
changeTab: SelectCallback = (tabKey: any): void => {
|
||||
this.setState({tabKey});
|
||||
}
|
||||
|
||||
onInput = (): void => {
|
||||
if (this.filterRef.current) {
|
||||
this.setState({filter: this.filterRef.current.value});
|
||||
|
||||
this.debouncedSearch();
|
||||
}
|
||||
}
|
||||
|
||||
handleClearSearch = (): void => {
|
||||
if (this.filterRef.current) {
|
||||
this.filterRef.current.value = '';
|
||||
this.setState({filter: this.filterRef.current.value}, this.doSearch);
|
||||
}
|
||||
}
|
||||
|
||||
doSearch = async (): Promise<void> => {
|
||||
trackEvent('plugins', 'ui_marketplace_search', {filter: this.state.filter});
|
||||
|
||||
const {error} = await this.props.actions.filterListing(this.state.filter);
|
||||
|
||||
this.setState({serverError: error});
|
||||
}
|
||||
|
||||
debouncedSearch = debounce(this.doSearch, SEARCH_TIMEOUT_MILLISECONDS);
|
||||
|
||||
render(): JSX.Element {
|
||||
const input = (
|
||||
<div className='filter-row filter-row--full'>
|
||||
<div className='col-sm-12'>
|
||||
<QuickInput
|
||||
id='searchMarketplaceTextbox'
|
||||
ref={this.filterRef}
|
||||
className='form-control filter-textbox search_input'
|
||||
placeholder={{id: t('marketplace_modal.search'), defaultMessage: 'Search Marketplace'}}
|
||||
inputComponent={LocalizedInput}
|
||||
onInput={this.onInput}
|
||||
value={this.state.filter}
|
||||
clearable={true}
|
||||
onClear={this.handleClearSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
let errorBanner = null;
|
||||
if (this.state.serverError) {
|
||||
errorBanner = (
|
||||
<div
|
||||
className='error-bar'
|
||||
id='error_bar'
|
||||
>
|
||||
<div className='error-bar__content'>
|
||||
<FormattedMarkdownMessage
|
||||
id='app.plugin.marketplace_plugins.app_error'
|
||||
defaultMessage='Error connecting to the marketplace server. Please check your settings in the [System Console]({siteURL}/admin_console/plugins/plugin_management).'
|
||||
values={{siteURL: this.props.siteURL}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RootPortal>
|
||||
<FullScreenModal
|
||||
show={this.props.show}
|
||||
onClose={this.close}
|
||||
ariaLabel={localizeMessage('marketplace_modal.title', 'Marketplace')}
|
||||
>
|
||||
{errorBanner}
|
||||
<div
|
||||
className='modal-marketplace'
|
||||
id='modal_marketplace'
|
||||
>
|
||||
<h1>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id='marketplace_modal.title'
|
||||
defaultMessage='Marketplace'
|
||||
/>
|
||||
</strong>
|
||||
</h1>
|
||||
{input}
|
||||
<Tabs
|
||||
id='marketplaceTabs'
|
||||
className='tabs'
|
||||
defaultActiveKey={MarketplaceTabs.ALL_LISTING}
|
||||
activeKey={this.state.tabKey}
|
||||
onSelect={this.changeTab}
|
||||
unmountOnExit={true}
|
||||
>
|
||||
<Tab
|
||||
eventKey={MarketplaceTabs.ALL_LISTING}
|
||||
title={localizeMessage('marketplace_modal.tabs.all_listing', 'All')}
|
||||
>
|
||||
{this.state.loading ? <LoadingScreen/> : <AllListing listing={this.props.listing}/>}
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey={MarketplaceTabs.INSTALLED_LISTING}
|
||||
title={localizeMessage('marketplace_modal.tabs.installed_listing', 'Installed') + ` (${this.props.installedListing.length})`}
|
||||
>
|
||||
<InstalledListing
|
||||
installedItems={this.props.installedListing}
|
||||
changeTab={this.changeTab}
|
||||
/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
</FullScreenModal>
|
||||
</RootPortal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const MarketplaceModal = ({
|
||||
openedFrom,
|
||||
}: MarketplaceModalProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const {formatMessage} = useIntl();
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const show = useSelector((state: GlobalState) => isModalOpen(state, ModalIdentifiers.PLUGIN_MARKETPLACE));
|
||||
const listing = useSelector(getListing);
|
||||
const installedListing = useSelector(getInstalledListing);
|
||||
const pluginStatuses = useSelector((state: GlobalState) => state.entities.admin.pluginStatuses);
|
||||
const hasFirstAdminVisitedMarketplace = useSelector(getFirstAdminVisitMarketplaceStatus);
|
||||
|
||||
const [tabKey, setTabKey] = useState(MarketplaceTabs.ALL_LISTING);
|
||||
const [filter, setFilter] = useState('');
|
||||
const [page, setPage] = useState(0);
|
||||
const [hasLoaded, setHasLoaded] = useState(false);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [serverError, setServerError] = React.useState(false);
|
||||
|
||||
const doFetchListing = useCallback(async () => {
|
||||
const {error} = await dispatch(fetchListing()) as ActionResult;
|
||||
|
||||
if (error) {
|
||||
setServerError(true);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
const doSearch = useCallback(async () => {
|
||||
trackEvent('plugins', 'ui_marketplace_search', {filter});
|
||||
|
||||
const {error} = await dispatch(filterListing(filter)) as ActionResult;
|
||||
|
||||
if (error) {
|
||||
setServerError(true);
|
||||
}
|
||||
}, [filter]);
|
||||
|
||||
const debouncedSearch = debounce(doSearch, SEARCH_TIMEOUT_MILLISECONDS);
|
||||
|
||||
useEffect(() => {
|
||||
async function doFetch() {
|
||||
await dispatch(getPluginStatuses());
|
||||
await doFetchListing();
|
||||
setHasLoaded(true);
|
||||
}
|
||||
|
||||
trackEvent('plugins', 'ui_marketplace_opened', {from: openedFrom});
|
||||
|
||||
if (!hasFirstAdminVisitedMarketplace) {
|
||||
trackEvent('plugins', 'ui_first_admin_visit_marketplace_status');
|
||||
dispatch(setFirstAdminVisitMarketplaceStatus());
|
||||
}
|
||||
|
||||
doFetch();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasLoaded) {
|
||||
doFetchListing();
|
||||
}
|
||||
}, [pluginStatuses]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasLoaded) {
|
||||
debouncedSearch();
|
||||
setPage(0);
|
||||
}
|
||||
}, [filter]);
|
||||
|
||||
const scrollListToTop = useCallback(() => {
|
||||
if (listRef.current) {
|
||||
listRef.current.scrollTop = 0;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleOnClose = () => {
|
||||
trackEvent('plugins', 'ui_marketplace_closed');
|
||||
dispatch(closeModal(ModalIdentifiers.PLUGIN_MARKETPLACE));
|
||||
};
|
||||
|
||||
const handleChangeTab: SelectCallback = useCallback((tabKey) => {
|
||||
setTabKey(tabKey);
|
||||
setPage(0);
|
||||
scrollListToTop();
|
||||
}, [scrollListToTop]);
|
||||
|
||||
const handleOnChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFilter(event.target.value);
|
||||
}, []);
|
||||
|
||||
const handleOnClear = useCallback(() => {
|
||||
setFilter('');
|
||||
}, []);
|
||||
|
||||
const handleOnNextPage = useCallback(() => {
|
||||
setPage(page + 1);
|
||||
scrollListToTop();
|
||||
}, [page, scrollListToTop]);
|
||||
|
||||
const handleOnPreviousPage = useCallback(() => {
|
||||
setPage(page - 1);
|
||||
scrollListToTop();
|
||||
}, [page, scrollListToTop]);
|
||||
|
||||
const handleNoResultsButtonClick = useCallback(() => {
|
||||
handleChangeTab(MarketplaceTabs.ALL_LISTING);
|
||||
}, [handleChangeTab]);
|
||||
|
||||
const getHeaderInput = useCallback(() => (
|
||||
<Input
|
||||
id='searchMarketplaceTextbox'
|
||||
name='searchMarketplaceTextbox'
|
||||
containerClassName='marketplace-modal-search'
|
||||
inputClassName='search_input'
|
||||
type='text'
|
||||
inputSize={SIZE.LARGE}
|
||||
inputPrefix={<MagnifyIcon size={24}/>}
|
||||
placeholder={formatMessage({id: 'marketplace_modal.search', defaultMessage: 'Search marketplace'})}
|
||||
useLegend={false}
|
||||
autoFocus={true}
|
||||
clearable={true}
|
||||
value={filter}
|
||||
onChange={handleOnChange}
|
||||
onClear={handleOnClear}
|
||||
/>
|
||||
), [filter, handleOnChange, handleOnClear]);
|
||||
|
||||
const getFooterContent = useCallback(() => (
|
||||
<FooterPagination
|
||||
page={page}
|
||||
total={tabKey === MarketplaceTabs.ALL_LISTING ? listing.length : installedListing.length}
|
||||
itemsPerPage={ITEMS_PER_PAGE}
|
||||
onNextPage={handleOnNextPage}
|
||||
onPreviousPage={handleOnPreviousPage}
|
||||
/>
|
||||
), [installedListing.length, listing.length, page, handleOnNextPage, handleOnPreviousPage, tabKey]);
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
id='marketplace-modal'
|
||||
className='marketplace-modal'
|
||||
modalHeaderText={formatMessage({id: 'marketplace_modal.title', defaultMessage: 'App Marketplace'})}
|
||||
ariaLabel={formatMessage({id: 'marketplace_modal.title', defaultMessage: 'App Marketplace'})}
|
||||
errorText={serverError ? (
|
||||
formatMessage(
|
||||
{
|
||||
id: 'marketplace_modal.app_error',
|
||||
defaultMessage: 'Error connecting to the marketplace server. Please check your settings in the <linkConsole>System Console</linkConsole>.',
|
||||
},
|
||||
{linkConsole},
|
||||
)
|
||||
) : undefined}
|
||||
show={show}
|
||||
compassDesign={true}
|
||||
bodyPadding={false}
|
||||
footerDivider={true}
|
||||
onExited={handleOnClose}
|
||||
footerContent={getFooterContent()}
|
||||
headerInput={getHeaderInput()}
|
||||
>
|
||||
<Tabs
|
||||
id='marketplaceTabs'
|
||||
className='tabs'
|
||||
defaultActiveKey={MarketplaceTabs.ALL_LISTING}
|
||||
activeKey={tabKey}
|
||||
onSelect={handleChangeTab}
|
||||
unmountOnExit={true}
|
||||
>
|
||||
<Tab
|
||||
eventKey={MarketplaceTabs.ALL_LISTING}
|
||||
title={formatMessage({id: 'marketplace_modal.tabs.all_listing', defaultMessage: 'All'})}
|
||||
>
|
||||
{loading ? (
|
||||
<LoadingScreen className='loading'/>
|
||||
) : (
|
||||
<MarketplaceList
|
||||
listRef={listRef}
|
||||
listing={listing}
|
||||
page={page}
|
||||
filter={filter}
|
||||
noResultsMessage={formatMessage({id: 'marketplace_modal.no_plugins', defaultMessage: 'No plugins found'})}
|
||||
/>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey={MarketplaceTabs.INSTALLED_LISTING}
|
||||
title={formatMessage(
|
||||
{id: 'marketplace_modal.tabs.installed_listing', defaultMessage: 'Installed ({count})'},
|
||||
{count: installedListing.length},
|
||||
)}
|
||||
>
|
||||
<MarketplaceList
|
||||
listRef={listRef}
|
||||
listing={installedListing}
|
||||
page={page}
|
||||
filter={filter}
|
||||
noResultsMessage={formatMessage({
|
||||
id: 'marketplace_modal.no_plugins_installed',
|
||||
defaultMessage: 'No plugins installed found',
|
||||
})}
|
||||
noResultsAction={{
|
||||
label: formatMessage({id: 'marketplace_modal.install_plugins', defaultMessage: 'Install plugins'}),
|
||||
onClick: handleNoResultsButtonClick,
|
||||
}}
|
||||
/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</GenericModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketplaceModal;
|
||||
|
||||
@@ -4,6 +4,7 @@ exports[`ProductNoticesModal Match snapshot for single notice 1`] = `
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={false}
|
||||
autoCloseOnConfirmButton={true}
|
||||
bodyPadding={true}
|
||||
cancelButtonText={null}
|
||||
className="productNotices"
|
||||
confirmButtonText={
|
||||
@@ -44,6 +45,7 @@ exports[`ProductNoticesModal Match snapshot for user notice 1`] = `
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={false}
|
||||
autoCloseOnConfirmButton={true}
|
||||
bodyPadding={true}
|
||||
cancelButtonText={
|
||||
<React.Fragment>
|
||||
<PreviousIcon
|
||||
@@ -122,6 +124,7 @@ exports[`ProductNoticesModal Should match snapshot for system admin notice 1`] =
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={false}
|
||||
autoCloseOnConfirmButton={false}
|
||||
bodyPadding={true}
|
||||
cancelButtonText={null}
|
||||
className="productNotices"
|
||||
confirmButtonText={
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/widgets/inputs/Input should match snapshot 1`] = `
|
||||
<div
|
||||
className="Input_container"
|
||||
>
|
||||
<fieldset
|
||||
className="Input_fieldset"
|
||||
>
|
||||
<legend
|
||||
className="Input_legend"
|
||||
/>
|
||||
<div
|
||||
className="Input_wrapper"
|
||||
>
|
||||
<input
|
||||
className="Input form-control medium"
|
||||
id="input_"
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
`;
|
||||
@@ -49,11 +49,16 @@
|
||||
.Input_wrapper {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
margin: 2px 0;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
|
||||
> :not(:first-child) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.Input_limit-exceeded {
|
||||
@@ -196,4 +201,10 @@
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
.Input__clear {
|
||||
display: flex;
|
||||
color: rgba(var(--center-channel-color), 0.68);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {mount, shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
|
||||
import Input from './input';
|
||||
|
||||
describe('components/widgets/inputs/Input', () => {
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<Input/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render with clearable enabled', () => {
|
||||
const value = 'value';
|
||||
const clearableTooltipText = 'tooltip text';
|
||||
const onClear = jest.fn();
|
||||
|
||||
const wrapper = shallow(
|
||||
<Input
|
||||
value={value}
|
||||
clearable={true}
|
||||
clearableTooltipText={clearableTooltipText}
|
||||
onClear={onClear}
|
||||
/>,
|
||||
);
|
||||
|
||||
const clear = wrapper.find('.Input__clear');
|
||||
expect(clear.length).toEqual(1);
|
||||
expect(wrapper.find('CloseCircleIcon').length).toEqual(1);
|
||||
|
||||
const tooltip = wrapper.find(OverlayTrigger);
|
||||
expect(tooltip.length).toEqual(1);
|
||||
|
||||
const overlay = mount(tooltip.prop('overlay'));
|
||||
expect(overlay.text()).toEqual(clearableTooltipText);
|
||||
|
||||
clear.first().simulate('mousedown');
|
||||
|
||||
expect(onClear).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -3,10 +3,14 @@
|
||||
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {ItemStatus} from 'utils/constants';
|
||||
import {CloseCircleIcon} from '@mattermost/compass-icons/components';
|
||||
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
import Tooltip from 'components/tooltip';
|
||||
|
||||
import Constants, {ItemStatus} from 'utils/constants';
|
||||
|
||||
import './input.scss';
|
||||
|
||||
@@ -32,6 +36,9 @@ interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
useLegend?: boolean;
|
||||
customMessage?: CustomMessageInputType;
|
||||
inputSize?: SIZE;
|
||||
clearable?: boolean;
|
||||
clearableTooltipText?: string;
|
||||
onClear?: () => void;
|
||||
}
|
||||
|
||||
const Input = React.forwardRef((
|
||||
@@ -56,9 +63,12 @@ const Input = React.forwardRef((
|
||||
maxLength,
|
||||
inputSize = SIZE.MEDIUM,
|
||||
disabled,
|
||||
clearable,
|
||||
clearableTooltipText,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onChange,
|
||||
onClear,
|
||||
...otherProps
|
||||
}: InputProps,
|
||||
ref?: React.Ref<HTMLInputElement>,
|
||||
@@ -109,6 +119,12 @@ const Input = React.forwardRef((
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnClear = () => {
|
||||
if (onClear) {
|
||||
onClear();
|
||||
}
|
||||
};
|
||||
|
||||
const validateInput = () => {
|
||||
if (!required || (value !== null && value !== '')) {
|
||||
return;
|
||||
@@ -121,6 +137,26 @@ const Input = React.forwardRef((
|
||||
const error = customInputLabel?.type === 'error';
|
||||
const limitExceeded = limit && value && !Array.isArray(value) ? value.toString().length - limit : 0;
|
||||
|
||||
const clearButton = value && clearable ? (
|
||||
<div
|
||||
className='Input__clear'
|
||||
onMouseDown={handleOnClear}
|
||||
onTouchEnd={handleOnClear}
|
||||
>
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
placement='bottom'
|
||||
overlay={(
|
||||
<Tooltip id={'InputClearTooltip'}>
|
||||
{clearableTooltipText || formatMessage({id: 'widget.input.clear', defaultMessage: 'Clear'})}
|
||||
</Tooltip>
|
||||
)}
|
||||
>
|
||||
<CloseCircleIcon size={18}/>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className={classNames('Input_container', containerClassName, {disabled})}>
|
||||
<fieldset
|
||||
@@ -157,6 +193,7 @@ const Input = React.forwardRef((
|
||||
</span>
|
||||
)}
|
||||
{inputSuffix}
|
||||
{clearButton}
|
||||
</div>
|
||||
{addon}
|
||||
</fieldset>
|
||||
|
||||
@@ -2652,7 +2652,6 @@
|
||||
"app.channel.post_update_channel_purpose_message.removed": "{username} removed the channel purpose (was: {old})",
|
||||
"app.channel.post_update_channel_purpose_message.updated_from": "{username} updated the channel purpose from: {old} to: {new}",
|
||||
"app.channel.post_update_channel_purpose_message.updated_to": "{username} updated the channel purpose to: {new}",
|
||||
"app.plugin.marketplace_plugins.app_error": "Error connecting to the marketplace server. Please check your settings in the [System Console]({siteURL}/admin_console/plugins/plugin_management).",
|
||||
"apps.error": "Error: {error}",
|
||||
"apps.error.command.field_missing": "Required fields missing: `{fieldName}`.",
|
||||
"apps.error.command.same_channel": "Channel repeated for field `{fieldName}`: `{option}`.",
|
||||
@@ -3441,6 +3440,9 @@
|
||||
"filtered_user_list.userStatus": "User Status:",
|
||||
"flag_post.flag": "Save",
|
||||
"flag_post.unflag": "Remove from Saved",
|
||||
"footer_pagination.count": "Showing {startCount, number}-{endCount, number} of {total, number}",
|
||||
"footer_pagination.next": "Next",
|
||||
"footer_pagination.prev": "Previous",
|
||||
"forward_post_button.label": "Forward",
|
||||
"forward_post_modal.button.cancel": "Cancel",
|
||||
"forward_post_modal.button.forward": "Forward",
|
||||
@@ -4085,8 +4087,9 @@
|
||||
"mark_all_threads_as_read_modal.title": "Mark all your threads as read?",
|
||||
"marketplace_command.disabled": "The marketplace is disabled. Please contact your System Administrator for details.",
|
||||
"marketplace_command.no_permission": "You do not have the appropriate permissions to access the marketplace.",
|
||||
"marketplace_list.count_total_page": "{startCount, number} - {endCount, number} {total, plural, one {plugin} other {plugins}} of {total, number} total",
|
||||
"marketplace_modal.install_plugins": "Install Plugins",
|
||||
"marketplace_modal_list.no_plugins_filter": "No results for \"{filter}\"",
|
||||
"marketplace_modal.app_error": "Error connecting to the marketplace server. Please check your settings in the <linkConsole>System Console</linkConsole>.",
|
||||
"marketplace_modal.install_plugins": "Install plugins",
|
||||
"marketplace_modal.installing": "Installing...",
|
||||
"marketplace_modal.list.configure": "Configure",
|
||||
"marketplace_modal.list.configure.plugin": "Configure {plugin}",
|
||||
@@ -4103,12 +4106,12 @@
|
||||
"marketplace_modal.list.update_confirmation.message.warning_major_version": "This update may contain breaking changes.",
|
||||
"marketplace_modal.list.update_confirmation.message.warning_major_version_with_release_notes": "This update may contain breaking changes. Consult the [release notes](!{releaseNotesUrl}) before upgrading.",
|
||||
"marketplace_modal.list.update_confirmation.title": "Confirm Plugin Update",
|
||||
"marketplace_modal.no_plugins": "There are no plugins available at this time.",
|
||||
"marketplace_modal.no_plugins_installed": "You do not have any plugins installed.",
|
||||
"marketplace_modal.search": "Search Marketplace",
|
||||
"marketplace_modal.no_plugins": "No plugins found",
|
||||
"marketplace_modal.no_plugins_installed": "No plugins installed found",
|
||||
"marketplace_modal.search": "Search marketplace",
|
||||
"marketplace_modal.tabs.all_listing": "All",
|
||||
"marketplace_modal.tabs.installed_listing": "Installed",
|
||||
"marketplace_modal.title": "Marketplace",
|
||||
"marketplace_modal.tabs.installed_listing": "Installed ({count})",
|
||||
"marketplace_modal.title": "App Marketplace",
|
||||
"members_popover.button.message": "message",
|
||||
"menu.cloudFree.enterpriseTrialDescription": "Your trial is active until {trialEndDay}. Discover our top Enterprise features. <openModalLink>Learn more</openModalLink>",
|
||||
"menu.cloudFree.enterpriseTrialTitle": "Enterprise Trial",
|
||||
@@ -5676,6 +5679,7 @@
|
||||
"welcome_post_renderer.user_message.first_paragraph": "Mattermost is an open source platform for secure communication, collaboration, and orchestration of work across tools and teams.",
|
||||
"welcome_post_renderer.user_message.second_paragraph": "Here is a list of commands to use to try and get familiar with the platform.",
|
||||
"welcome_post_renderer.user_message.title": "Welcome to Mattermost! :rocket:",
|
||||
"widget.input.clear": "Clear",
|
||||
"widget.input.required": "This field is required",
|
||||
"widget.passwordInput.createPassword": "Choose a Password",
|
||||
"widget.passwordInput.password": "Password",
|
||||
|
||||
@@ -114,9 +114,13 @@
|
||||
display: flex;
|
||||
font-size: 18px;
|
||||
|
||||
&::before {
|
||||
&:first-child::before {
|
||||
margin: 0 7px 0 0;
|
||||
}
|
||||
|
||||
&:last-child::before {
|
||||
margin: 0 0 0 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +150,7 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
&:disabled:not(.always-show-enabled) {
|
||||
background: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
color: rgba(var(--center-channel-color-rgb), 0.32);
|
||||
cursor: not-allowed;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/GenericModal/FooterPagination should render default 1`] = `
|
||||
<div
|
||||
className="footer-pagination"
|
||||
>
|
||||
<div
|
||||
className="footer-pagination__legend"
|
||||
/>
|
||||
<div
|
||||
className="footer-pagination__button-container"
|
||||
>
|
||||
<button
|
||||
className="footer-pagination__button-container__button disabled"
|
||||
disabled={true}
|
||||
onClick={[MockFunction]}
|
||||
type="button"
|
||||
>
|
||||
<ChevronLeftIcon
|
||||
size={16}
|
||||
/>
|
||||
<span>
|
||||
Previous
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="footer-pagination__button-container__button disabled"
|
||||
disabled={true}
|
||||
onClick={[MockFunction]}
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Next
|
||||
</span>
|
||||
<ChevronRightIcon
|
||||
size={16}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
@import '../../../../../channels/src/sass/utils/mixins';
|
||||
|
||||
.footer-pagination {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
&__legend {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
&__button-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&__button {
|
||||
@include tertiary-button;
|
||||
@include button-small;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
> :not(:first-child) {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import {FooterPagination} from './';
|
||||
|
||||
describe('components/GenericModal/FooterPagination', () => {
|
||||
const baseProps = {
|
||||
page: 0,
|
||||
total: 0,
|
||||
itemsPerPage: 0,
|
||||
onNextPage: jest.fn(),
|
||||
onPreviousPage: jest.fn(),
|
||||
};
|
||||
|
||||
test('should render default', () => {
|
||||
const wrapper = shallow(
|
||||
<FooterPagination {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render pagination legend', () => {
|
||||
const wrapper = shallow(
|
||||
<FooterPagination
|
||||
{...baseProps}
|
||||
page={0}
|
||||
total={17}
|
||||
itemsPerPage={10}
|
||||
/>,
|
||||
);
|
||||
|
||||
const legend = wrapper.find('.footer-pagination__legend');
|
||||
|
||||
expect(legend.length).toEqual(1);
|
||||
expect(legend.at(0).text()).toEqual('Showing 1-10 of 17');
|
||||
});
|
||||
|
||||
test('should render pagination buttons', () => {
|
||||
const wrapper = shallow(
|
||||
<FooterPagination
|
||||
{...baseProps}
|
||||
page={1}
|
||||
total={30}
|
||||
itemsPerPage={10}
|
||||
/>,
|
||||
);
|
||||
|
||||
const buttons = wrapper.find('.footer-pagination__button-container__button');
|
||||
|
||||
expect(buttons.length).toEqual(2);
|
||||
expect(buttons.at(0).text()).toEqual('<ChevronLeftIcon />Previous');
|
||||
expect(buttons.at(1).text()).toEqual('Next<ChevronRightIcon />');
|
||||
});
|
||||
|
||||
test('should handle pagination buttons', async () => {
|
||||
const onPreviousPage = jest.fn();
|
||||
const onNextPage = jest.fn();
|
||||
|
||||
const wrapper = shallow(
|
||||
<FooterPagination
|
||||
page={1}
|
||||
total={30}
|
||||
itemsPerPage={10}
|
||||
onPreviousPage={onPreviousPage}
|
||||
onNextPage={onNextPage}
|
||||
/>,
|
||||
);
|
||||
|
||||
const buttons = wrapper.find('.footer-pagination__button-container__button');
|
||||
const prevButton = buttons.at(0);
|
||||
const nextButton = buttons.at(1);
|
||||
|
||||
expect(prevButton.hasClass('disabled')).toBeFalsy();
|
||||
expect(nextButton.hasClass('disabled')).toBeFalsy();
|
||||
|
||||
nextButton.simulate('click');
|
||||
|
||||
expect(onNextPage).toHaveBeenCalledTimes(1);
|
||||
|
||||
prevButton.simulate('click');
|
||||
|
||||
expect(onPreviousPage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import {ChevronLeftIcon, ChevronRightIcon} from '@mattermost/compass-icons/components';
|
||||
|
||||
import './footer_pagination.scss';
|
||||
|
||||
const BUTTON_ICON_SIZE = 16;
|
||||
|
||||
type FooterPaginationProps = {
|
||||
page: number;
|
||||
total: number;
|
||||
itemsPerPage: number;
|
||||
onNextPage: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
onPreviousPage: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
};
|
||||
|
||||
export const FooterPagination = ({
|
||||
page,
|
||||
total,
|
||||
itemsPerPage,
|
||||
onNextPage,
|
||||
onPreviousPage,
|
||||
}: FooterPaginationProps) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const startCount = page * itemsPerPage;
|
||||
const endCount = Math.min(startCount + itemsPerPage, total);
|
||||
const totalPages = Math.trunc((total - 1) / itemsPerPage);
|
||||
|
||||
const prevDisabled = page <= 0;
|
||||
const nextDisabled = page >= totalPages;
|
||||
|
||||
return (
|
||||
<div className='footer-pagination'>
|
||||
<div className='footer-pagination__legend'>
|
||||
{Boolean(total) && (
|
||||
formatMessage(
|
||||
{
|
||||
id: 'footer_pagination.count',
|
||||
defaultMessage: 'Showing {startCount, number}-{endCount, number} of {total, number}',
|
||||
},
|
||||
{
|
||||
startCount: startCount + 1,
|
||||
endCount,
|
||||
total,
|
||||
},
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className='footer-pagination__button-container'>
|
||||
<button
|
||||
type='button'
|
||||
className={classNames(
|
||||
'footer-pagination__button-container__button',
|
||||
{disabled: prevDisabled},
|
||||
)}
|
||||
onClick={onPreviousPage}
|
||||
disabled={prevDisabled}
|
||||
>
|
||||
<ChevronLeftIcon size={BUTTON_ICON_SIZE}/>
|
||||
<span>
|
||||
{formatMessage({
|
||||
id: 'footer_pagination.prev',
|
||||
defaultMessage: 'Previous',
|
||||
})}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className={classNames(
|
||||
'footer-pagination__button-container__button',
|
||||
{disabled: nextDisabled},
|
||||
)}
|
||||
onClick={onNextPage}
|
||||
disabled={nextDisabled}
|
||||
>
|
||||
<span>
|
||||
{formatMessage({
|
||||
id: 'footer_pagination.next',
|
||||
defaultMessage: 'Next',
|
||||
})}
|
||||
</span>
|
||||
<ChevronRightIcon size={BUTTON_ICON_SIZE}/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export * from './footer_pagination';
|
||||
@@ -34,13 +34,17 @@ export type Props = {
|
||||
enforceFocus?: boolean;
|
||||
container?: React.ReactNode | React.ReactNodeArray;
|
||||
ariaLabel?: string;
|
||||
errorText?: string;
|
||||
errorText?: string | React.ReactNode;
|
||||
compassDesign?: boolean;
|
||||
backdrop?: boolean;
|
||||
backdropClassName?: string;
|
||||
tabIndex?: number;
|
||||
children: React.ReactNode;
|
||||
keyboardEscape?: boolean;
|
||||
headerInput?: React.ReactNode;
|
||||
bodyPadding?: boolean;
|
||||
footerContent?: React.ReactNode;
|
||||
footerDivider?: boolean;
|
||||
};
|
||||
|
||||
type State = {
|
||||
@@ -56,6 +60,7 @@ export class GenericModal extends React.PureComponent<Props, State> {
|
||||
autoCloseOnConfirmButton: true,
|
||||
enforceFocus: true,
|
||||
keyboardEscape: true,
|
||||
bodyPadding: true,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
@@ -195,7 +200,12 @@ export class GenericModal extends React.PureComponent<Props, State> {
|
||||
className='GenericModal__wrapper-enter-key-press-catcher'
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
{this.props.compassDesign && headerText}
|
||||
{this.props.compassDesign && (
|
||||
<>
|
||||
{headerText}
|
||||
{this.props.headerInput}
|
||||
</>
|
||||
)}
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{this.props.compassDesign ? (
|
||||
@@ -208,14 +218,22 @@ export class GenericModal extends React.PureComponent<Props, State> {
|
||||
) : (
|
||||
headerText
|
||||
)}
|
||||
<div className='GenericModal__body'>
|
||||
<div className={classNames('GenericModal__body', {padding: this.props.bodyPadding})}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</Modal.Body>
|
||||
{(cancelButton || confirmButton) && <Modal.Footer>
|
||||
{cancelButton}
|
||||
{confirmButton}
|
||||
</Modal.Footer>}
|
||||
{(cancelButton || confirmButton || this.props.footerContent) && (
|
||||
<Modal.Footer className={classNames({divider: this.props.footerDivider})}>
|
||||
{(cancelButton || confirmButton) ? (
|
||||
<>
|
||||
{cancelButton}
|
||||
{confirmButton}
|
||||
</>
|
||||
) : (
|
||||
this.props.footerContent
|
||||
)}
|
||||
</Modal.Footer>
|
||||
)}
|
||||
</div>
|
||||
</FocusTrap>
|
||||
</Modal>
|
||||
|
||||
@@ -7,6 +7,7 @@ export type {CircleSkeletonLoaderProps, RectangleSkeletonLoaderProps} from './sk
|
||||
export type {Props as FocusTrapProps} from './focus_trap';
|
||||
|
||||
// components
|
||||
export * from './generic_modal/footer_content';
|
||||
export {GenericModal} from './generic_modal/generic_modal';
|
||||
export {CircleSkeletonLoader, RectangleSkeletonLoader} from './skeleton_loader';
|
||||
export * from './tour_tip';
|
||||
|
||||
Ссылка в новой задаче
Block a user