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 {GlobalState} from 'types/store';
|
||||||
|
|
||||||
import {t} from 'utils/i18n';
|
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 WorkTemplateModal from 'components/work_templates';
|
||||||
import {haveICurrentTeamPermission} from 'mattermost-redux/selectors/entities/roles';
|
import {haveICurrentTeamPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||||
import {Permissions} from 'mattermost-redux/constants';
|
import {Permissions} from 'mattermost-redux/constants';
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ exports[`components/GenericModal should match snapshot for base case 1`] = `
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="GenericModal__body"
|
className="GenericModal__body padding"
|
||||||
/>
|
/>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,11 +126,12 @@ exports[`components/GenericModal should match snapshot with both buttons 1`] = `
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="GenericModal__body"
|
className="GenericModal__body padding"
|
||||||
/>
|
/>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter
|
<ModalFooter
|
||||||
bsClass="modal-footer"
|
bsClass="modal-footer"
|
||||||
|
className=""
|
||||||
componentClass="div"
|
componentClass="div"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -218,11 +219,12 @@ exports[`components/GenericModal should match snapshot with disabled confirm but
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="GenericModal__body"
|
className="GenericModal__body padding"
|
||||||
/>
|
/>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter
|
<ModalFooter
|
||||||
bsClass="modal-footer"
|
bsClass="modal-footer"
|
||||||
|
className=""
|
||||||
componentClass="div"
|
componentClass="div"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ import {Locations, Constants, ModalIdentifiers} from 'utils/constants';
|
|||||||
import Permissions from 'mattermost-redux/constants/permissions';
|
import Permissions from 'mattermost-redux/constants/permissions';
|
||||||
import {ActionsTutorialTip} from 'components/actions_menu/actions_menu_tutorial_tip';
|
import {ActionsTutorialTip} from 'components/actions_menu/actions_menu_tutorial_tip';
|
||||||
import {ModalData} from 'types/actions';
|
import {ModalData} from 'types/actions';
|
||||||
import MarketplaceModal from 'components/plugin_marketplace';
|
import MarketplaceModal, {OpenedFromType} from 'components/plugin_marketplace/marketplace_modal';
|
||||||
import {OpenedFromType} from 'components/plugin_marketplace/marketplace_modal';
|
|
||||||
import OverlayTrigger from 'components/overlay_trigger';
|
import OverlayTrigger from 'components/overlay_trigger';
|
||||||
import * as PostUtils from 'utils/post_utils';
|
import * as PostUtils from 'utils/post_utils';
|
||||||
import * as Utils from 'utils/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 {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 OverlayTrigger from 'components/overlay_trigger';
|
||||||
|
|
||||||
import {Constants, ModalIdentifiers} from 'utils/constants';
|
import {Constants, ModalIdentifiers} from 'utils/constants';
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {uniqWith} from 'lodash';
|
|||||||
import {Post} from '@mattermost/types/posts';
|
import {Post} from '@mattermost/types/posts';
|
||||||
import {MarketplacePlugin} from '@mattermost/types/marketplace';
|
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 Markdown from 'components/markdown';
|
||||||
|
|
||||||
import {getUsers} from 'mattermost-redux/selectors/entities/users';
|
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
|
<GenericModal
|
||||||
autoCloseOnCancelButton={true}
|
autoCloseOnCancelButton={true}
|
||||||
autoCloseOnConfirmButton={true}
|
autoCloseOnConfirmButton={true}
|
||||||
|
bodyPadding={true}
|
||||||
compassDesign={true}
|
compassDesign={true}
|
||||||
confirmButtonText="Yes, delete"
|
confirmButtonText="Yes, delete"
|
||||||
enforceFocus={true}
|
enforceFocus={true}
|
||||||
@@ -45,6 +46,7 @@ exports[`components/drafts/draft_actions/delete_draft_modal should have called o
|
|||||||
<GenericModal
|
<GenericModal
|
||||||
autoCloseOnCancelButton={true}
|
autoCloseOnCancelButton={true}
|
||||||
autoCloseOnConfirmButton={true}
|
autoCloseOnConfirmButton={true}
|
||||||
|
bodyPadding={true}
|
||||||
compassDesign={true}
|
compassDesign={true}
|
||||||
confirmButtonText="Yes, delete"
|
confirmButtonText="Yes, delete"
|
||||||
enforceFocus={true}
|
enforceFocus={true}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ exports[`components/drafts/draft_actions/send_draft_modal should have called onC
|
|||||||
<GenericModal
|
<GenericModal
|
||||||
autoCloseOnCancelButton={true}
|
autoCloseOnCancelButton={true}
|
||||||
autoCloseOnConfirmButton={true}
|
autoCloseOnConfirmButton={true}
|
||||||
|
bodyPadding={true}
|
||||||
compassDesign={true}
|
compassDesign={true}
|
||||||
confirmButtonText="Yes, send now"
|
confirmButtonText="Yes, send now"
|
||||||
enforceFocus={true}
|
enforceFocus={true}
|
||||||
@@ -44,6 +45,7 @@ exports[`components/drafts/draft_actions/send_draft_modal should have called onE
|
|||||||
<GenericModal
|
<GenericModal
|
||||||
autoCloseOnCancelButton={true}
|
autoCloseOnCancelButton={true}
|
||||||
autoCloseOnConfirmButton={true}
|
autoCloseOnConfirmButton={true}
|
||||||
|
bodyPadding={true}
|
||||||
compassDesign={true}
|
compassDesign={true}
|
||||||
confirmButtonText="Yes, send now"
|
confirmButtonText="Yes, send now"
|
||||||
enforceFocus={true}
|
enforceFocus={true}
|
||||||
|
|||||||
@@ -104,6 +104,10 @@
|
|||||||
padding: 12px 24px 24px 24px;
|
padding: 12px 24px 24px 24px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&.divider {
|
||||||
|
border-top: 1px solid rgba(63, 67, 80, 0.08);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.GenericModal__compassDesign {
|
&.GenericModal__compassDesign {
|
||||||
@@ -112,10 +116,11 @@
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
|
flex-direction: column;
|
||||||
padding: 26px 32px 7px;
|
padding: 26px 32px 7px;
|
||||||
|
|
||||||
.close {
|
.close {
|
||||||
top: 20px;
|
top: 22px;
|
||||||
right: 18px;
|
right: 18px;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
@@ -133,7 +138,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.GenericModal__header {
|
.GenericModal__header {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
margin-right: 32px;
|
||||||
border-top-left-radius: 12px;
|
border-top-left-radius: 12px;
|
||||||
border-top-right-radius: 12px;
|
border-top-right-radius: 12px;
|
||||||
|
|
||||||
@@ -145,10 +156,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
padding: 15px 32px 0;
|
|
||||||
|
|
||||||
.GenericModal__body {
|
.GenericModal__body {
|
||||||
padding: 0;
|
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
|
// find them there, so we've had to redefine them here
|
||||||
t('generic_modal.cancel');
|
t('generic_modal.cancel');
|
||||||
t('generic_modal.confirm');
|
t('generic_modal.confirm');
|
||||||
|
t('footer_pagination.count');
|
||||||
|
t('footer_pagination.prev');
|
||||||
|
t('footer_pagination.next');
|
||||||
|
|
||||||
export default GenericModal;
|
export default GenericModal;
|
||||||
|
|||||||
@@ -152,14 +152,7 @@ exports[`components/global/product_switcher_menu should match snapshot with id 1
|
|||||||
"openedFrom": "product_menu",
|
"openedFrom": "product_menu",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dialogType={
|
dialogType={[Function]}
|
||||||
Object {
|
|
||||||
"$$typeof": Symbol(react.memo),
|
|
||||||
"WrappedComponent": [Function],
|
|
||||||
"compare": null,
|
|
||||||
"type": [Function],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
icon={
|
icon={
|
||||||
<Icon
|
<Icon
|
||||||
glyph="view-grid-plus-outline"
|
glyph="view-grid-plus-outline"
|
||||||
@@ -360,14 +353,7 @@ exports[`components/global/product_switcher_menu should match snapshot with most
|
|||||||
"openedFrom": "product_menu",
|
"openedFrom": "product_menu",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dialogType={
|
dialogType={[Function]}
|
||||||
Object {
|
|
||||||
"$$typeof": Symbol(react.memo),
|
|
||||||
"WrappedComponent": [Function],
|
|
||||||
"compare": null,
|
|
||||||
"type": [Function],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
icon={
|
icon={
|
||||||
<Icon
|
<Icon
|
||||||
glyph="view-grid-plus-outline"
|
glyph="view-grid-plus-outline"
|
||||||
@@ -688,14 +674,7 @@ exports[`components/global/product_switcher_menu should show integrations should
|
|||||||
"openedFrom": "product_menu",
|
"openedFrom": "product_menu",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dialogType={
|
dialogType={[Function]}
|
||||||
Object {
|
|
||||||
"$$typeof": Symbol(react.memo),
|
|
||||||
"WrappedComponent": [Function],
|
|
||||||
"compare": null,
|
|
||||||
"type": [Function],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
icon={
|
icon={
|
||||||
<Icon
|
<Icon
|
||||||
glyph="view-grid-plus-outline"
|
glyph="view-grid-plus-outline"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {Permissions} from 'mattermost-redux/constants';
|
|||||||
import AboutBuildModal from 'components/about_build_modal';
|
import AboutBuildModal from 'components/about_build_modal';
|
||||||
import SystemPermissionGate from 'components/permissions_gates/system_permission_gate';
|
import SystemPermissionGate from 'components/permissions_gates/system_permission_gate';
|
||||||
import TeamPermissionGate from 'components/permissions_gates/team_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 Menu from 'components/widgets/menu/menu';
|
||||||
import RestrictedIndicator from 'components/widgets/menu/menu_items/restricted_indicator';
|
import RestrictedIndicator from 'components/widgets/menu/menu_items/restricted_indicator';
|
||||||
import {VisitSystemConsoleTour} from 'components/onboarding_tasks';
|
import {VisitSystemConsoleTour} from 'components/onboarding_tasks';
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ exports[`components/new_channel_modal should match snapshot 1`] = `
|
|||||||
<GenericModal
|
<GenericModal
|
||||||
autoCloseOnCancelButton={true}
|
autoCloseOnCancelButton={true}
|
||||||
autoCloseOnConfirmButton={false}
|
autoCloseOnConfirmButton={false}
|
||||||
|
bodyPadding={true}
|
||||||
cancelButtonText="Cancel"
|
cancelButtonText="Cancel"
|
||||||
className="new-channel-modal"
|
className="new-channel-modal"
|
||||||
compassDesign={true}
|
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={
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="app-install always-show-enabled"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -50,7 +50,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render installed a
|
|||||||
}
|
}
|
||||||
button={
|
button={
|
||||||
<button
|
<button
|
||||||
className="btn btn-outline"
|
className="app-installed"
|
||||||
disabled={true}
|
disabled={true}
|
||||||
>
|
>
|
||||||
<Memo(MemoizedFormattedMessage)
|
<Memo(MemoizedFormattedMessage)
|
||||||
@@ -83,7 +83,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render with empty
|
|||||||
}
|
}
|
||||||
button={
|
button={
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="app-install always-show-enabled"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -123,7 +123,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render with icon 1
|
|||||||
}
|
}
|
||||||
button={
|
button={
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="app-install always-show-enabled"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -164,7 +164,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render with no hom
|
|||||||
}
|
}
|
||||||
button={
|
button={
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="app-install always-show-enabled"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -202,7 +202,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render with no plu
|
|||||||
}
|
}
|
||||||
button={
|
button={
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="app-install always-show-enabled"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -240,7 +240,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render with one la
|
|||||||
}
|
}
|
||||||
button={
|
button={
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="app-install always-show-enabled"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -288,7 +288,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render with server
|
|||||||
}
|
}
|
||||||
button={
|
button={
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="app-install always-show-enabled"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -328,7 +328,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render with two la
|
|||||||
}
|
}
|
||||||
button={
|
button={
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="app-install always-show-enabled"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -381,7 +381,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem when installing 1`] = `
|
|||||||
}
|
}
|
||||||
button={
|
button={
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="app-install always-show-enabled"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export default class MarketplaceItemApp extends React.PureComponent <Marketplace
|
|||||||
if (this.props.installed && !this.props.installing && !this.props.error) {
|
if (this.props.installed && !this.props.installing && !this.props.error) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className='btn btn-outline'
|
className='app-installed'
|
||||||
disabled={true}
|
disabled={true}
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
@@ -81,7 +81,7 @@ export default class MarketplaceItemApp extends React.PureComponent <Marketplace
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={this.onInstall}
|
onClick={this.onInstall}
|
||||||
className='btn btn-primary'
|
className='app-install always-show-enabled'
|
||||||
disabled={this.props.installing}
|
disabled={this.props.installing}
|
||||||
>
|
>
|
||||||
<LoadingWrapper
|
<LoadingWrapper
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render 1`] = `
|
|||||||
}
|
}
|
||||||
button={
|
button={
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="plugin-install always-show-enabled"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -78,7 +78,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render installe
|
|||||||
to="/admin_console/plugins/plugin_id"
|
to="/admin_console/plugins/plugin_id"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="btn btn-outline"
|
className="plugin-configure"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<Memo(MemoizedFormattedMessage)
|
<Memo(MemoizedFormattedMessage)
|
||||||
@@ -137,7 +137,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with emp
|
|||||||
}
|
}
|
||||||
button={
|
button={
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="plugin-install always-show-enabled"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -202,7 +202,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with no
|
|||||||
}
|
}
|
||||||
button={
|
button={
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="plugin-install always-show-enabled"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -265,7 +265,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with no
|
|||||||
}
|
}
|
||||||
button={
|
button={
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="plugin-install always-show-enabled"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -328,7 +328,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with no
|
|||||||
}
|
}
|
||||||
button={
|
button={
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="plugin-install always-show-enabled"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -390,7 +390,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with one
|
|||||||
}
|
}
|
||||||
button={
|
button={
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="plugin-install always-show-enabled"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -463,7 +463,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with plu
|
|||||||
}
|
}
|
||||||
button={
|
button={
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="plugin-install always-show-enabled"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -540,7 +540,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with ser
|
|||||||
}
|
}
|
||||||
button={
|
button={
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="plugin-install always-show-enabled"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -605,7 +605,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with two
|
|||||||
}
|
}
|
||||||
button={
|
button={
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="plugin-install always-show-enabled"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
@@ -686,7 +686,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with upd
|
|||||||
to="/admin_console/plugins/plugin_id"
|
to="/admin_console/plugins/plugin_id"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="btn btn-outline"
|
className="plugin-configure"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<Memo(MemoizedFormattedMessage)
|
<Memo(MemoizedFormattedMessage)
|
||||||
@@ -751,7 +751,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with upd
|
|||||||
to="/admin_console/plugins/plugin_id"
|
to="/admin_console/plugins/plugin_id"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="btn btn-outline"
|
className="plugin-configure"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<Memo(MemoizedFormattedMessage)
|
<Memo(MemoizedFormattedMessage)
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ export default class MarketplaceItemPlugin extends React.PureComponent <Marketpl
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={this.onConfigure}
|
onClick={this.onConfigure}
|
||||||
className='btn btn-outline'
|
className='plugin-configure'
|
||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='marketplace_modal.list.configure'
|
id='marketplace_modal.list.configure'
|
||||||
@@ -325,7 +325,7 @@ export default class MarketplaceItemPlugin extends React.PureComponent <Marketpl
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={this.onInstall}
|
onClick={this.onInstall}
|
||||||
className='btn btn-primary'
|
className='plugin-install always-show-enabled'
|
||||||
disabled={this.props.installing}
|
disabled={this.props.installing}
|
||||||
>
|
>
|
||||||
<LoadingWrapper
|
<LoadingWrapper
|
||||||
|
|||||||
@@ -1,150 +1,14 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// 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
|
<div
|
||||||
className="more-modal__list"
|
className="no_plugins"
|
||||||
>
|
>
|
||||||
<Connect(MarketplaceItemPlugin)
|
<PluginIcon
|
||||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
className="icon__plugin"
|
||||||
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)
|
<div
|
||||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
className="no_plugins__message"
|
||||||
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>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ import {AuthorType, MarketplacePlugin, ReleaseStage} from '@mattermost/types/mar
|
|||||||
|
|
||||||
import MarketplaceItem from '../marketplace_item/marketplace_item_plugin';
|
import MarketplaceItem from '../marketplace_item/marketplace_item_plugin';
|
||||||
|
|
||||||
import MarketplaceList from './marketplace_list';
|
import MarketplaceList, {ITEMS_PER_PAGE} from './marketplace_list';
|
||||||
import NavigationRow from './navigation_row';
|
|
||||||
|
|
||||||
describe('components/marketplace/marketplace_list', () => {
|
describe('components/marketplace/marketplace_list', () => {
|
||||||
const samplePlugin: MarketplacePlugin = {
|
const samplePlugin: MarketplacePlugin = {
|
||||||
@@ -28,8 +27,20 @@ describe('components/marketplace/marketplace_list', () => {
|
|||||||
installed_version: '',
|
installed_version: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should render with multiple plugins', () => {
|
it('should render default', () => {
|
||||||
const wrapper = shallow<MarketplaceList>(
|
const wrapper = shallow(
|
||||||
|
<MarketplaceList
|
||||||
|
listing={[]}
|
||||||
|
page={0}
|
||||||
|
noResultsMessage=''
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render page with ITEMS_PER_PAGE plugins', () => {
|
||||||
|
const wrapper = shallow(
|
||||||
<MarketplaceList
|
<MarketplaceList
|
||||||
listing={[
|
listing={[
|
||||||
samplePlugin, samplePlugin, samplePlugin, samplePlugin, samplePlugin,
|
samplePlugin, samplePlugin, samplePlugin, samplePlugin, samplePlugin,
|
||||||
@@ -37,28 +48,29 @@ describe('components/marketplace/marketplace_list', () => {
|
|||||||
samplePlugin, samplePlugin, samplePlugin, samplePlugin, samplePlugin,
|
samplePlugin, samplePlugin, samplePlugin, samplePlugin, samplePlugin,
|
||||||
samplePlugin, samplePlugin,
|
samplePlugin, samplePlugin,
|
||||||
]}
|
]}
|
||||||
|
page={0}
|
||||||
|
noResultsMessage=''
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
|
|
||||||
expect(wrapper.state().page).toEqual(0);
|
expect(wrapper.find(MarketplaceItem)).toHaveLength(ITEMS_PER_PAGE);
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set page to 0 when list of plugins changed', () => {
|
it('should render no results', () => {
|
||||||
const wrapper = shallow<MarketplaceList>(
|
const wrapper = shallow(
|
||||||
<MarketplaceList
|
<MarketplaceList
|
||||||
listing={[samplePlugin, samplePlugin]}
|
listing={[]}
|
||||||
|
page={0}
|
||||||
|
noResultsMessage='No plugins available'
|
||||||
|
noResultsAction={{
|
||||||
|
label: 'action',
|
||||||
|
onClick: jest.fn(),
|
||||||
|
}}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
wrapper.setState({page: 10});
|
expect(wrapper.find('.icon__plugin').length).toEqual(1);
|
||||||
wrapper.setProps({listing: [samplePlugin]});
|
expect(wrapper.find('.no_plugins__message').length).toEqual(1);
|
||||||
|
expect(wrapper.find('.no_plugins__action').length).toEqual(1);
|
||||||
expect(wrapper.state().page).toEqual(0);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,107 +1,116 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
import React, {useCallback, useMemo} from 'react';
|
||||||
|
import {useIntl} from 'react-intl';
|
||||||
|
|
||||||
import type {MarketplaceApp, MarketplacePlugin} from '@mattermost/types/marketplace';
|
import type {MarketplaceApp, MarketplacePlugin} from '@mattermost/types/marketplace';
|
||||||
import {isPlugin, getName} from 'mattermost-redux/utils/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 MarketplaceItemPlugin from '../marketplace_item/marketplace_item_plugin';
|
||||||
import MarketplaceItemApp from '../marketplace_item/marketplace_item_app';
|
import MarketplaceItemApp from '../marketplace_item/marketplace_item_app';
|
||||||
|
|
||||||
import NavigationRow from './navigation_row';
|
export const ITEMS_PER_PAGE = 15;
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 15;
|
|
||||||
|
|
||||||
type MarketplaceListProps = {
|
type MarketplaceListProps = {
|
||||||
listing: Array<MarketplacePlugin | MarketplaceApp>;
|
listing: Array<MarketplacePlugin | MarketplaceApp>;
|
||||||
};
|
|
||||||
|
|
||||||
type MarketplaceListState = {
|
|
||||||
page: number;
|
page: number;
|
||||||
|
noResultsMessage: string;
|
||||||
|
noResultsAction?: {
|
||||||
|
label: string;
|
||||||
|
onClick: () => void;
|
||||||
|
};
|
||||||
|
filter?: string;
|
||||||
|
listRef?: React.RefObject<HTMLDivElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class MarketplaceList extends React.PureComponent <MarketplaceListProps, MarketplaceListState> {
|
const MarketplaceList = ({
|
||||||
static getDerivedStateFromProps(props: MarketplaceListProps, state: MarketplaceListState): MarketplaceListState | null {
|
listing,
|
||||||
if (state.page > 0 && props.listing.length < ITEMS_PER_PAGE) {
|
page,
|
||||||
return {page: 0};
|
noResultsMessage,
|
||||||
|
noResultsAction,
|
||||||
|
filter,
|
||||||
|
listRef,
|
||||||
|
}: MarketplaceListProps) => {
|
||||||
|
const {formatMessage} = useIntl();
|
||||||
|
|
||||||
|
const pageItems = useMemo(() => {
|
||||||
|
if (listing.length === 0) {
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
const pageStart = page * ITEMS_PER_PAGE;
|
||||||
}
|
|
||||||
|
|
||||||
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 pageEnd = pageStart + ITEMS_PER_PAGE;
|
const pageEnd = pageStart + ITEMS_PER_PAGE;
|
||||||
|
|
||||||
this.props.listing.sort((a, b) => {
|
return [...listing].
|
||||||
return getName(a).localeCompare(getName(b));
|
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 (
|
return (listing.length === 0 ? (
|
||||||
<div className='more-modal__list'>
|
<div className='no_plugins'>
|
||||||
{itemsToDisplay.map((i) => {
|
<PluginIcon className='icon__plugin'/>
|
||||||
if (isPlugin(i)) {
|
<div className='no_plugins__message'>
|
||||||
return (
|
{getNoResultsMessage()}
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
</div>
|
</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/variables';
|
||||||
|
@import 'utils/mixins';
|
||||||
|
|
||||||
.modal-marketplace {
|
.marketplace-modal {
|
||||||
display: flex;
|
width: 800px;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.navigation-row {
|
div.navigation-row {
|
||||||
|
overflow: unset;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
|
||||||
div {
|
div {
|
||||||
@@ -29,7 +19,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
div.count {
|
div.count {
|
||||||
padding-top: 8px;
|
padding: 9px 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,33 +35,47 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs {
|
.nav-tabs {
|
||||||
font-size: 14px;
|
padding: 0 32px;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
|
||||||
> li {
|
li {
|
||||||
> a {
|
margin-right: 0;
|
||||||
padding: 10px 16px;
|
|
||||||
|
a {
|
||||||
|
padding: 13px 12px;
|
||||||
|
border: none;
|
||||||
background: transparent;
|
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;
|
transition: all 0.15s ease;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:active,
|
&:active,
|
||||||
&:focus {
|
&:focus,
|
||||||
|
&:focus-within {
|
||||||
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
border-radius: none;
|
||||||
color: var(--center-channel-color);
|
color: var(--center-channel-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active > a {
|
&.active {
|
||||||
color: var(--center-channel-color);
|
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 {
|
h2 {
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
|
|
||||||
@@ -81,8 +85,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.more-modal__list {
|
.more-modal__list {
|
||||||
|
height: 390px;
|
||||||
|
margin: 0 6px 8px 12px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
.more-modal__row {
|
.more-modal__row {
|
||||||
align-items: normal;
|
min-height: 80px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: none;
|
||||||
|
|
||||||
.marketplace__tag {
|
.marketplace__tag {
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
@@ -94,25 +104,91 @@
|
|||||||
margin: 10px 10px 0 0;
|
margin: 10px 10px 0 0;
|
||||||
font-size: 0.9em;
|
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 {
|
.icon__plugin {
|
||||||
margin: 2px 0 0;
|
display: flex;
|
||||||
font-size: 0.9em;
|
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 {
|
.marketplace-modal-search {
|
||||||
width: 720px;
|
padding: 24px 0 0;
|
||||||
height: 40px;
|
|
||||||
flex: 1;
|
.search_input {
|
||||||
margin-top: 28px;
|
width: 100%;
|
||||||
margin-right: 16px;
|
border: 0 !important;
|
||||||
margin-bottom: 20px;
|
border-radius: 0 !important;
|
||||||
margin-left: 16px;
|
box-shadow: none;
|
||||||
box-shadow: none;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
@@ -130,8 +206,9 @@
|
|||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 720px;
|
width: 100%;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
.subtitle {
|
||||||
@@ -160,67 +237,41 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon__plugin {
|
.no_plugins {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 42px;
|
height: 390px;
|
||||||
flex: 0 0 42px;
|
flex-flow: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-right: 4px;
|
margin-bottom: 8px;
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
svg {
|
&__message {
|
||||||
width: 32px;
|
margin-top: 20px;
|
||||||
height: 32px;
|
color: var(--center-channel-color);
|
||||||
fill: var(--button-bg);
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 28px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.icon__plugin--background {
|
&__action {
|
||||||
padding: 6px;
|
@include primary-button;
|
||||||
background-color: $white;
|
@include button-medium;
|
||||||
|
|
||||||
svg {
|
margin-top: 30px;
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.no_plugins_div {
|
.icon__plugin {
|
||||||
text-align: center;
|
svg {
|
||||||
|
fill: var(--button-bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.item_error {
|
.item_error {
|
||||||
background-color: rgba(var(--error-text-rgb), 0.08);
|
background-color: rgba(var(--error-text-color-rgb), 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.error_text {
|
.loading {
|
||||||
color: var(--error-text);
|
height: 390px;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,19 +5,21 @@ import React from 'react';
|
|||||||
import {shallow} from 'enzyme';
|
import {shallow} from 'enzyme';
|
||||||
|
|
||||||
import {AuthorType, MarketplacePlugin, ReleaseStage} from '@mattermost/types/marketplace';
|
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', () => {
|
import MarketplaceModal, {OpenedFromType} from './marketplace_modal';
|
||||||
const original = jest.requireActual('actions/telemetry_actions.jsx');
|
|
||||||
return {
|
let mockState: GlobalState;
|
||||||
...original,
|
|
||||||
trackEvent: jest.fn(),
|
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/', () => {
|
describe('components/marketplace/', () => {
|
||||||
const samplePlugin: MarketplacePlugin = {
|
const samplePlugin: MarketplacePlugin = {
|
||||||
@@ -52,161 +54,106 @@ describe('components/marketplace/', () => {
|
|||||||
installed_version: '1.0.3',
|
installed_version: '1.0.3',
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('AllListing', () => {
|
const defaultProps = {
|
||||||
it('should render with no plugins', () => {
|
openedFrom: 'actions_menu' as OpenedFromType,
|
||||||
const wrapper = shallow(
|
};
|
||||||
<AllListing listing={[]}/>,
|
|
||||||
);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render with one plugin', () => {
|
beforeEach(() => {
|
||||||
const wrapper = shallow(
|
mockState = {
|
||||||
<AllListing listing={[samplePlugin]}/>,
|
views: {
|
||||||
);
|
modals: {
|
||||||
expect(wrapper).toMatchSnapshot();
|
modalState: {
|
||||||
});
|
[ModalIdentifiers.PLUGIN_MARKETPLACE]: {
|
||||||
|
open: true,
|
||||||
it('should render with plugins', () => {
|
},
|
||||||
const wrapper = shallow(
|
},
|
||||||
<AllListing listing={[samplePlugin, sampleInstalledPlugin]}/>,
|
},
|
||||||
);
|
marketplace: {
|
||||||
expect(wrapper).toMatchSnapshot();
|
plugins: [],
|
||||||
});
|
apps: [],
|
||||||
});
|
},
|
||||||
|
|
||||||
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(),
|
|
||||||
},
|
},
|
||||||
};
|
entities: {
|
||||||
|
general: {
|
||||||
|
firstAdminCompleteSetup: false,
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
pluginStatuses: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as GlobalState;
|
||||||
|
});
|
||||||
|
|
||||||
test('should render with no plugins installed', () => {
|
test('should render default', () => {
|
||||||
const wrapper = shallow<MarketplaceModal>(
|
const wrapper = shallow(
|
||||||
<MarketplaceModal {...baseProps}/>,
|
<MarketplaceModal {...defaultProps}/>,
|
||||||
);
|
);
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render with plugins installed', () => {
|
expect(wrapper.shallow()).toMatchSnapshot();
|
||||||
const props = {
|
});
|
||||||
...baseProps,
|
|
||||||
plugins: [
|
|
||||||
...baseProps.listing,
|
|
||||||
sampleInstalledPlugin,
|
|
||||||
],
|
|
||||||
installedListing: [
|
|
||||||
sampleInstalledPlugin,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = shallow<MarketplaceModal>(
|
test('should render with no plugins available', () => {
|
||||||
<MarketplaceModal {...props}/>,
|
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', () => {
|
wrapper.update();
|
||||||
const fetchListing = baseProps.actions.fetchListing;
|
|
||||||
const wrapper = shallow<MarketplaceModal>(<MarketplaceModal {...baseProps}/>);
|
|
||||||
|
|
||||||
expect(fetchListing).toBeCalledTimes(1);
|
expect(wrapper.shallow()).toMatchSnapshot();
|
||||||
wrapper.setProps({...baseProps});
|
});
|
||||||
expect(fetchListing).toBeCalledTimes(1);
|
|
||||||
|
|
||||||
const status = {
|
test('should render with plugins available', () => {
|
||||||
id: 'test',
|
const setState = jest.fn();
|
||||||
} as PluginStatusRedux;
|
const useStateSpy = jest.spyOn(React, 'useState');
|
||||||
wrapper.setProps({...baseProps, pluginStatuses: {test: status}});
|
useStateSpy.mockImplementationOnce(() => [false, setState]);
|
||||||
expect(fetchListing).toBeCalledTimes(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should render with error banner', () => {
|
mockState.views.marketplace.plugins = [
|
||||||
const wrapper = shallow<MarketplaceModal>(
|
samplePlugin,
|
||||||
<MarketplaceModal {...baseProps}/>,
|
];
|
||||||
);
|
|
||||||
|
|
||||||
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', () => {
|
expect(wrapper.shallow()).toMatchSnapshot();
|
||||||
const wrapper = shallow<MarketplaceModal>(
|
});
|
||||||
<MarketplaceModal {...baseProps}/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
wrapper.setState({filter: 'nps'});
|
test('should render with plugins installed', () => {
|
||||||
wrapper.instance().doSearch();
|
const setState = jest.fn();
|
||||||
|
const useStateSpy = jest.spyOn(React, 'useState');
|
||||||
|
useStateSpy.mockImplementationOnce(() => [false, setState]);
|
||||||
|
|
||||||
expect(trackEvent).toHaveBeenCalledWith('plugins', 'ui_marketplace_opened', {from: 'actions_menu'});
|
mockState.views.marketplace.plugins = [
|
||||||
expect(trackEvent).toHaveBeenCalledWith('plugins', 'ui_marketplace_search', {filter: 'nps'});
|
samplePlugin,
|
||||||
});
|
sampleInstalledPlugin,
|
||||||
|
];
|
||||||
|
|
||||||
test('Should call for opened track event on mount', () => {
|
const wrapper = shallow(
|
||||||
const openedFrom = 'actions_menu';
|
<MarketplaceModal {...defaultProps}/>,
|
||||||
|
);
|
||||||
|
|
||||||
shallow<MarketplaceModal>(
|
wrapper.update();
|
||||||
<MarketplaceModal
|
|
||||||
{...baseProps}
|
|
||||||
openedFrom={openedFrom}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
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.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||||
import {FormattedMessage} from 'react-intl';
|
|
||||||
import debounce from 'lodash/debounce';
|
|
||||||
import {Tabs, Tab, SelectCallback} from 'react-bootstrap';
|
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 {MagnifyIcon} from '@mattermost/compass-icons/components';
|
||||||
import type {MarketplaceApp, MarketplacePlugin} from '@mattermost/types/marketplace';
|
|
||||||
|
|
||||||
import FullScreenModal from 'components/widgets/modals/full_screen_modal';
|
import {FooterPagination} from '@mattermost/components';
|
||||||
import RootPortal from 'components/root_portal';
|
import {getPluginStatuses} from 'mattermost-redux/actions/admin';
|
||||||
import QuickInput from 'components/quick_input';
|
import {setFirstAdminVisitMarketplaceStatus} from 'mattermost-redux/actions/general';
|
||||||
import LocalizedInput from 'components/localized_input/localized_input';
|
import {getFirstAdminVisitMarketplaceStatus} from 'mattermost-redux/selectors/entities/general';
|
||||||
import PluginIcon from 'components/widgets/icons/plugin_icon';
|
import {ActionResult} from 'mattermost-redux/types/actions';
|
||||||
import LoadingScreen from 'components/loading_screen';
|
|
||||||
import FormattedMarkdownMessage from 'components/formatted_markdown_message';
|
|
||||||
|
|
||||||
|
import {fetchListing, filterListing} from 'actions/marketplace';
|
||||||
import {trackEvent} from 'actions/telemetry_actions.jsx';
|
import {trackEvent} from 'actions/telemetry_actions.jsx';
|
||||||
import {t} from 'utils/i18n';
|
import {closeModal} from 'actions/views/modals';
|
||||||
import {localizeMessage} from 'utils/utils';
|
|
||||||
|
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 './marketplace_modal.scss';
|
||||||
import MarketplaceList from './marketplace_list/marketplace_list';
|
|
||||||
|
import MarketplaceList, {ITEMS_PER_PAGE} from './marketplace_list/marketplace_list';
|
||||||
|
|
||||||
const MarketplaceTabs = {
|
const MarketplaceTabs = {
|
||||||
ALL_LISTING: 'allListing',
|
ALL_LISTING: 'all',
|
||||||
INSTALLED_LISTING: 'installed',
|
INSTALLED_LISTING: 'installed',
|
||||||
};
|
};
|
||||||
|
|
||||||
const SEARCH_TIMEOUT_MILLISECONDS = 200;
|
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';
|
export type OpenedFromType = 'actions_menu' | 'app_bar' | 'channel_header' | 'command' | 'open_plugin_install_post' | 'product_menu';
|
||||||
|
|
||||||
type AllListingProps = {
|
type MarketplaceModalProps = {
|
||||||
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;
|
|
||||||
openedFrom: OpenedFromType;
|
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
|
<GenericModal
|
||||||
autoCloseOnCancelButton={false}
|
autoCloseOnCancelButton={false}
|
||||||
autoCloseOnConfirmButton={true}
|
autoCloseOnConfirmButton={true}
|
||||||
|
bodyPadding={true}
|
||||||
cancelButtonText={null}
|
cancelButtonText={null}
|
||||||
className="productNotices"
|
className="productNotices"
|
||||||
confirmButtonText={
|
confirmButtonText={
|
||||||
@@ -44,6 +45,7 @@ exports[`ProductNoticesModal Match snapshot for user notice 1`] = `
|
|||||||
<GenericModal
|
<GenericModal
|
||||||
autoCloseOnCancelButton={false}
|
autoCloseOnCancelButton={false}
|
||||||
autoCloseOnConfirmButton={true}
|
autoCloseOnConfirmButton={true}
|
||||||
|
bodyPadding={true}
|
||||||
cancelButtonText={
|
cancelButtonText={
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<PreviousIcon
|
<PreviousIcon
|
||||||
@@ -122,6 +124,7 @@ exports[`ProductNoticesModal Should match snapshot for system admin notice 1`] =
|
|||||||
<GenericModal
|
<GenericModal
|
||||||
autoCloseOnCancelButton={false}
|
autoCloseOnCancelButton={false}
|
||||||
autoCloseOnConfirmButton={false}
|
autoCloseOnConfirmButton={false}
|
||||||
|
bodyPadding={true}
|
||||||
cancelButtonText={null}
|
cancelButtonText={null}
|
||||||
className="productNotices"
|
className="productNotices"
|
||||||
confirmButtonText={
|
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 {
|
.Input_wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
margin: 2px 0;
|
margin: 2px 0;
|
||||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
|
|
||||||
|
> :not(:first-child) {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.Input_limit-exceeded {
|
.Input_limit-exceeded {
|
||||||
@@ -196,4 +201,10 @@
|
|||||||
background-color: rgba(var(--center-channel-color-rgb), 0.04);
|
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 React, {useState, useEffect} from 'react';
|
||||||
import {useIntl} from 'react-intl';
|
import {useIntl} from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
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';
|
import './input.scss';
|
||||||
|
|
||||||
@@ -32,6 +36,9 @@ interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|||||||
useLegend?: boolean;
|
useLegend?: boolean;
|
||||||
customMessage?: CustomMessageInputType;
|
customMessage?: CustomMessageInputType;
|
||||||
inputSize?: SIZE;
|
inputSize?: SIZE;
|
||||||
|
clearable?: boolean;
|
||||||
|
clearableTooltipText?: string;
|
||||||
|
onClear?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Input = React.forwardRef((
|
const Input = React.forwardRef((
|
||||||
@@ -56,9 +63,12 @@ const Input = React.forwardRef((
|
|||||||
maxLength,
|
maxLength,
|
||||||
inputSize = SIZE.MEDIUM,
|
inputSize = SIZE.MEDIUM,
|
||||||
disabled,
|
disabled,
|
||||||
|
clearable,
|
||||||
|
clearableTooltipText,
|
||||||
onFocus,
|
onFocus,
|
||||||
onBlur,
|
onBlur,
|
||||||
onChange,
|
onChange,
|
||||||
|
onClear,
|
||||||
...otherProps
|
...otherProps
|
||||||
}: InputProps,
|
}: InputProps,
|
||||||
ref?: React.Ref<HTMLInputElement>,
|
ref?: React.Ref<HTMLInputElement>,
|
||||||
@@ -109,6 +119,12 @@ const Input = React.forwardRef((
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOnClear = () => {
|
||||||
|
if (onClear) {
|
||||||
|
onClear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const validateInput = () => {
|
const validateInput = () => {
|
||||||
if (!required || (value !== null && value !== '')) {
|
if (!required || (value !== null && value !== '')) {
|
||||||
return;
|
return;
|
||||||
@@ -121,6 +137,26 @@ const Input = React.forwardRef((
|
|||||||
const error = customInputLabel?.type === 'error';
|
const error = customInputLabel?.type === 'error';
|
||||||
const limitExceeded = limit && value && !Array.isArray(value) ? value.toString().length - limit : 0;
|
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 (
|
return (
|
||||||
<div className={classNames('Input_container', containerClassName, {disabled})}>
|
<div className={classNames('Input_container', containerClassName, {disabled})}>
|
||||||
<fieldset
|
<fieldset
|
||||||
@@ -157,6 +193,7 @@ const Input = React.forwardRef((
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{inputSuffix}
|
{inputSuffix}
|
||||||
|
{clearButton}
|
||||||
</div>
|
</div>
|
||||||
{addon}
|
{addon}
|
||||||
</fieldset>
|
</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.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_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.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": "Error: {error}",
|
||||||
"apps.error.command.field_missing": "Required fields missing: `{fieldName}`.",
|
"apps.error.command.field_missing": "Required fields missing: `{fieldName}`.",
|
||||||
"apps.error.command.same_channel": "Channel repeated for field `{fieldName}`: `{option}`.",
|
"apps.error.command.same_channel": "Channel repeated for field `{fieldName}`: `{option}`.",
|
||||||
@@ -3441,6 +3440,9 @@
|
|||||||
"filtered_user_list.userStatus": "User Status:",
|
"filtered_user_list.userStatus": "User Status:",
|
||||||
"flag_post.flag": "Save",
|
"flag_post.flag": "Save",
|
||||||
"flag_post.unflag": "Remove from Saved",
|
"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_button.label": "Forward",
|
||||||
"forward_post_modal.button.cancel": "Cancel",
|
"forward_post_modal.button.cancel": "Cancel",
|
||||||
"forward_post_modal.button.forward": "Forward",
|
"forward_post_modal.button.forward": "Forward",
|
||||||
@@ -4085,8 +4087,9 @@
|
|||||||
"mark_all_threads_as_read_modal.title": "Mark all your threads as read?",
|
"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.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_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_list.no_plugins_filter": "No results for \"{filter}\"",
|
||||||
"marketplace_modal.install_plugins": "Install Plugins",
|
"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.installing": "Installing...",
|
||||||
"marketplace_modal.list.configure": "Configure",
|
"marketplace_modal.list.configure": "Configure",
|
||||||
"marketplace_modal.list.configure.plugin": "Configure {plugin}",
|
"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": "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.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.list.update_confirmation.title": "Confirm Plugin Update",
|
||||||
"marketplace_modal.no_plugins": "There are no plugins available at this time.",
|
"marketplace_modal.no_plugins": "No plugins found",
|
||||||
"marketplace_modal.no_plugins_installed": "You do not have any plugins installed.",
|
"marketplace_modal.no_plugins_installed": "No plugins installed found",
|
||||||
"marketplace_modal.search": "Search Marketplace",
|
"marketplace_modal.search": "Search marketplace",
|
||||||
"marketplace_modal.tabs.all_listing": "All",
|
"marketplace_modal.tabs.all_listing": "All",
|
||||||
"marketplace_modal.tabs.installed_listing": "Installed",
|
"marketplace_modal.tabs.installed_listing": "Installed ({count})",
|
||||||
"marketplace_modal.title": "Marketplace",
|
"marketplace_modal.title": "App Marketplace",
|
||||||
"members_popover.button.message": "message",
|
"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.enterpriseTrialDescription": "Your trial is active until {trialEndDay}. Discover our top Enterprise features. <openModalLink>Learn more</openModalLink>",
|
||||||
"menu.cloudFree.enterpriseTrialTitle": "Enterprise Trial",
|
"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.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.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:",
|
"welcome_post_renderer.user_message.title": "Welcome to Mattermost! :rocket:",
|
||||||
|
"widget.input.clear": "Clear",
|
||||||
"widget.input.required": "This field is required",
|
"widget.input.required": "This field is required",
|
||||||
"widget.passwordInput.createPassword": "Choose a Password",
|
"widget.passwordInput.createPassword": "Choose a Password",
|
||||||
"widget.passwordInput.password": "Password",
|
"widget.passwordInput.password": "Password",
|
||||||
|
|||||||
@@ -114,9 +114,13 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
|
|
||||||
&::before {
|
&:first-child::before {
|
||||||
margin: 0 7px 0 0;
|
margin: 0 7px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:last-child::before {
|
||||||
|
margin: 0 0 0 7px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +150,7 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled:not(.always-show-enabled) {
|
||||||
background: rgba(var(--center-channel-color-rgb), 0.08);
|
background: rgba(var(--center-channel-color-rgb), 0.08);
|
||||||
color: rgba(var(--center-channel-color-rgb), 0.32);
|
color: rgba(var(--center-channel-color-rgb), 0.32);
|
||||||
cursor: not-allowed;
|
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;
|
enforceFocus?: boolean;
|
||||||
container?: React.ReactNode | React.ReactNodeArray;
|
container?: React.ReactNode | React.ReactNodeArray;
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
errorText?: string;
|
errorText?: string | React.ReactNode;
|
||||||
compassDesign?: boolean;
|
compassDesign?: boolean;
|
||||||
backdrop?: boolean;
|
backdrop?: boolean;
|
||||||
backdropClassName?: string;
|
backdropClassName?: string;
|
||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
keyboardEscape?: boolean;
|
keyboardEscape?: boolean;
|
||||||
|
headerInput?: React.ReactNode;
|
||||||
|
bodyPadding?: boolean;
|
||||||
|
footerContent?: React.ReactNode;
|
||||||
|
footerDivider?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
@@ -56,6 +60,7 @@ export class GenericModal extends React.PureComponent<Props, State> {
|
|||||||
autoCloseOnConfirmButton: true,
|
autoCloseOnConfirmButton: true,
|
||||||
enforceFocus: true,
|
enforceFocus: true,
|
||||||
keyboardEscape: true,
|
keyboardEscape: true,
|
||||||
|
bodyPadding: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
@@ -195,7 +200,12 @@ export class GenericModal extends React.PureComponent<Props, State> {
|
|||||||
className='GenericModal__wrapper-enter-key-press-catcher'
|
className='GenericModal__wrapper-enter-key-press-catcher'
|
||||||
>
|
>
|
||||||
<Modal.Header closeButton={true}>
|
<Modal.Header closeButton={true}>
|
||||||
{this.props.compassDesign && headerText}
|
{this.props.compassDesign && (
|
||||||
|
<>
|
||||||
|
{headerText}
|
||||||
|
{this.props.headerInput}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
{this.props.compassDesign ? (
|
{this.props.compassDesign ? (
|
||||||
@@ -208,14 +218,22 @@ export class GenericModal extends React.PureComponent<Props, State> {
|
|||||||
) : (
|
) : (
|
||||||
headerText
|
headerText
|
||||||
)}
|
)}
|
||||||
<div className='GenericModal__body'>
|
<div className={classNames('GenericModal__body', {padding: this.props.bodyPadding})}>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</div>
|
</div>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
{(cancelButton || confirmButton) && <Modal.Footer>
|
{(cancelButton || confirmButton || this.props.footerContent) && (
|
||||||
{cancelButton}
|
<Modal.Footer className={classNames({divider: this.props.footerDivider})}>
|
||||||
{confirmButton}
|
{(cancelButton || confirmButton) ? (
|
||||||
</Modal.Footer>}
|
<>
|
||||||
|
{cancelButton}
|
||||||
|
{confirmButton}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
this.props.footerContent
|
||||||
|
)}
|
||||||
|
</Modal.Footer>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</FocusTrap>
|
</FocusTrap>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export type {CircleSkeletonLoaderProps, RectangleSkeletonLoaderProps} from './sk
|
|||||||
export type {Props as FocusTrapProps} from './focus_trap';
|
export type {Props as FocusTrapProps} from './focus_trap';
|
||||||
|
|
||||||
// components
|
// components
|
||||||
|
export * from './generic_modal/footer_content';
|
||||||
export {GenericModal} from './generic_modal/generic_modal';
|
export {GenericModal} from './generic_modal/generic_modal';
|
||||||
export {CircleSkeletonLoader, RectangleSkeletonLoader} from './skeleton_loader';
|
export {CircleSkeletonLoader, RectangleSkeletonLoader} from './skeleton_loader';
|
||||||
export * from './tour_tip';
|
export * from './tour_tip';
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user