Merge branch 'master' into MM-51842-number-change-detection
Этот коммит содержится в:
@@ -1,4 +1,3 @@
|
||||
e2e/results
|
||||
node_modules
|
||||
dist
|
||||
lib
|
||||
|
||||
@@ -2,22 +2,19 @@
|
||||
"root": true,
|
||||
"extends": [
|
||||
"plugin:mattermost/react",
|
||||
"plugin:cypress/recommended",
|
||||
"plugin:react-hooks/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/eslint-plugin",
|
||||
"mattermost",
|
||||
"import",
|
||||
"cypress",
|
||||
"no-only-tests",
|
||||
"@typescript-eslint",
|
||||
"formatjs"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"env": {
|
||||
"jest": true,
|
||||
"cypress/globals": true
|
||||
"jest": true
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": "webpack",
|
||||
@@ -166,17 +163,6 @@
|
||||
"no-process-env": 0,
|
||||
"prefer-arrow-callback": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["e2e/**"],
|
||||
"rules": {
|
||||
"@babel/no-unused-expressions": 0,
|
||||
"func-names": 0,
|
||||
"import/no-unresolved": 0,
|
||||
"max-nested-callbacks": 0,
|
||||
"no-process-env": 0,
|
||||
"no-unused-expressions": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"properties": {
|
||||
"GomodMainOnly": true,
|
||||
"GomodExcludes": "_tests/|[-_]generators?$|^plugin/checker$",
|
||||
"NpmExcludes": "/e2e$",
|
||||
"GradlePath": "./gradlew:../gradlew"
|
||||
},
|
||||
"filters": [
|
||||
|
||||
@@ -6,58 +6,6 @@ export NODE_OPTIONS=--max-old-space-size=4096
|
||||
i18n-extract: ## Extract strings for translation from the source code
|
||||
npm run i18n-extract
|
||||
|
||||
e2e/playwright/node_modules:
|
||||
@echo Install Playwright and its dependencies
|
||||
cd e2e/playwright && npm install
|
||||
|
||||
.PHONY: e2e-test
|
||||
e2e-test:
|
||||
@echo E2E: Running mattermost-mysql-e2e
|
||||
@if [ $(shell docker ps -a | grep -ci mattermost-mysql-e2e) -eq 0 ]; then \
|
||||
echo starting mattermost-mysql-e2e; \
|
||||
docker run --name mattermost-mysql-e2e -p 35476:3306 -e MYSQL_ROOT_PASSWORD=mostest \
|
||||
-e MYSQL_USER=mmuser -e MYSQL_PASSWORD=mostest -e MYSQL_DATABASE=mattermost_test -d mysql:5.7 > /dev/null; \
|
||||
elif [ $(shell docker ps | grep -ci mattermost-mysql-e2e) -eq 0 ]; then \
|
||||
echo restarting mattermost-mysql-e2e; \
|
||||
docker start mattermost-mysql-e2e > /dev/null; \
|
||||
fi
|
||||
|
||||
cd $(BUILD_SERVER_DIR) && [[ -f config/config.json ]] && \
|
||||
cp config/config.json config/config-backup.json && make config-reset || \
|
||||
echo "config.json not found" && make config-reset
|
||||
|
||||
@echo E2E: Starting the server
|
||||
cd $(BUILD_SERVER_DIR) && $(MAKE) run
|
||||
|
||||
@echo E2E: Generating test data
|
||||
cd $(BUILD_SERVER_DIR) && $(MAKE) test-data
|
||||
|
||||
@echo E2E: Running end-to-end testing
|
||||
cd e2e && npm install && npm run cypress:run
|
||||
|
||||
@echo E2E: Stoppping the server
|
||||
cd $(BUILD_SERVER_DIR) && $(MAKE) stop
|
||||
|
||||
@echo E2E: stopping mattermost-mysql-e2e
|
||||
docker stop mattermost-mysql-e2e > /dev/null
|
||||
|
||||
cd $(BUILD_SERVER_DIR) && [[ -f config/config-backup.json ]] && \
|
||||
cp config/config-backup.json config/config.json && echo "revert local config.json" || \
|
||||
echo "config-backup.json not found" && sed -i'' -e 's|"DataSource": ".*"|"DataSource": "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8\u0026readTimeout=30s\u0026writeTimeout=30s"|g' config/config.json
|
||||
|
||||
@echo E2E: Tests completed
|
||||
|
||||
.PHONY: clean-e2e
|
||||
clean-e2e:
|
||||
@if [ $(shell docker ps -a | grep -ci mattermost-mysql-e2e) -eq 1 ]; then \
|
||||
echo stopping mattermost-mysql-e2e; \
|
||||
docker stop mattermost-mysql-e2e > /dev/null; \
|
||||
fi
|
||||
|
||||
cd $(BUILD_SERVER_DIR) && [[ -f config/config-backup.json ]] && \
|
||||
cp config/config-backup.json config/config.json && echo "revert local config.json" || \
|
||||
echo "config-backup.json not found" && sed -i'' -e 's|"DataSource": ".*"|"DataSource": "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8\u0026readTimeout=30s\u0026writeTimeout=30s"|g' config/config.json
|
||||
|
||||
.PHONY: emojis
|
||||
emojis: ## Creates emoji JSON, JSX and Go files and extracts emoji images from the system font
|
||||
SERVER_DIR=$(BUILD_SERVER_DIR) npm run make-emojis
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
const config = {
|
||||
snapshotSerializers: ['enzyme-to-json/serializer'],
|
||||
testPathIgnorePatterns: ['/node_modules/', '/e2e/'],
|
||||
testPathIgnorePatterns: ['/node_modules/'],
|
||||
clearMocks: true,
|
||||
collectCoverageFrom: [
|
||||
'actions/src/**/*.{js,jsx,ts,tsx}',
|
||||
@@ -17,7 +17,6 @@ const config = {
|
||||
'selectors/src/**/*.{js,jsx,ts,tsx}',
|
||||
'stores/src/**/*.{js,jsx,ts,tsx}',
|
||||
'utils/src/**/*.{js,jsx,ts,tsx}',
|
||||
'!e2e/**',
|
||||
],
|
||||
coverageReporters: ['lcov', 'text-summary'],
|
||||
moduleNameMapper: {
|
||||
|
||||
@@ -169,7 +169,6 @@
|
||||
"enzyme-to-json": "3.6.2",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-import-resolver-webpack": "0.13.2",
|
||||
"eslint-plugin-cypress": "2.11.3",
|
||||
"eslint-plugin-formatjs": "4.3.4",
|
||||
"eslint-plugin-header": "3.1.1",
|
||||
"eslint-plugin-import": "2.23.4",
|
||||
|
||||
@@ -35,7 +35,7 @@ import KeyboardShortcutsModal from 'components/keyboard_shortcuts/keyboard_short
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
import {t} from 'utils/i18n';
|
||||
import MarketplaceModal from 'components/plugin_marketplace';
|
||||
import MarketplaceModal from 'components/plugin_marketplace/marketplace_modal';
|
||||
import WorkTemplateModal from 'components/work_templates';
|
||||
import {haveICurrentTeamPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
|
||||
@@ -14,13 +14,12 @@ import {
|
||||
searchFilesWithParams,
|
||||
} from 'mattermost-redux/actions/search';
|
||||
import * as PostActions from 'mattermost-redux/actions/posts';
|
||||
import {getCurrentUserId, getCurrentUserMentionKeys} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getCurrentUserMentionKeys} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getCurrentChannelId, getCurrentChannelNameForSearchShortcut, getChannel as getChannelSelector} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {makeGetUserTimezone} from 'mattermost-redux/selectors/entities/timezone';
|
||||
import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils';
|
||||
import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
|
||||
import {Action, ActionResult, DispatchFunc, GenericAction, GetStateFunc} from 'mattermost-redux/types/actions';
|
||||
import {Post} from '@mattermost/types/posts';
|
||||
|
||||
@@ -174,9 +173,7 @@ export function performSearch(terms: string, isMentionSearch?: boolean) {
|
||||
}
|
||||
|
||||
// timezone offset in seconds
|
||||
const userId = getCurrentUserId(getState());
|
||||
const userTimezone = makeGetUserTimezone()(getState(), userId);
|
||||
const userCurrentTimezone = getUserCurrentTimezone(userTimezone);
|
||||
const userCurrentTimezone = getCurrentTimezone(getState());
|
||||
const timezoneOffset = ((userCurrentTimezone && (userCurrentTimezone.length > 0)) ? getUtcOffsetForTimeZone(userCurrentTimezone) : getBrowserUtcOffset()) * 60;
|
||||
const messagesPromise = dispatch(searchPostsWithParams(isMentionSearch ? '' : teamId, {terms, is_or_search: Boolean(isMentionSearch), include_deleted_channels: viewArchivedChannels, time_zone_offset: timezoneOffset, page: 0, per_page: 20}));
|
||||
const filesPromise = dispatch(searchFilesWithParams(teamId, {terms: termsWithExtensionsFilters, is_or_search: Boolean(isMentionSearch), include_deleted_channels: viewArchivedChannels, time_zone_offset: timezoneOffset, page: 0, per_page: 20}));
|
||||
|
||||
@@ -59,7 +59,7 @@ exports[`components/GenericModal should match snapshot for base case 1`] = `
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
className="GenericModal__body"
|
||||
className="GenericModal__body padding"
|
||||
/>
|
||||
</ModalBody>
|
||||
</div>
|
||||
@@ -126,11 +126,12 @@ exports[`components/GenericModal should match snapshot with both buttons 1`] = `
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
className="GenericModal__body"
|
||||
className="GenericModal__body padding"
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter
|
||||
bsClass="modal-footer"
|
||||
className=""
|
||||
componentClass="div"
|
||||
>
|
||||
<button
|
||||
@@ -218,11 +219,12 @@ exports[`components/GenericModal should match snapshot with disabled confirm but
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
className="GenericModal__body"
|
||||
className="GenericModal__body padding"
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter
|
||||
bsClass="modal-footer"
|
||||
className=""
|
||||
componentClass="div"
|
||||
>
|
||||
<button
|
||||
|
||||
@@ -39,7 +39,6 @@ exports[`components/actions_menu/ActionsMenu has actions - marketplace disabled
|
||||
className="post-menu__item post-menu__item--active"
|
||||
id="center_actions_button_post_id_1"
|
||||
key="more-actions-button"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
@@ -107,7 +106,6 @@ exports[`components/actions_menu/ActionsMenu has actions - marketplace enabled a
|
||||
className="post-menu__item post-menu__item--active"
|
||||
id="center_actions_button_post_id_1"
|
||||
key="more-actions-button"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
@@ -194,7 +192,6 @@ exports[`components/actions_menu/ActionsMenu no actions - sysadmin - menu should
|
||||
className="post-menu__item post-menu__item--active"
|
||||
id="center_actions_button_post_id_1"
|
||||
key="more-actions-button"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
|
||||
@@ -38,12 +38,7 @@ describe('components/actions_menu/ActionsMenu', () => {
|
||||
isSysAdmin: true,
|
||||
pluginMenuItems: [],
|
||||
post: TestHelper.getPostMock({id: 'post_id_1', is_pinned: false, type: '' as PostType}),
|
||||
showTutorialTip: false,
|
||||
components: {},
|
||||
handleOpenTip: jest.fn(),
|
||||
handleNextTip: jest.fn(),
|
||||
handleDismissTip: jest.fn(),
|
||||
showPulsatingDot: false,
|
||||
location: 'center',
|
||||
canOpenMarketplace: false,
|
||||
actions: {
|
||||
|
||||
@@ -17,10 +17,8 @@ import {AppCallResponseTypes} from 'mattermost-redux/constants/apps';
|
||||
import {HandleBindingClick, PostEphemeralCallResponseForPost, OpenAppsModal} from 'types/apps';
|
||||
import {Locations, Constants, ModalIdentifiers} from 'utils/constants';
|
||||
import Permissions from 'mattermost-redux/constants/permissions';
|
||||
import {ActionsTutorialTip} from 'components/actions_menu/actions_menu_tutorial_tip';
|
||||
import {ModalData} from 'types/actions';
|
||||
import MarketplaceModal from 'components/plugin_marketplace';
|
||||
import {OpenedFromType} from 'components/plugin_marketplace/marketplace_modal';
|
||||
import MarketplaceModal, {OpenedFromType} from 'components/plugin_marketplace/marketplace_modal';
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
import * as PostUtils from 'utils/post_utils';
|
||||
import * as Utils from 'utils/utils';
|
||||
@@ -45,11 +43,6 @@ export type Props = {
|
||||
pluginMenuItems?: PluginComponent[];
|
||||
post: Post;
|
||||
teamId: string;
|
||||
handleOpenTip: () => void;
|
||||
handleNextTip: (e: React.MouseEvent) => void;
|
||||
handleDismissTip: () => void;
|
||||
showPulsatingDot?: boolean;
|
||||
showTutorialTip: boolean;
|
||||
canOpenMarketplace: boolean;
|
||||
|
||||
/**
|
||||
@@ -237,14 +230,6 @@ export class ActionMenuClass extends React.PureComponent<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
handleActionsIconClick = (e: React.MouseEvent) => {
|
||||
if (this.props.showPulsatingDot || this.props.showTutorialTip) {
|
||||
this.props.handleOpenTip();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
renderDivider = (suffix: string): React.ReactNode => {
|
||||
return (
|
||||
<li
|
||||
@@ -414,17 +399,8 @@ export class ActionMenuClass extends React.PureComponent<Props, State> {
|
||||
})}
|
||||
type='button'
|
||||
aria-expanded='false'
|
||||
onClick={this.handleActionsIconClick}
|
||||
>
|
||||
<i className={'icon icon-apps'}/>
|
||||
{this.props.showPulsatingDot &&
|
||||
<ActionsTutorialTip
|
||||
showTip={this.props.showTutorialTip}
|
||||
handleNext={this.props.handleNextTip}
|
||||
handleOpen={this.props.handleOpenTip}
|
||||
handleDismiss={this.props.handleDismissTip}
|
||||
/>
|
||||
}
|
||||
</button>
|
||||
</OverlayTrigger>
|
||||
<Menu
|
||||
|
||||
@@ -37,11 +37,6 @@ describe('components/actions_menu/ActionsMenu returning empty ("")', () => {
|
||||
},
|
||||
appBindings: [],
|
||||
pluginMenuItems: [],
|
||||
handleOpenTip: jest.fn(),
|
||||
handleNextTip: jest.fn(),
|
||||
handleDismissTip: jest.fn(),
|
||||
showPulsatingDot: false,
|
||||
showTutorialTip: false,
|
||||
appsEnabled: false,
|
||||
isSysAdmin: true,
|
||||
canOpenMarketplace: false,
|
||||
|
||||
@@ -37,11 +37,6 @@ describe('components/actions_menu/ActionsMenu on mobile view', () => {
|
||||
},
|
||||
appBindings: [],
|
||||
pluginMenuItems: [],
|
||||
handleOpenTip: jest.fn(),
|
||||
handleNextTip: jest.fn(),
|
||||
handleDismissTip: jest.fn(),
|
||||
showPulsatingDot: false,
|
||||
showTutorialTip: false,
|
||||
appsEnabled: false,
|
||||
isSysAdmin: true,
|
||||
canOpenMarketplace: false,
|
||||
|
||||
@@ -11,14 +11,13 @@ import {TopChannel, TopChannelGraphData} from '@mattermost/types/insights';
|
||||
import {CircleSkeletonLoader, RectangleSkeletonLoader} from '@mattermost/components';
|
||||
|
||||
import {getCurrentRelativeTeamUrl, getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
|
||||
import {getMyTopChannels, getTopChannelsForTeam} from 'mattermost-redux/actions/insights';
|
||||
|
||||
import Constants, {InsightsScopes} from 'utils/constants';
|
||||
|
||||
import {trackEvent} from 'actions/telemetry_actions';
|
||||
|
||||
import {getCurrentUserTimezone} from 'selectors/general';
|
||||
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
import Tooltip from 'components/tooltip';
|
||||
|
||||
@@ -38,7 +37,7 @@ const TopChannels = (props: WidgetHocProps) => {
|
||||
|
||||
const currentTeamId = useSelector(getCurrentTeamId);
|
||||
const currentTeamUrl = useSelector(getCurrentRelativeTeamUrl);
|
||||
const timeZone = useSelector(getCurrentUserTimezone);
|
||||
const timeZone = useSelector(getCurrentTimezone);
|
||||
|
||||
const getTopTeamChannels = useCallback(async () => {
|
||||
if (props.filterType === InsightsScopes.TEAM) {
|
||||
|
||||
@@ -226,9 +226,9 @@ export const InvoiceInfo = ({invoice, product, fullCharges, partialCharges, hasM
|
||||
currency='USD'
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscriptions.billing_summary.lastInvoice.userCount'
|
||||
defaultMessage=' x {users} users'
|
||||
values={{users: charge.quantity}}
|
||||
id='admin.billing.subscriptions.billing_summary.lastInvoice.seatCount'
|
||||
defaultMessage=' x {seats} seats'
|
||||
values={{seats: charge.quantity}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -309,9 +309,9 @@ export const InvoiceInfo = ({invoice, product, fullCharges, partialCharges, hasM
|
||||
>
|
||||
<div className='BillingSummary__lastInvoice-chargeDescription'>
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscriptions.billing_summary.lastInvoice.userCountPartial'
|
||||
defaultMessage='{users} users'
|
||||
values={{users: charge.quantity}}
|
||||
id='admin.billing.subscriptions.billing_summary.lastInvoice.seatCountPartial'
|
||||
defaultMessage='{seats} seats'
|
||||
values={{seats: charge.quantity}}
|
||||
/>
|
||||
</div>
|
||||
<div className='BillingSummary__lastInvoice-chargeAmount'>
|
||||
|
||||
@@ -15,8 +15,6 @@ import {isCloudLicense} from 'utils/license_utils';
|
||||
|
||||
import {getCloudSubscription, getSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud';
|
||||
|
||||
import ExternalLink from 'components/external_link';
|
||||
|
||||
import DeleteWorkspaceModal from './delete_workspace_modal';
|
||||
|
||||
export default function DeleteWorkspaceCTA() {
|
||||
@@ -80,17 +78,15 @@ export default function DeleteWorkspaceCTA() {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ExternalLink
|
||||
href=''
|
||||
location='delete_workspace_cta'
|
||||
className='cancelSubscriptionSection__contactUs'
|
||||
<button
|
||||
className='btn cancelSubscriptionSection__contactUs'
|
||||
onClick={handleOnClickDelete}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.billing.subscription.deleteWorkspaceSection.delete'
|
||||
defaultMessage='Delete Workspace'
|
||||
/>
|
||||
</ExternalLink>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -51,28 +51,28 @@ describe('InvoiceUserCount', () => {
|
||||
[1, InvoiceLineItemType.Full],
|
||||
[1, InvoiceLineItemType.Partial],
|
||||
),
|
||||
expected: '1 metered users, 1 users at full rate, 1 users with partial charges',
|
||||
expected: '1 metered seats, 1 seats at full rate, 1 seats with partial charges',
|
||||
},
|
||||
{
|
||||
name: 'Supports cloud invoices with only metered line items',
|
||||
invoice: makeInvoice(
|
||||
[12.34, InvoiceLineItemType.Metered],
|
||||
),
|
||||
expected: '12.34 users',
|
||||
expected: '12.34 seats',
|
||||
},
|
||||
{
|
||||
name: 'Shows minimum decimal necessary',
|
||||
invoice: makeInvoice(
|
||||
[12.499, InvoiceLineItemType.Metered],
|
||||
),
|
||||
expected: '12.5 users',
|
||||
expected: '12.5 seats',
|
||||
},
|
||||
{
|
||||
name: 'hides insignificant decimals',
|
||||
invoice: makeInvoice(
|
||||
[12.002, InvoiceLineItemType.Metered],
|
||||
),
|
||||
expected: '12 users',
|
||||
expected: '12 seats',
|
||||
},
|
||||
{
|
||||
name: 'Supports cloud invoices with only non-metered line items',
|
||||
@@ -80,7 +80,7 @@ describe('InvoiceUserCount', () => {
|
||||
[1, InvoiceLineItemType.Full],
|
||||
[249, InvoiceLineItemType.Partial],
|
||||
),
|
||||
expected: '1 users at full rate, 249 users with partial charges',
|
||||
expected: '1 seats at full rate, 249 seats with partial charges',
|
||||
},
|
||||
{
|
||||
name: 'Shows default of 0 full users, 0 partial users when there are no users',
|
||||
@@ -89,19 +89,19 @@ describe('InvoiceUserCount', () => {
|
||||
[0, InvoiceLineItemType.Full],
|
||||
[0, InvoiceLineItemType.Partial],
|
||||
),
|
||||
expected: '0 users at full rate, 0 users with partial charges',
|
||||
expected: '0 seats at full rate, 0 seats with partial charges',
|
||||
},
|
||||
{
|
||||
name: 'Shows default of 0 full users, 0 partial users when there are no line items in invoice',
|
||||
invoice: makeInvoice(),
|
||||
expected: '0 users at full rate, 0 users with partial charges',
|
||||
expected: '0 seats at full rate, 0 seats with partial charges',
|
||||
},
|
||||
{
|
||||
name: 'Shows 3 full userswhen there are on prem users',
|
||||
invoice: makeInvoice(
|
||||
[3, InvoiceLineItemType.OnPremise],
|
||||
),
|
||||
expected: '3 users',
|
||||
expected: '3 seats',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ export default function InvoiceUserCount({invoice}: {invoice: Invoice}): JSX.Ele
|
||||
if (onPremUsers) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='admin.billing.history.onPremUsers'
|
||||
defaultMessage='{num} users'
|
||||
id='admin.billing.history.onPremSeats'
|
||||
defaultMessage='{num} seats'
|
||||
values={{
|
||||
|
||||
// should always be a whole number, but truncate just in case
|
||||
@@ -32,12 +32,12 @@ export default function InvoiceUserCount({invoice}: {invoice: Invoice}): JSX.Ele
|
||||
if (fullUsers || partialUsers) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='admin.billing.history.fractionalAndRatedUsers'
|
||||
defaultMessage='{fractionalUsers} metered users, {fullUsers} users at full rate, {partialUsers} users with partial charges'
|
||||
id='admin.billing.history.fractionalAndRatedSeats'
|
||||
defaultMessage='{fractionalSeats} metered seats, {fullSeats} seats at full rate, {partialSeats} seats with partial charges'
|
||||
values={{
|
||||
fractionalUsers: numberToFixedDynamic(meteredUsers, 2),
|
||||
fullUsers: fullUsers.toFixed(0),
|
||||
partialUsers: partialUsers.toFixed(0),
|
||||
fractionalSeats: numberToFixedDynamic(meteredUsers, 2),
|
||||
fullSeats: fullUsers.toFixed(0),
|
||||
partialSeats: partialUsers.toFixed(0),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -45,10 +45,10 @@ export default function InvoiceUserCount({invoice}: {invoice: Invoice}): JSX.Ele
|
||||
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='admin.billing.history.fractionalUsers'
|
||||
defaultMessage='{fractionalUsers} users'
|
||||
id='admin.billing.history.fractionalSeats'
|
||||
defaultMessage='{fractionalSeats} seats'
|
||||
values={{
|
||||
fractionalUsers: numberToFixedDynamic(meteredUsers, 2),
|
||||
fractionalSeats: numberToFixedDynamic(meteredUsers, 2),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -56,11 +56,11 @@ export default function InvoiceUserCount({invoice}: {invoice: Invoice}): JSX.Ele
|
||||
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='admin.billing.history.usersAndRates'
|
||||
defaultMessage='{fullUsers} users at full rate, {partialUsers} users with partial charges'
|
||||
id='admin.billing.history.seatsAndRates'
|
||||
defaultMessage='{fullSeats} seats at full rate, {partialSeats} seats with partial charges'
|
||||
values={{
|
||||
fullUsers: fullUsers.toFixed(0),
|
||||
partialUsers: partialUsers.toFixed(0),
|
||||
fullSeats: fullUsers.toFixed(0),
|
||||
partialSeats: partialUsers.toFixed(0),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('components/admin_console/license_settings/enterprise_edition/enterpris
|
||||
return n.children().length === 2 &&
|
||||
n.childAt(0).type() === 'span' &&
|
||||
!n.childAt(0).text().includes('ACTIVE') &&
|
||||
n.childAt(0).text().includes('USERS');
|
||||
n.childAt(0).text().includes('LICENSED SEATS');
|
||||
});
|
||||
|
||||
expect(item.text()).toContain('1,000,000');
|
||||
|
||||
@@ -160,7 +160,7 @@ const EnterpriseEditionLeftPanel = ({
|
||||
);
|
||||
};
|
||||
|
||||
type LegendValues = 'START DATE:' | 'EXPIRES:' | 'USERS:' | 'ACTIVE USERS:' | 'EDITION:' | 'LICENSE ISSUED:' | 'NAME:' | 'COMPANY / ORG:'
|
||||
type LegendValues = 'START DATE:' | 'EXPIRES:' | 'LICENSED SEATS:' | 'ACTIVE USERS:' | 'EDITION:' | 'LICENSE ISSUED:' | 'NAME:' | 'COMPANY / ORG:'
|
||||
|
||||
const renderLicenseValues = (activeUsers: number, seatsPurchased: number) => ({legend, value}: {legend: LegendValues; value: string | JSX.Element | null}, index: number): React.ReactNode => {
|
||||
if (legend === 'ACTIVE USERS:') {
|
||||
@@ -236,7 +236,7 @@ const renderLicenseContent = (
|
||||
}> = [
|
||||
{legend: 'START DATE:', value: startsAt},
|
||||
{legend: 'EXPIRES:', value: expiresAt},
|
||||
{legend: 'USERS:', value: users},
|
||||
{legend: 'LICENSED SEATS:', value: users},
|
||||
{legend: 'ACTIVE USERS:', value: activeUsers},
|
||||
{legend: 'EDITION:', value: sku},
|
||||
{legend: 'LICENSE ISSUED:', value: issued},
|
||||
|
||||
@@ -260,7 +260,7 @@ const UploadLicenseModal = (props: Props): JSX.Element | null => {
|
||||
<div className='subtitle'>
|
||||
<FormattedMessage
|
||||
id='admin.license.upload-modal.successfulUpgradeText'
|
||||
defaultMessage='You have upgraded to the {skuName} plan for {licensedUsersNum, number} users. This is effective from {startsAt} until {expiresAt}. '
|
||||
defaultMessage='You have upgraded to the {skuName} plan for {licensedUsersNum, number} seats. This is effective from {startsAt} until {expiresAt}. '
|
||||
values={{
|
||||
expiresAt,
|
||||
startsAt,
|
||||
|
||||
@@ -131,7 +131,7 @@ const AdvanceTextEditor = ({
|
||||
canUploadFiles,
|
||||
enableEmojiPicker,
|
||||
enableGifPicker,
|
||||
handleBlur,
|
||||
handleBlur: onBlur,
|
||||
handlePostError,
|
||||
emitTypingEvent,
|
||||
handleMouseUpKeyUp,
|
||||
@@ -170,6 +170,7 @@ const AdvanceTextEditor = ({
|
||||
const [scrollbarWidth, setScrollbarWidth] = useState(0);
|
||||
const [renderScrollbar, setRenderScrollbar] = useState(false);
|
||||
const [showFormattingSpacer, setShowFormattingSpacer] = useState(shouldShowPreview);
|
||||
const [keepEditorInFocus, setKeepEditorInFocus] = useState(false);
|
||||
|
||||
const input = textboxRef.current?.getInputBox();
|
||||
|
||||
@@ -187,6 +188,15 @@ const AdvanceTextEditor = ({
|
||||
setShowPreview(!shouldShowPreview);
|
||||
}, [shouldShowPreview, setShowPreview]);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
onBlur?.();
|
||||
setKeepEditorInFocus(false);
|
||||
}, [onBlur]);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
setKeepEditorInFocus(true);
|
||||
}, []);
|
||||
|
||||
let serverErrorJsx = null;
|
||||
if (serverError) {
|
||||
serverErrorJsx = (
|
||||
@@ -422,6 +432,7 @@ const AdvanceTextEditor = ({
|
||||
/>
|
||||
)}
|
||||
slot2={null}
|
||||
shouldScrollIntoView={keepEditorInFocus}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -479,6 +490,7 @@ const AdvanceTextEditor = ({
|
||||
handlePostError={handlePostError}
|
||||
value={messageValue}
|
||||
onBlur={handleBlur}
|
||||
onFocus={handleFocus}
|
||||
emojiEnabled={enableEmojiPicker}
|
||||
createMessage={createMessage}
|
||||
channelId={channelId}
|
||||
|
||||
@@ -56,7 +56,7 @@ export const ActivatedUserCard = ({activatedUsers, seatsPurchased, isCloud}: Act
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='analytics.team.overageUsersSeats'
|
||||
defaultMessage='This exceeds total paid users'
|
||||
defaultMessage='This exceeds total paid seats'
|
||||
>
|
||||
{(text) => <span>{text}</span>}
|
||||
</FormattedMessage>
|
||||
|
||||
@@ -348,7 +348,7 @@ export default class SystemAnalytics extends React.PureComponent<Props, State> {
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.seatsPurchased'
|
||||
defaultMessage='Total paid users'
|
||||
defaultMessage='Licensed Seats'
|
||||
/>
|
||||
}
|
||||
icon='fa-users'
|
||||
|
||||
@@ -10,7 +10,7 @@ import Icon from '@mattermost/compass-components/foundations/icon';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
|
||||
import MarketplaceModal from 'components/plugin_marketplace';
|
||||
import MarketplaceModal from 'components/plugin_marketplace/marketplace_modal';
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
|
||||
import {Constants, ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import React, {ChangeEventHandler} from 'react';
|
||||
|
||||
import {Channel} from '@mattermost/types/channels';
|
||||
|
||||
@@ -10,7 +10,7 @@ import * as Utils from 'utils/utils';
|
||||
|
||||
type Props = {
|
||||
channels: Channel[];
|
||||
onChange?: () => void;
|
||||
onChange?: ChangeEventHandler<HTMLSelectElement>;
|
||||
value?: string;
|
||||
selectOpen: boolean;
|
||||
selectPrivate: boolean;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
@import '../payment_form/mixins';
|
||||
|
||||
.shipping-address-section {
|
||||
display: flex;
|
||||
align-content: flex-start;
|
||||
align-items: center;
|
||||
padding-bottom: 24px;
|
||||
font-weight: normal;
|
||||
|
||||
button.no-style {
|
||||
height: auto;
|
||||
padding-left: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
@@ -26,7 +29,6 @@
|
||||
padding-left: 12px;
|
||||
cursor: default;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.billing_address_btn_text {
|
||||
@@ -35,3 +37,7 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.PurchaseModal .shipping-address-section {
|
||||
@include payment-form-padding;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {uniqWith} from 'lodash';
|
||||
import {Post} from '@mattermost/types/posts';
|
||||
import {MarketplacePlugin} from '@mattermost/types/marketplace';
|
||||
|
||||
import MarketplaceModal from 'components/plugin_marketplace';
|
||||
import MarketplaceModal from 'components/plugin_marketplace/marketplace_modal';
|
||||
import Markdown from 'components/markdown';
|
||||
|
||||
import {getUsers} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
@@ -12,8 +12,8 @@ import mockStore from 'tests/test_store';
|
||||
|
||||
import CustomStatusEmoji from './custom_status_emoji';
|
||||
|
||||
jest.mock('mattermost-redux/selectors/entities/timezone');
|
||||
jest.mock('selectors/views/custom_status');
|
||||
jest.mock('selectors/general');
|
||||
|
||||
describe('components/custom_status/custom_status_emoji', () => {
|
||||
const store = mockStore({});
|
||||
|
||||
@@ -6,8 +6,9 @@ import {useSelector} from 'react-redux';
|
||||
|
||||
import {CustomStatusDuration} from '@mattermost/types/users';
|
||||
|
||||
import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
import {getCurrentUserTimezone} from 'selectors/general';
|
||||
import {makeGetCustomStatus, isCustomStatusEnabled, isCustomStatusExpired} from 'selectors/views/custom_status';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
@@ -41,7 +42,7 @@ function CustomStatusEmoji({
|
||||
const getCustomStatus = useMemo(makeGetCustomStatus, []);
|
||||
const customStatus = useSelector((state: GlobalState) => getCustomStatus(state, userID));
|
||||
|
||||
const timezone = useSelector(getCurrentUserTimezone);
|
||||
const timezone = useSelector(getCurrentTimezone);
|
||||
|
||||
const customStatusExpired = useSelector((state: GlobalState) => isCustomStatusExpired(state, customStatus));
|
||||
const customStatusEnabled = useSelector(isCustomStatusEnabled);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {useRouteMatch} from 'react-router-dom';
|
||||
import {setCustomStatus, unsetCustomStatus, removeRecentCustomStatus} from 'mattermost-redux/actions/users';
|
||||
import {setCustomStatusInitialisationState} from 'mattermost-redux/actions/preferences';
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
|
||||
|
||||
import {loadCustomEmojisIfNeeded} from 'actions/emoji_actions';
|
||||
import {closeModal} from 'actions/views/modals';
|
||||
@@ -20,7 +21,6 @@ import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay';
|
||||
import RenderEmoji from 'components/emoji/render_emoji';
|
||||
import QuickInput, {MaxLengthInput} from 'components/quick_input';
|
||||
import {makeGetCustomStatus, getRecentCustomStatuses, showStatusDropdownPulsatingDot, isCustomStatusExpired} from 'selectors/views/custom_status';
|
||||
import {getCurrentUserTimezone} from 'selectors/general';
|
||||
import {GlobalState} from 'types/store';
|
||||
import {getCurrentMomentForTimezone} from 'utils/timezone';
|
||||
import {A11yCustomEventTypes, A11yFocusEventDetail, Constants, ModalIdentifiers} from 'utils/constants';
|
||||
@@ -115,7 +115,7 @@ const CustomStatusModal: React.FC<Props> = (props: Props) => {
|
||||
const [duration, setDuration] = useState<CustomStatusDuration>(initialDuration === undefined ? defaultDuration : initialDuration);
|
||||
const isStatusSet = Boolean(emoji || text);
|
||||
const firstTimeModalOpened = useSelector(showStatusDropdownPulsatingDot);
|
||||
const timezone = useSelector(getCurrentUserTimezone);
|
||||
const timezone = useSelector(getCurrentTimezone);
|
||||
const inCustomEmojiPath = useRouteMatch('/:team/emoji');
|
||||
|
||||
const currentTime = getCurrentMomentForTimezone(timezone);
|
||||
|
||||
@@ -107,113 +107,6 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`components/dot_menu/DotMenu should match snapshot, hide "New" badge on forward post 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div>
|
||||
<button
|
||||
aria-controls="CENTER_dropdown_post_id_1"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-label="Actions"
|
||||
class="post-menu__item"
|
||||
data-testid="PostDotMenu-Button-post_id_1"
|
||||
id="CENTER_button_post_id_1"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<button
|
||||
aria-controls="CENTER_dropdown_post_id_1"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-label="Actions"
|
||||
class="post-menu__item"
|
||||
data-testid="PostDotMenu-Button-post_id_1"
|
||||
id="CENTER_button_post_id_1"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>,
|
||||
"debug": [Function],
|
||||
"findAllByAltText": [Function],
|
||||
"findAllByDisplayValue": [Function],
|
||||
"findAllByLabelText": [Function],
|
||||
"findAllByPlaceholderText": [Function],
|
||||
"findAllByRole": [Function],
|
||||
"findAllByTestId": [Function],
|
||||
"findAllByText": [Function],
|
||||
"findAllByTitle": [Function],
|
||||
"findByAltText": [Function],
|
||||
"findByDisplayValue": [Function],
|
||||
"findByLabelText": [Function],
|
||||
"findByPlaceholderText": [Function],
|
||||
"findByRole": [Function],
|
||||
"findByTestId": [Function],
|
||||
"findByText": [Function],
|
||||
"findByTitle": [Function],
|
||||
"getAllByAltText": [Function],
|
||||
"getAllByDisplayValue": [Function],
|
||||
"getAllByLabelText": [Function],
|
||||
"getAllByPlaceholderText": [Function],
|
||||
"getAllByRole": [Function],
|
||||
"getAllByTestId": [Function],
|
||||
"getAllByText": [Function],
|
||||
"getAllByTitle": [Function],
|
||||
"getByAltText": [Function],
|
||||
"getByDisplayValue": [Function],
|
||||
"getByLabelText": [Function],
|
||||
"getByPlaceholderText": [Function],
|
||||
"getByRole": [Function],
|
||||
"getByTestId": [Function],
|
||||
"getByText": [Function],
|
||||
"getByTitle": [Function],
|
||||
"queryAllByAltText": [Function],
|
||||
"queryAllByDisplayValue": [Function],
|
||||
"queryAllByLabelText": [Function],
|
||||
"queryAllByPlaceholderText": [Function],
|
||||
"queryAllByRole": [Function],
|
||||
"queryAllByTestId": [Function],
|
||||
"queryAllByText": [Function],
|
||||
"queryAllByTitle": [Function],
|
||||
"queryByAltText": [Function],
|
||||
"queryByDisplayValue": [Function],
|
||||
"queryByLabelText": [Function],
|
||||
"queryByPlaceholderText": [Function],
|
||||
"queryByRole": [Function],
|
||||
"queryByTestId": [Function],
|
||||
"queryByText": [Function],
|
||||
"queryByTitle": [Function],
|
||||
"rerender": [Function],
|
||||
"unmount": [Function],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`components/dot_menu/DotMenu should match snapshot, on Center 1`] = `
|
||||
<Menu
|
||||
menu={
|
||||
@@ -483,110 +376,3 @@ exports[`components/dot_menu/DotMenu should match snapshot, on Center 1`] = `
|
||||
/>
|
||||
</Menu>
|
||||
`;
|
||||
|
||||
exports[`components/dot_menu/DotMenu should match snapshot, show "New" badge on forward post 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div>
|
||||
<button
|
||||
aria-controls="CENTER_dropdown_post_id_1"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-label="Actions"
|
||||
class="post-menu__item"
|
||||
data-testid="PostDotMenu-Button-post_id_1"
|
||||
id="CENTER_button_post_id_1"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<button
|
||||
aria-controls="CENTER_dropdown_post_id_1"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
aria-label="Actions"
|
||||
class="post-menu__item"
|
||||
data-testid="PostDotMenu-Button-post_id_1"
|
||||
id="CENTER_button_post_id_1"
|
||||
>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
height="16"
|
||||
version="1.1"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>,
|
||||
"debug": [Function],
|
||||
"findAllByAltText": [Function],
|
||||
"findAllByDisplayValue": [Function],
|
||||
"findAllByLabelText": [Function],
|
||||
"findAllByPlaceholderText": [Function],
|
||||
"findAllByRole": [Function],
|
||||
"findAllByTestId": [Function],
|
||||
"findAllByText": [Function],
|
||||
"findAllByTitle": [Function],
|
||||
"findByAltText": [Function],
|
||||
"findByDisplayValue": [Function],
|
||||
"findByLabelText": [Function],
|
||||
"findByPlaceholderText": [Function],
|
||||
"findByRole": [Function],
|
||||
"findByTestId": [Function],
|
||||
"findByText": [Function],
|
||||
"findByTitle": [Function],
|
||||
"getAllByAltText": [Function],
|
||||
"getAllByDisplayValue": [Function],
|
||||
"getAllByLabelText": [Function],
|
||||
"getAllByPlaceholderText": [Function],
|
||||
"getAllByRole": [Function],
|
||||
"getAllByTestId": [Function],
|
||||
"getAllByText": [Function],
|
||||
"getAllByTitle": [Function],
|
||||
"getByAltText": [Function],
|
||||
"getByDisplayValue": [Function],
|
||||
"getByLabelText": [Function],
|
||||
"getByPlaceholderText": [Function],
|
||||
"getByRole": [Function],
|
||||
"getByTestId": [Function],
|
||||
"getByText": [Function],
|
||||
"getByTitle": [Function],
|
||||
"queryAllByAltText": [Function],
|
||||
"queryAllByDisplayValue": [Function],
|
||||
"queryAllByLabelText": [Function],
|
||||
"queryAllByPlaceholderText": [Function],
|
||||
"queryAllByRole": [Function],
|
||||
"queryAllByTestId": [Function],
|
||||
"queryAllByText": [Function],
|
||||
"queryAllByTitle": [Function],
|
||||
"queryByAltText": [Function],
|
||||
"queryByDisplayValue": [Function],
|
||||
"queryByLabelText": [Function],
|
||||
"queryByPlaceholderText": [Function],
|
||||
"queryByRole": [Function],
|
||||
"queryByTestId": [Function],
|
||||
"queryByText": [Function],
|
||||
"queryByTitle": [Function],
|
||||
"rerender": [Function],
|
||||
"unmount": [Function],
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -146,7 +146,6 @@ describe('components/dot_menu/DotMenu', () => {
|
||||
threadReplyCount: 0,
|
||||
userId: 'user_id_1',
|
||||
isMilitaryTime: false,
|
||||
showForwardPostNewLabel: false,
|
||||
};
|
||||
|
||||
test('should match snapshot, on Center', () => {
|
||||
@@ -181,32 +180,6 @@ describe('components/dot_menu/DotMenu', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot, show "New" badge on forward post', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
showForwardPostNewLabel: true,
|
||||
};
|
||||
const wrapper = renderWithIntlAndStore(
|
||||
<DotMenu {...props}/>,
|
||||
initialState,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot, hide "New" badge on forward post', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
showForwardPostNewLabel: false,
|
||||
};
|
||||
const wrapper = renderWithIntlAndStore(
|
||||
<DotMenu {...props}/>,
|
||||
initialState,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should show mark as unread when channel is not archived', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
|
||||
import Permissions from 'mattermost-redux/constants/permissions';
|
||||
|
||||
import {Locations, ModalIdentifiers, Constants, TELEMETRY_LABELS, Preferences} from 'utils/constants';
|
||||
import {Locations, ModalIdentifiers, Constants, TELEMETRY_LABELS} from 'utils/constants';
|
||||
import DeletePostModal from 'components/delete_post_modal';
|
||||
import DelayedAction from 'utils/delayed_action';
|
||||
import * as PostUtils from 'utils/post_utils';
|
||||
@@ -33,12 +33,10 @@ import * as Menu from 'components/menu';
|
||||
import * as Utils from 'utils/utils';
|
||||
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
|
||||
import {ModalData} from 'types/actions';
|
||||
import {PluginComponent} from 'types/store/plugins';
|
||||
|
||||
import {UserThread} from '@mattermost/types/threads';
|
||||
import {Post} from '@mattermost/types/posts';
|
||||
import ForwardPostModal from '../forward_post_modal';
|
||||
import Tag from '../widgets/tag/tag';
|
||||
|
||||
import {ChangeEvent, trackDotMenuEvent} from './utils';
|
||||
|
||||
@@ -70,20 +68,11 @@ type Props = {
|
||||
postEditTimeLimit?: string; // TechDebt: Made non-mandatory while converting to typescript
|
||||
enableEmojiPicker?: boolean; // TechDebt: Made non-mandatory while converting to typescript
|
||||
channelIsArchived?: boolean; // TechDebt: Made non-mandatory while converting to typescript
|
||||
currentTeamUrl?: string; // TechDebt: Made non-mandatory while converting to typescript
|
||||
teamUrl?: string; // TechDebt: Made non-mandatory while converting to typescript
|
||||
isMobileView: boolean;
|
||||
showForwardPostNewLabel: boolean;
|
||||
timezone?: string;
|
||||
isMilitaryTime: boolean;
|
||||
|
||||
/**
|
||||
* Components for overriding provided by plugins
|
||||
*/
|
||||
components: {
|
||||
[componentName: string]: PluginComponent[];
|
||||
};
|
||||
|
||||
actions: {
|
||||
|
||||
/**
|
||||
@@ -292,9 +281,6 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
||||
},
|
||||
};
|
||||
|
||||
if (this.props.showForwardPostNewLabel) {
|
||||
this.props.actions.setGlobalItem(Preferences.FORWARD_POST_VIEWED, false);
|
||||
}
|
||||
this.props.actions.openModal(forwardPostModalData);
|
||||
}
|
||||
|
||||
@@ -441,15 +427,6 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
||||
id='forward_post_button.label'
|
||||
defaultMessage='Forward'
|
||||
/>
|
||||
{this.props.showForwardPostNewLabel && (
|
||||
<Tag
|
||||
variant='success'
|
||||
text={formatMessage({
|
||||
id: 'tag.default.new',
|
||||
defaultMessage: 'NEW',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -519,12 +496,12 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
||||
class: classNames('post-menu__item', {
|
||||
'post-menu__item--active': this.props.isMenuOpen,
|
||||
}),
|
||||
'aria-label': this.props.intl.formatMessage({id: 'post_info.dot_menu.tooltip.more_actions', defaultMessage: 'Actions'}),
|
||||
'aria-label': formatMessage({id: 'post_info.dot_menu.tooltip.more_actions', defaultMessage: 'Actions'}),
|
||||
children: <DotsHorizontalIcon size={16}/>,
|
||||
}}
|
||||
menu={{
|
||||
id: `${this.props.location}_dropdown_${this.props.post.id}`,
|
||||
'aria-label': this.props.intl.formatMessage({id: 'post_info.menuAriaLabel', defaultMessage: 'Post extra options'}),
|
||||
'aria-label': formatMessage({id: 'post_info.menuAriaLabel', defaultMessage: 'Post extra options'}),
|
||||
onKeyDown: this.onShortcutKeyDown,
|
||||
width: '264px',
|
||||
onToggle: this.handleMenuToggle,
|
||||
@@ -532,7 +509,7 @@ export class DotMenuClass extends React.PureComponent<Props, State> {
|
||||
}}
|
||||
menuButtonTooltip={{
|
||||
id: `PostDotMenu-ButtonTooltip-${this.props.post.id}`,
|
||||
text: this.props.intl.formatMessage({id: 'post_info.dot_menu.tooltip.more_actions', defaultMessage: 'More'}),
|
||||
text: formatMessage({id: 'post_info.dot_menu.tooltip.more_actions', defaultMessage: 'More'}),
|
||||
class: 'hidden-xs',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -59,7 +59,6 @@ describe('components/dot_menu/DotMenu returning empty ("")', () => {
|
||||
threadId: 'post_id_1',
|
||||
userId: 'user_id_1',
|
||||
isMilitaryTime: false,
|
||||
showForwardPostNewLabel: false,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
|
||||
@@ -59,7 +59,6 @@ describe('components/dot_menu/DotMenu on mobile view', () => {
|
||||
threadId: 'post_id_1',
|
||||
userId: 'user_id_1',
|
||||
isMilitaryTime: false,
|
||||
showForwardPostNewLabel: false,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
|
||||
@@ -12,7 +12,7 @@ import {getCurrentTeamId, getCurrentTeam, getTeam} from 'mattermost-redux/select
|
||||
import {makeGetThreadOrSynthetic} from 'mattermost-redux/selectors/entities/threads';
|
||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getBool, isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserTimezone} from 'selectors/general';
|
||||
import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
|
||||
import {isSystemMessage} from 'mattermost-redux/utils/post_utils';
|
||||
import {GenericAction} from 'mattermost-redux/types/actions';
|
||||
import {setThreadFollow} from 'mattermost-redux/actions/threads';
|
||||
@@ -44,7 +44,6 @@ import {matchUserMentionTriggersWithMessageMentions} from 'utils/post_utils';
|
||||
|
||||
import {Post} from '@mattermost/types/posts';
|
||||
import {setGlobalItem} from '../../actions/storage';
|
||||
import {getGlobalItem} from '../../selectors/storage';
|
||||
|
||||
import DotMenu from './dot_menu';
|
||||
|
||||
@@ -110,8 +109,6 @@ function makeMapStateToProps() {
|
||||
}
|
||||
}
|
||||
|
||||
const showForwardPostNewLabel = getGlobalItem(state, Preferences.FORWARD_POST_VIEWED, true);
|
||||
|
||||
return {
|
||||
channelIsArchived: isArchivedChannel(channel),
|
||||
components: state.plugins.components,
|
||||
@@ -129,8 +126,7 @@ function makeMapStateToProps() {
|
||||
isCollapsedThreadsEnabled: collapsedThreads,
|
||||
threadReplyCount,
|
||||
isMobileView: getIsMobileView(state),
|
||||
showForwardPostNewLabel,
|
||||
timezone: getCurrentUserTimezone(state),
|
||||
timezone: getCurrentTimezone(state),
|
||||
isMilitaryTime,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ exports[`components/drafts/draft_actions/delete_draft_modal should have called o
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={true}
|
||||
autoCloseOnConfirmButton={true}
|
||||
bodyPadding={true}
|
||||
compassDesign={true}
|
||||
confirmButtonText="Yes, delete"
|
||||
enforceFocus={true}
|
||||
@@ -45,6 +46,7 @@ exports[`components/drafts/draft_actions/delete_draft_modal should have called o
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={true}
|
||||
autoCloseOnConfirmButton={true}
|
||||
bodyPadding={true}
|
||||
compassDesign={true}
|
||||
confirmButtonText="Yes, delete"
|
||||
enforceFocus={true}
|
||||
|
||||
@@ -4,6 +4,7 @@ exports[`components/drafts/draft_actions/send_draft_modal should have called onC
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={true}
|
||||
autoCloseOnConfirmButton={true}
|
||||
bodyPadding={true}
|
||||
compassDesign={true}
|
||||
confirmButtonText="Yes, send now"
|
||||
enforceFocus={true}
|
||||
@@ -44,6 +45,7 @@ exports[`components/drafts/draft_actions/send_draft_modal should have called onE
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={true}
|
||||
autoCloseOnConfirmButton={true}
|
||||
bodyPadding={true}
|
||||
compassDesign={true}
|
||||
confirmButtonText="Yes, send now"
|
||||
enforceFocus={true}
|
||||
|
||||
@@ -104,6 +104,10 @@
|
||||
padding: 12px 24px 24px 24px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
|
||||
&.divider {
|
||||
border-top: 1px solid rgba(63, 67, 80, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
&.GenericModal__compassDesign {
|
||||
@@ -112,10 +116,11 @@
|
||||
border-radius: 12px;
|
||||
|
||||
.modal-header {
|
||||
flex-direction: column;
|
||||
padding: 26px 32px 7px;
|
||||
|
||||
.close {
|
||||
top: 20px;
|
||||
top: 22px;
|
||||
right: 18px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@@ -133,7 +138,13 @@
|
||||
}
|
||||
|
||||
.GenericModal__header {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0;
|
||||
margin-right: 32px;
|
||||
border-top-left-radius: 12px;
|
||||
border-top-right-radius: 12px;
|
||||
|
||||
@@ -145,10 +156,12 @@
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 15px 32px 0;
|
||||
|
||||
.GenericModal__body {
|
||||
padding: 0;
|
||||
|
||||
&.padding {
|
||||
padding: 15px 32px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,5 +11,8 @@ import './generic_modal.scss';
|
||||
// find them there, so we've had to redefine them here
|
||||
t('generic_modal.cancel');
|
||||
t('generic_modal.confirm');
|
||||
t('footer_pagination.count');
|
||||
t('footer_pagination.prev');
|
||||
t('footer_pagination.next');
|
||||
|
||||
export default GenericModal;
|
||||
|
||||
@@ -152,14 +152,7 @@ exports[`components/global/product_switcher_menu should match snapshot with id 1
|
||||
"openedFrom": "product_menu",
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
dialogType={[Function]}
|
||||
icon={
|
||||
<Icon
|
||||
glyph="view-grid-plus-outline"
|
||||
@@ -360,14 +353,7 @@ exports[`components/global/product_switcher_menu should match snapshot with most
|
||||
"openedFrom": "product_menu",
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
dialogType={[Function]}
|
||||
icon={
|
||||
<Icon
|
||||
glyph="view-grid-plus-outline"
|
||||
@@ -688,14 +674,7 @@ exports[`components/global/product_switcher_menu should show integrations should
|
||||
"openedFrom": "product_menu",
|
||||
}
|
||||
}
|
||||
dialogType={
|
||||
Object {
|
||||
"$$typeof": Symbol(react.memo),
|
||||
"WrappedComponent": [Function],
|
||||
"compare": null,
|
||||
"type": [Function],
|
||||
}
|
||||
}
|
||||
dialogType={[Function]}
|
||||
icon={
|
||||
<Icon
|
||||
glyph="view-grid-plus-outline"
|
||||
|
||||
@@ -11,7 +11,7 @@ import {Permissions} from 'mattermost-redux/constants';
|
||||
import AboutBuildModal from 'components/about_build_modal';
|
||||
import SystemPermissionGate from 'components/permissions_gates/system_permission_gate';
|
||||
import TeamPermissionGate from 'components/permissions_gates/team_permission_gate';
|
||||
import MarketplaceModal from 'components/plugin_marketplace';
|
||||
import MarketplaceModal from 'components/plugin_marketplace/marketplace_modal';
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
import RestrictedIndicator from 'components/widgets/menu/menu_items/restricted_indicator';
|
||||
import {VisitSystemConsoleTour} from 'components/onboarding_tasks';
|
||||
|
||||
@@ -43,7 +43,7 @@ exports[`components/integrations/AbstractIncomingWebhook should call action func
|
||||
<input
|
||||
className="form-control"
|
||||
id="displayName"
|
||||
maxLength="64"
|
||||
maxLength={64}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value="testIncomingWebhook"
|
||||
@@ -76,7 +76,7 @@ exports[`components/integrations/AbstractIncomingWebhook should call action func
|
||||
<input
|
||||
className="form-control"
|
||||
id="description"
|
||||
maxLength="500"
|
||||
maxLength={500}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value="testing"
|
||||
@@ -107,8 +107,8 @@ exports[`components/integrations/AbstractIncomingWebhook should call action func
|
||||
className="col-md-5 col-sm-8"
|
||||
>
|
||||
<Connect(ChannelSelect)
|
||||
id="channelId"
|
||||
onChange={[Function]}
|
||||
selectDm={false}
|
||||
selectOpen={true}
|
||||
selectPrivate={true}
|
||||
value="88cxd9wpzpbpfp8pad78xj75pr"
|
||||
@@ -172,7 +172,7 @@ exports[`components/integrations/AbstractIncomingWebhook should call action func
|
||||
<input
|
||||
className="form-control"
|
||||
id="username"
|
||||
maxLength="22"
|
||||
maxLength={22}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value=""
|
||||
@@ -205,7 +205,7 @@ exports[`components/integrations/AbstractIncomingWebhook should call action func
|
||||
<input
|
||||
className="form-control"
|
||||
id="iconURL"
|
||||
maxLength="1024"
|
||||
maxLength={1024}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value=""
|
||||
@@ -304,7 +304,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot 1
|
||||
<input
|
||||
className="form-control"
|
||||
id="displayName"
|
||||
maxLength="64"
|
||||
maxLength={64}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value="testIncomingWebhook"
|
||||
@@ -337,7 +337,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot 1
|
||||
<input
|
||||
className="form-control"
|
||||
id="description"
|
||||
maxLength="500"
|
||||
maxLength={500}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value="testing"
|
||||
@@ -368,8 +368,8 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot 1
|
||||
className="col-md-5 col-sm-8"
|
||||
>
|
||||
<Connect(ChannelSelect)
|
||||
id="channelId"
|
||||
onChange={[Function]}
|
||||
selectDm={false}
|
||||
selectOpen={true}
|
||||
selectPrivate={true}
|
||||
value="88cxd9wpzpbpfp8pad78xj75pr"
|
||||
@@ -433,7 +433,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot 1
|
||||
<input
|
||||
className="form-control"
|
||||
id="username"
|
||||
maxLength="22"
|
||||
maxLength={22}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value=""
|
||||
@@ -466,7 +466,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot 1
|
||||
<input
|
||||
className="form-control"
|
||||
id="iconURL"
|
||||
maxLength="1024"
|
||||
maxLength={1024}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value=""
|
||||
@@ -565,7 +565,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
|
||||
<input
|
||||
className="form-control"
|
||||
id="displayName"
|
||||
maxLength="64"
|
||||
maxLength={64}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value=""
|
||||
@@ -598,7 +598,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
|
||||
<input
|
||||
className="form-control"
|
||||
id="description"
|
||||
maxLength="500"
|
||||
maxLength={500}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value=""
|
||||
@@ -629,8 +629,8 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
|
||||
className="col-md-5 col-sm-8"
|
||||
>
|
||||
<Connect(ChannelSelect)
|
||||
id="channelId"
|
||||
onChange={[Function]}
|
||||
selectDm={false}
|
||||
selectOpen={true}
|
||||
selectPrivate={true}
|
||||
value=""
|
||||
@@ -694,7 +694,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
|
||||
<input
|
||||
className="form-control"
|
||||
id="username"
|
||||
maxLength="22"
|
||||
maxLength={22}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value=""
|
||||
@@ -727,7 +727,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
|
||||
<input
|
||||
className="form-control"
|
||||
id="iconURL"
|
||||
maxLength="1024"
|
||||
maxLength={1024}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value=""
|
||||
@@ -829,7 +829,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
|
||||
<input
|
||||
className="form-control"
|
||||
id="displayName"
|
||||
maxLength="64"
|
||||
maxLength={64}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value="testIncomingWebhook"
|
||||
@@ -862,7 +862,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
|
||||
<input
|
||||
className="form-control"
|
||||
id="description"
|
||||
maxLength="500"
|
||||
maxLength={500}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value="testing"
|
||||
@@ -893,8 +893,8 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
|
||||
className="col-md-5 col-sm-8"
|
||||
>
|
||||
<Connect(ChannelSelect)
|
||||
id="channelId"
|
||||
onChange={[Function]}
|
||||
selectDm={false}
|
||||
selectOpen={true}
|
||||
selectPrivate={true}
|
||||
value="88cxd9wpzpbpfp8pad78xj75pr"
|
||||
@@ -958,7 +958,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
|
||||
<input
|
||||
className="form-control"
|
||||
id="username"
|
||||
maxLength="22"
|
||||
maxLength={22}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value=""
|
||||
@@ -1057,7 +1057,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
|
||||
<input
|
||||
className="form-control"
|
||||
id="displayName"
|
||||
maxLength="64"
|
||||
maxLength={64}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value="testIncomingWebhook"
|
||||
@@ -1090,7 +1090,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
|
||||
<input
|
||||
className="form-control"
|
||||
id="description"
|
||||
maxLength="500"
|
||||
maxLength={500}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value="testing"
|
||||
@@ -1121,8 +1121,8 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
|
||||
className="col-md-5 col-sm-8"
|
||||
>
|
||||
<Connect(ChannelSelect)
|
||||
id="channelId"
|
||||
onChange={[Function]}
|
||||
selectDm={false}
|
||||
selectOpen={true}
|
||||
selectPrivate={true}
|
||||
value="88cxd9wpzpbpfp8pad78xj75pr"
|
||||
@@ -1186,7 +1186,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
|
||||
<input
|
||||
className="form-control"
|
||||
id="iconURL"
|
||||
maxLength="1024"
|
||||
maxLength={1024}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value=""
|
||||
@@ -1285,7 +1285,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
|
||||
<input
|
||||
className="form-control"
|
||||
id="displayName"
|
||||
maxLength="64"
|
||||
maxLength={64}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value="testIncomingWebhook"
|
||||
@@ -1318,7 +1318,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
|
||||
<input
|
||||
className="form-control"
|
||||
id="description"
|
||||
maxLength="500"
|
||||
maxLength={500}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value="testing"
|
||||
@@ -1349,8 +1349,8 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
|
||||
className="col-md-5 col-sm-8"
|
||||
>
|
||||
<Connect(ChannelSelect)
|
||||
id="channelId"
|
||||
onChange={[Function]}
|
||||
selectDm={false}
|
||||
selectOpen={true}
|
||||
selectPrivate={true}
|
||||
value="88cxd9wpzpbpfp8pad78xj75pr"
|
||||
@@ -1414,7 +1414,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
|
||||
<input
|
||||
className="form-control"
|
||||
id="username"
|
||||
maxLength="22"
|
||||
maxLength={22}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value=""
|
||||
@@ -1447,7 +1447,7 @@ exports[`components/integrations/AbstractIncomingWebhook should match snapshot,
|
||||
<input
|
||||
className="form-control"
|
||||
id="iconURL"
|
||||
maxLength="1024"
|
||||
maxLength={1024}
|
||||
onChange={[Function]}
|
||||
type="text"
|
||||
value=""
|
||||
|
||||
@@ -4,10 +4,28 @@
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import ChannelSelect from 'components/channel_select';
|
||||
|
||||
import AbstractIncomingWebhook from 'components/integrations/abstract_incoming_webhook';
|
||||
import {Team} from '@mattermost/types/teams';
|
||||
|
||||
describe('components/integrations/AbstractIncomingWebhook', () => {
|
||||
const team = {name: 'team_name'};
|
||||
const team: Team = {id: 'team_id',
|
||||
create_at: 0,
|
||||
update_at: 0,
|
||||
delete_at: 0,
|
||||
display_name: 'team_name',
|
||||
name: 'team_name',
|
||||
description: 'team_description',
|
||||
email: 'team_email',
|
||||
type: 'I',
|
||||
company_name: 'team_company_name',
|
||||
allowed_domains: 'team_allowed_domains',
|
||||
invite_id: 'team_invite_id',
|
||||
allow_open_invite: false,
|
||||
scheme_id: 'team_scheme_id',
|
||||
group_constrained: false,
|
||||
};
|
||||
const header = {id: 'header_id', defaultMessage: 'Header'};
|
||||
const footer = {id: 'footer_id', defaultMessage: 'Footer'};
|
||||
const loading = {id: 'loading_id', defaultMessage: 'Loading'};
|
||||
@@ -16,6 +34,15 @@ describe('components/integrations/AbstractIncomingWebhook', () => {
|
||||
display_name: 'testIncomingWebhook',
|
||||
channel_id: '88cxd9wpzpbpfp8pad78xj75pr',
|
||||
description: 'testing',
|
||||
id: 'test_id',
|
||||
team_id: 'test_team_id',
|
||||
create_at: 0,
|
||||
update_at: 0,
|
||||
delete_at: 0,
|
||||
user_id: 'test_user_id',
|
||||
username: '',
|
||||
icon_url: '',
|
||||
channel_locked: false,
|
||||
};
|
||||
const enablePostUsernameOverride = true;
|
||||
const enablePostIconOverride = true;
|
||||
@@ -104,7 +131,7 @@ describe('components/integrations/AbstractIncomingWebhook', () => {
|
||||
};
|
||||
|
||||
const wrapper = shallow(<AbstractIncomingWebhook {...requiredProps}/>);
|
||||
wrapper.find('#channelId').simulate('change', evt);
|
||||
wrapper.find(ChannelSelect).simulate('change', evt);
|
||||
|
||||
expect(wrapper.state('channelId')).toBe(newChannelId);
|
||||
});
|
||||
|
||||
@@ -1,87 +1,100 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import React, {ChangeEventHandler, FormEvent, MouseEvent, PureComponent} from 'react';
|
||||
import {FormattedMessage, MessageDescriptor} from 'react-intl';
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
import BackstageHeader from 'components/backstage/components/backstage_header';
|
||||
import ChannelSelect from 'components/channel_select';
|
||||
import FormError from 'components/form_error';
|
||||
import SpinnerButton from 'components/spinner_button';
|
||||
import {Team} from '@mattermost/types/teams';
|
||||
import {localizeMessage} from 'utils/utils';
|
||||
import {IncomingWebhook} from '@mattermost/types/integrations';
|
||||
|
||||
export default class AbstractIncomingWebhook extends React.PureComponent {
|
||||
static propTypes = {
|
||||
interface State {
|
||||
displayName: string;
|
||||
description: string;
|
||||
channelId: string;
|
||||
channelLocked: boolean;
|
||||
username: string;
|
||||
iconURL: string;
|
||||
saving: boolean;
|
||||
serverError: string;
|
||||
clientError: JSX.Element | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current team
|
||||
*/
|
||||
team: PropTypes.object.isRequired,
|
||||
interface Props {
|
||||
|
||||
/**
|
||||
* The header text to render, has id and defaultMessage
|
||||
*/
|
||||
header: PropTypes.object.isRequired,
|
||||
/**
|
||||
* The current team
|
||||
*/
|
||||
team: Team;
|
||||
|
||||
/**
|
||||
* The footer text to render, has id and defaultMessage
|
||||
*/
|
||||
footer: PropTypes.object.isRequired,
|
||||
/**
|
||||
* The header text to render, has id and defaultMessage
|
||||
*/
|
||||
header: MessageDescriptor;
|
||||
|
||||
/**
|
||||
* The spinner loading text to render, has id and defaultMessage
|
||||
*/
|
||||
loading: PropTypes.object.isRequired,
|
||||
/**
|
||||
* The footer text to render, has id and defaultMessage
|
||||
*/
|
||||
footer: MessageDescriptor;
|
||||
|
||||
/**
|
||||
* The server error text after a failed action
|
||||
*/
|
||||
serverError: PropTypes.string.isRequired,
|
||||
/**
|
||||
* The spinner loading text to render, has id and defaultMessage
|
||||
*/
|
||||
loading: MessageDescriptor;
|
||||
|
||||
/**
|
||||
* The hook used to set the initial state
|
||||
*/
|
||||
initialHook: PropTypes.object,
|
||||
/**
|
||||
* The server error text after a failed action
|
||||
*/
|
||||
serverError: string;
|
||||
|
||||
/**
|
||||
* Whether to allow configuration of the default post username.
|
||||
*/
|
||||
enablePostUsernameOverride: PropTypes.bool.isRequired,
|
||||
/**
|
||||
* The hook used to set the initial state
|
||||
*/
|
||||
initialHook?: IncomingWebhook | Record<string, never>;
|
||||
|
||||
/**
|
||||
* Whether to allow configuration of the default post icon.
|
||||
*/
|
||||
enablePostIconOverride: PropTypes.bool.isRequired,
|
||||
/**
|
||||
* Whether to allow configuration of the default post username.
|
||||
*/
|
||||
enablePostUsernameOverride: boolean;
|
||||
|
||||
/**
|
||||
* The async function to run when the action button is pressed
|
||||
*/
|
||||
action: PropTypes.func.isRequired,
|
||||
}
|
||||
/**
|
||||
* Whether to allow configuration of the default post icon.
|
||||
*/
|
||||
enablePostIconOverride: boolean;
|
||||
|
||||
constructor(props) {
|
||||
/**
|
||||
* The async function to run when the action button is pressed
|
||||
*/
|
||||
action: (hook: IncomingWebhook) => Promise<void>;
|
||||
}
|
||||
|
||||
export default class AbstractIncomingWebhook extends PureComponent<Props, State> {
|
||||
constructor(props: Props | Readonly<Props>) {
|
||||
super(props);
|
||||
|
||||
this.state = this.getStateFromHook(this.props.initialHook || {});
|
||||
}
|
||||
|
||||
getStateFromHook = (hook) => {
|
||||
getStateFromHook = (hook: IncomingWebhook | Record<string, never>) => {
|
||||
return {
|
||||
displayName: hook.display_name || '',
|
||||
description: hook.description || '',
|
||||
channelId: hook.channel_id || '',
|
||||
channelLocked: hook.channel_locked || false,
|
||||
username: hook.username || '',
|
||||
iconURL: hook.icon_url || '',
|
||||
displayName: hook?.display_name || '',
|
||||
description: hook?.description || '',
|
||||
channelId: hook?.channel_id || '',
|
||||
channelLocked: hook?.channel_locked || false,
|
||||
username: hook?.username || '',
|
||||
iconURL: hook?.icon_url || '',
|
||||
saving: false,
|
||||
serverError: '',
|
||||
clientError: null,
|
||||
};
|
||||
}
|
||||
|
||||
handleSubmit = (e) => {
|
||||
handleSubmit = (e: MouseEvent<HTMLElement> | FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.state.saving) {
|
||||
@@ -91,7 +104,7 @@ export default class AbstractIncomingWebhook extends React.PureComponent {
|
||||
this.setState({
|
||||
saving: true,
|
||||
serverError: '',
|
||||
clientError: '',
|
||||
clientError: null,
|
||||
});
|
||||
|
||||
if (!this.state.channelId) {
|
||||
@@ -115,50 +128,56 @@ export default class AbstractIncomingWebhook extends React.PureComponent {
|
||||
description: this.state.description,
|
||||
username: this.state.username,
|
||||
icon_url: this.state.iconURL,
|
||||
id: this.props.initialHook?.id || '',
|
||||
create_at: this.props.initialHook?.create_at || 0,
|
||||
update_at: this.props.initialHook?.update_at || 0,
|
||||
delete_at: this.props.initialHook?.delete_at || 0,
|
||||
team_id: this.props.initialHook?.team_id || '',
|
||||
user_id: this.props.initialHook?.user_id || '',
|
||||
};
|
||||
|
||||
this.props.action(hook).then(() => this.setState({saving: false}));
|
||||
}
|
||||
|
||||
updateDisplayName = (e) => {
|
||||
updateDisplayName: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
this.setState({
|
||||
displayName: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
updateDescription = (e) => {
|
||||
updateDescription: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
this.setState({
|
||||
description: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
updateChannelId = (e) => {
|
||||
updateChannelId: ChangeEventHandler<HTMLSelectElement> = (e) => {
|
||||
this.setState({
|
||||
channelId: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
updateChannelLocked = (e) => {
|
||||
updateChannelLocked: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
this.setState({
|
||||
channelLocked: e.target.checked,
|
||||
});
|
||||
}
|
||||
|
||||
updateUsername = (e) => {
|
||||
updateUsername: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
this.setState({
|
||||
username: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
updateIconURL = (e) => {
|
||||
updateIconURL: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
this.setState({
|
||||
iconURL: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
var headerToRender = this.props.header;
|
||||
var footerToRender = this.props.footer;
|
||||
const headerToRender = this.props.header;
|
||||
const footerToRender = this.props.footer;
|
||||
|
||||
return (
|
||||
<div className='backstage-content'>
|
||||
@@ -177,7 +196,7 @@ export default class AbstractIncomingWebhook extends React.PureComponent {
|
||||
<div className='backstage-form'>
|
||||
<form
|
||||
className='form-horizontal'
|
||||
onSubmit={this.handleSubmit}
|
||||
onSubmit={(e) => this.handleSubmit(e)}
|
||||
>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
@@ -193,7 +212,7 @@ export default class AbstractIncomingWebhook extends React.PureComponent {
|
||||
<input
|
||||
id='displayName'
|
||||
type='text'
|
||||
maxLength='64'
|
||||
maxLength={64}
|
||||
className='form-control'
|
||||
value={this.state.displayName}
|
||||
onChange={this.updateDisplayName}
|
||||
@@ -220,7 +239,7 @@ export default class AbstractIncomingWebhook extends React.PureComponent {
|
||||
<input
|
||||
id='description'
|
||||
type='text'
|
||||
maxLength='500'
|
||||
maxLength={500}
|
||||
className='form-control'
|
||||
value={this.state.description}
|
||||
onChange={this.updateDescription}
|
||||
@@ -245,11 +264,11 @@ export default class AbstractIncomingWebhook extends React.PureComponent {
|
||||
</label>
|
||||
<div className='col-md-5 col-sm-8'>
|
||||
<ChannelSelect
|
||||
id='channelId'
|
||||
value={this.state.channelId}
|
||||
onChange={this.updateChannelId}
|
||||
selectOpen={true}
|
||||
selectPrivate={true}
|
||||
selectDm={false}
|
||||
/>
|
||||
<div className='form__help'>
|
||||
<FormattedMessage
|
||||
@@ -299,7 +318,7 @@ export default class AbstractIncomingWebhook extends React.PureComponent {
|
||||
<input
|
||||
id='username'
|
||||
type='text'
|
||||
maxLength='22'
|
||||
maxLength={22}
|
||||
className='form-control'
|
||||
value={this.state.username}
|
||||
onChange={this.updateUsername}
|
||||
@@ -328,7 +347,7 @@ export default class AbstractIncomingWebhook extends React.PureComponent {
|
||||
<input
|
||||
id='iconURL'
|
||||
type='text'
|
||||
maxLength='1024'
|
||||
maxLength={1024}
|
||||
className='form-control'
|
||||
value={this.state.iconURL}
|
||||
onChange={this.updateIconURL}
|
||||
@@ -360,8 +379,8 @@ export default class AbstractIncomingWebhook extends React.PureComponent {
|
||||
className='btn btn-primary'
|
||||
type='submit'
|
||||
spinning={this.state.saving}
|
||||
spinningText={localizeMessage(this.props.loading.id, this.props.loading.defaultMessage)}
|
||||
onClick={this.handleSubmit}
|
||||
spinningText={localizeMessage(this.props.loading.id as string, this.props.loading.defaultMessage as string)}
|
||||
onClick={(e) => this.handleSubmit(e)}
|
||||
id='saveWebhook'
|
||||
>
|
||||
<FormattedMessage
|
||||
@@ -249,6 +249,7 @@ export default class AddBot extends React.PureComponent<Props, State> {
|
||||
/>
|
||||
),
|
||||
});
|
||||
return;
|
||||
} else if (this.state.pictureFile.size > this.props.maxFileSize) {
|
||||
this.setState({
|
||||
error: (
|
||||
@@ -258,6 +259,7 @@ export default class AddBot extends React.PureComponent<Props, State> {
|
||||
/>
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,10 @@ button.InviteView__copyLink.tertiary-button:focus {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-guest {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import React, {useEffect, useMemo} from 'react';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import deepFreeze from 'mattermost-redux/utils/deep_freeze';
|
||||
import {Channel} from '@mattermost/types/channels';
|
||||
@@ -101,6 +102,7 @@ export default function InviteView(props: Props) {
|
||||
}, {inviteURL})
|
||||
}
|
||||
className='InviteView__copyLink'
|
||||
aria-live='polite'
|
||||
>
|
||||
{!copyText.copiedRecently && (
|
||||
<>
|
||||
@@ -259,8 +261,8 @@ export default function InviteView(props: Props) {
|
||||
)}
|
||||
<OverageUsersBannerNotice/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer className={'InviteView__footer ' + props.footerClass}>
|
||||
{copyButton}
|
||||
<Modal.Footer className={classNames('InviteView__footer', props.footerClass, {'InviteView__footer-guest': props.inviteType === InviteType.GUEST})}>
|
||||
{props.inviteType === InviteType.MEMBER && copyButton}
|
||||
<button
|
||||
disabled={!isInviteValid}
|
||||
onClick={props.invite}
|
||||
|
||||
@@ -4,6 +4,7 @@ exports[`components/new_channel_modal should match snapshot 1`] = `
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={true}
|
||||
autoCloseOnConfirmButton={false}
|
||||
bodyPadding={true}
|
||||
cancelButtonText="Cancel"
|
||||
className="new-channel-modal"
|
||||
compassDesign={true}
|
||||
|
||||
4
webapp/channels/src/components/payment_form/mixins.scss
Обычный файл
4
webapp/channels/src/components/payment_form/mixins.scss
Обычный файл
@@ -0,0 +1,4 @@
|
||||
@mixin payment-form-padding() {
|
||||
padding-right: 96px;
|
||||
padding-left: 96px;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
@import './mixins';
|
||||
|
||||
.PaymentForm {
|
||||
padding-right: 96px;
|
||||
padding-left: 96px;
|
||||
@include payment-form-padding;
|
||||
|
||||
margin: 0 auto;
|
||||
|
||||
.form-row {
|
||||
|
||||
@@ -25,4 +25,5 @@ function devConfirmCardSetup(confirmCardSetup: ConfirmCardSetupType): ConfirmCar
|
||||
export const getConfirmCardSetup = (isDevMode?: boolean) => (isDevMode ? devConfirmCardSetup : prodConfirmCardSetup);
|
||||
|
||||
export const STRIPE_CSS_SRC = 'https://fonts.googleapis.com/css?family=Open+Sans:400,400i,600,600i&display=swap';
|
||||
export const STRIPE_PUBLIC_KEY = 'pk_test_ttEpW6dCHksKyfAFzh6MvgBj';
|
||||
//eslint-disable-next-line no-process-env
|
||||
export const STRIPE_PUBLIC_KEY = process.env.STRIPE_PUBLIC_KEY || 'pk_test_ttEpW6dCHksKyfAFzh6MvgBj';
|
||||
|
||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -1,58 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators, Dispatch, ActionCreatorsMapObject} from 'redux';
|
||||
|
||||
import {GenericAction, ActionFunc} from 'mattermost-redux/types/actions';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
import {getListing, getInstalledListing} from 'selectors/views/marketplace';
|
||||
import {setFirstAdminVisitMarketplaceStatus} from 'mattermost-redux/actions/general';
|
||||
import {getPluginStatuses} from 'mattermost-redux/actions/admin';
|
||||
import {getFirstAdminVisitMarketplaceStatus} from 'mattermost-redux/selectors/entities/general';
|
||||
|
||||
import {makeAsyncComponent} from 'components/async_load';
|
||||
|
||||
import {isModalOpen} from 'selectors/views/modals';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
import {getSiteURL} from 'utils/url';
|
||||
|
||||
import {closeModal} from 'actions/views/modals';
|
||||
import {fetchListing, filterListing} from 'actions/marketplace';
|
||||
|
||||
const MarketplaceModal = makeAsyncComponent('MarketplaceModal', React.lazy(() => import('./marketplace_modal')));
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
return {
|
||||
show: isModalOpen(state, ModalIdentifiers.PLUGIN_MARKETPLACE),
|
||||
listing: getListing(state),
|
||||
installedListing: getInstalledListing(state),
|
||||
siteURL: getSiteURL(),
|
||||
pluginStatuses: state.entities.admin.pluginStatuses,
|
||||
firstAdminVisitMarketplaceStatus: getFirstAdminVisitMarketplaceStatus(state),
|
||||
};
|
||||
}
|
||||
|
||||
type Actions = {
|
||||
closeModal(): void;
|
||||
fetchListing(localOnly?: boolean): Promise<{error?: Error}>;
|
||||
filterListing(filter: string): Promise<{error?: Error}>;
|
||||
setFirstAdminVisitMarketplaceStatus(): Promise<void>;
|
||||
getPluginStatuses(): Promise<void>;
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
||||
return {
|
||||
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc | GenericAction>, Actions>({
|
||||
closeModal: () => closeModal(ModalIdentifiers.PLUGIN_MARKETPLACE),
|
||||
fetchListing,
|
||||
filterListing,
|
||||
setFirstAdminVisitMarketplaceStatus,
|
||||
getPluginStatuses,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(MarketplaceModal);
|
||||
@@ -11,7 +11,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render 1`] = `
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="app-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -50,7 +50,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render installed a
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-outline"
|
||||
className="app-installed"
|
||||
disabled={true}
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
@@ -83,7 +83,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render with empty
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="app-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -123,7 +123,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render with icon 1
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="app-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -164,7 +164,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render with no hom
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="app-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -202,7 +202,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render with no plu
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="app-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -240,7 +240,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render with one la
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="app-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -288,7 +288,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render with server
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="app-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -328,7 +328,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render with two la
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="app-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -381,7 +381,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem when installing 1`] = `
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="app-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
|
||||
@@ -50,7 +50,7 @@ export default class MarketplaceItemApp extends React.PureComponent <Marketplace
|
||||
if (this.props.installed && !this.props.installing && !this.props.error) {
|
||||
return (
|
||||
<button
|
||||
className='btn btn-outline'
|
||||
className='app-installed'
|
||||
disabled={true}
|
||||
>
|
||||
<FormattedMessage
|
||||
@@ -81,7 +81,7 @@ export default class MarketplaceItemApp extends React.PureComponent <Marketplace
|
||||
return (
|
||||
<button
|
||||
onClick={this.onInstall}
|
||||
className='btn btn-primary'
|
||||
className='app-install always-show-enabled'
|
||||
disabled={this.props.installing}
|
||||
>
|
||||
<LoadingWrapper
|
||||
|
||||
@@ -11,7 +11,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render 1`] = `
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="plugin-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -78,7 +78,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render installe
|
||||
to="/admin_console/plugins/plugin_id"
|
||||
>
|
||||
<button
|
||||
className="btn btn-outline"
|
||||
className="plugin-configure"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
@@ -137,7 +137,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with emp
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="plugin-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -202,7 +202,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with no
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="plugin-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -265,7 +265,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with no
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="plugin-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -328,7 +328,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with no
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="plugin-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -390,7 +390,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with one
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="plugin-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -463,7 +463,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with plu
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="plugin-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -540,7 +540,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with ser
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="plugin-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -605,7 +605,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with two
|
||||
}
|
||||
button={
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
className="plugin-install always-show-enabled"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
>
|
||||
@@ -686,7 +686,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with upd
|
||||
to="/admin_console/plugins/plugin_id"
|
||||
>
|
||||
<button
|
||||
className="btn btn-outline"
|
||||
className="plugin-configure"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
@@ -751,7 +751,7 @@ exports[`components/MarketplaceItemPlugin MarketplaceItem should render with upd
|
||||
to="/admin_console/plugins/plugin_id"
|
||||
>
|
||||
<button
|
||||
className="btn btn-outline"
|
||||
className="plugin-configure"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
|
||||
@@ -294,7 +294,7 @@ export default class MarketplaceItemPlugin extends React.PureComponent <Marketpl
|
||||
>
|
||||
<button
|
||||
onClick={this.onConfigure}
|
||||
className='btn btn-outline'
|
||||
className='plugin-configure'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='marketplace_modal.list.configure'
|
||||
@@ -325,7 +325,7 @@ export default class MarketplaceItemPlugin extends React.PureComponent <Marketpl
|
||||
return (
|
||||
<button
|
||||
onClick={this.onInstall}
|
||||
className='btn btn-primary'
|
||||
className='plugin-install always-show-enabled'
|
||||
disabled={this.props.installing}
|
||||
>
|
||||
<LoadingWrapper
|
||||
|
||||
@@ -1,150 +1,14 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/marketplace/marketplace_list should render with multiple plugins 1`] = `
|
||||
exports[`components/marketplace/marketplace_list should render default 1`] = `
|
||||
<div
|
||||
className="more-modal__list"
|
||||
className="no_plugins"
|
||||
>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
<PluginIcon
|
||||
className="icon__plugin"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(MarketplaceItemPlugin)
|
||||
description="This plugin sends quarterly user satisfaction surveys to gather feedback and help improve Mattermost"
|
||||
homepageUrl="https://github.com/mattermost/mattermost-plugin-nps"
|
||||
id="com.mattermost.nps"
|
||||
installedVersion=""
|
||||
key="com.mattermost.nps"
|
||||
name="User Satisfaction Surveys"
|
||||
version="1.0.3"
|
||||
/>
|
||||
<Connect(NavigationRow)
|
||||
maximumPerPage={15}
|
||||
onNextPageButtonClick={[Function]}
|
||||
onPreviousPageButtonClick={[Function]}
|
||||
page={0}
|
||||
total={17}
|
||||
<div
|
||||
className="no_plugins__message"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -8,8 +8,7 @@ import {AuthorType, MarketplacePlugin, ReleaseStage} from '@mattermost/types/mar
|
||||
|
||||
import MarketplaceItem from '../marketplace_item/marketplace_item_plugin';
|
||||
|
||||
import MarketplaceList from './marketplace_list';
|
||||
import NavigationRow from './navigation_row';
|
||||
import MarketplaceList, {ITEMS_PER_PAGE} from './marketplace_list';
|
||||
|
||||
describe('components/marketplace/marketplace_list', () => {
|
||||
const samplePlugin: MarketplacePlugin = {
|
||||
@@ -28,8 +27,20 @@ describe('components/marketplace/marketplace_list', () => {
|
||||
installed_version: '',
|
||||
};
|
||||
|
||||
it('should render with multiple plugins', () => {
|
||||
const wrapper = shallow<MarketplaceList>(
|
||||
it('should render default', () => {
|
||||
const wrapper = shallow(
|
||||
<MarketplaceList
|
||||
listing={[]}
|
||||
page={0}
|
||||
noResultsMessage=''
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render page with ITEMS_PER_PAGE plugins', () => {
|
||||
const wrapper = shallow(
|
||||
<MarketplaceList
|
||||
listing={[
|
||||
samplePlugin, samplePlugin, samplePlugin, samplePlugin, samplePlugin,
|
||||
@@ -37,28 +48,29 @@ describe('components/marketplace/marketplace_list', () => {
|
||||
samplePlugin, samplePlugin, samplePlugin, samplePlugin, samplePlugin,
|
||||
samplePlugin, samplePlugin,
|
||||
]}
|
||||
page={0}
|
||||
noResultsMessage=''
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
expect(wrapper.state().page).toEqual(0);
|
||||
expect(wrapper.find(MarketplaceItem)).toHaveLength(15);
|
||||
expect(wrapper.find(NavigationRow)).toHaveLength(1);
|
||||
expect(wrapper.find(NavigationRow).props().page).toEqual(0);
|
||||
expect(wrapper.find(NavigationRow).props().total).toEqual(17);
|
||||
expect(wrapper.find(NavigationRow).props().maximumPerPage).toEqual(15);
|
||||
expect(wrapper.find(MarketplaceItem)).toHaveLength(ITEMS_PER_PAGE);
|
||||
});
|
||||
|
||||
it('should set page to 0 when list of plugins changed', () => {
|
||||
const wrapper = shallow<MarketplaceList>(
|
||||
it('should render no results', () => {
|
||||
const wrapper = shallow(
|
||||
<MarketplaceList
|
||||
listing={[samplePlugin, samplePlugin]}
|
||||
listing={[]}
|
||||
page={0}
|
||||
noResultsMessage='No plugins available'
|
||||
noResultsAction={{
|
||||
label: 'action',
|
||||
onClick: jest.fn(),
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
wrapper.setState({page: 10});
|
||||
wrapper.setProps({listing: [samplePlugin]});
|
||||
|
||||
expect(wrapper.state().page).toEqual(0);
|
||||
expect(wrapper.find('.icon__plugin').length).toEqual(1);
|
||||
expect(wrapper.find('.no_plugins__message').length).toEqual(1);
|
||||
expect(wrapper.find('.no_plugins__action').length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,107 +1,116 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import type {MarketplaceApp, MarketplacePlugin} from '@mattermost/types/marketplace';
|
||||
import {isPlugin, getName} from 'mattermost-redux/utils/marketplace';
|
||||
|
||||
import PluginIcon from 'components/widgets/icons/plugin_icon';
|
||||
|
||||
import MarketplaceItemPlugin from '../marketplace_item/marketplace_item_plugin';
|
||||
import MarketplaceItemApp from '../marketplace_item/marketplace_item_app';
|
||||
|
||||
import NavigationRow from './navigation_row';
|
||||
|
||||
const ITEMS_PER_PAGE = 15;
|
||||
export const ITEMS_PER_PAGE = 15;
|
||||
|
||||
type MarketplaceListProps = {
|
||||
listing: Array<MarketplacePlugin | MarketplaceApp>;
|
||||
};
|
||||
|
||||
type MarketplaceListState = {
|
||||
page: number;
|
||||
noResultsMessage: string;
|
||||
noResultsAction?: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
filter?: string;
|
||||
listRef?: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export default class MarketplaceList extends React.PureComponent <MarketplaceListProps, MarketplaceListState> {
|
||||
static getDerivedStateFromProps(props: MarketplaceListProps, state: MarketplaceListState): MarketplaceListState | null {
|
||||
if (state.page > 0 && props.listing.length < ITEMS_PER_PAGE) {
|
||||
return {page: 0};
|
||||
const MarketplaceList = ({
|
||||
listing,
|
||||
page,
|
||||
noResultsMessage,
|
||||
noResultsAction,
|
||||
filter,
|
||||
listRef,
|
||||
}: MarketplaceListProps) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const pageItems = useMemo(() => {
|
||||
if (listing.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
constructor(props: MarketplaceListProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
page: 0,
|
||||
};
|
||||
}
|
||||
|
||||
nextPage = (): void => {
|
||||
this.setState((state) => ({
|
||||
page: state.page + 1,
|
||||
}));
|
||||
};
|
||||
|
||||
previousPage = (): void => {
|
||||
this.setState((state) => ({
|
||||
page: state.page - 1,
|
||||
}));
|
||||
};
|
||||
|
||||
render(): JSX.Element {
|
||||
const pageStart = this.state.page * ITEMS_PER_PAGE;
|
||||
const pageStart = page * ITEMS_PER_PAGE;
|
||||
const pageEnd = pageStart + ITEMS_PER_PAGE;
|
||||
|
||||
this.props.listing.sort((a, b) => {
|
||||
return getName(a).localeCompare(getName(b));
|
||||
});
|
||||
return [...listing].
|
||||
sort((a, b) => getName(a).localeCompare(getName(b))).
|
||||
slice(pageStart, pageEnd).
|
||||
map((i) => (
|
||||
isPlugin(i) ? (
|
||||
<MarketplaceItemPlugin
|
||||
key={i.manifest.id}
|
||||
id={i.manifest.id}
|
||||
name={i.manifest.name}
|
||||
description={i.manifest.description}
|
||||
version={i.manifest.version}
|
||||
homepageUrl={i.homepage_url}
|
||||
releaseNotesUrl={i.release_notes_url}
|
||||
labels={i.labels}
|
||||
iconData={i.icon_data}
|
||||
installedVersion={i.installed_version}
|
||||
/>
|
||||
) : (
|
||||
<MarketplaceItemApp
|
||||
key={i.manifest.app_id}
|
||||
id={i.manifest.app_id}
|
||||
name={i.manifest.display_name}
|
||||
description={i.manifest.description}
|
||||
homepageUrl={i.manifest.homepage_url}
|
||||
iconURL={i.icon_url}
|
||||
installed={i.installed}
|
||||
labels={i.labels}
|
||||
/>
|
||||
)
|
||||
));
|
||||
}, [listing, page]);
|
||||
|
||||
const itemsToDisplay = this.props.listing.slice(pageStart, pageEnd);
|
||||
const getNoResultsMessage = useCallback(() => (
|
||||
filter ? (
|
||||
formatMessage(
|
||||
{id: 'marketplace_modal_list.no_plugins_filter', defaultMessage: 'No results for "{filter}"'},
|
||||
{filter},
|
||||
)
|
||||
) : (
|
||||
noResultsMessage
|
||||
)
|
||||
), [filter, noResultsMessage]);
|
||||
|
||||
return (
|
||||
<div className='more-modal__list'>
|
||||
{itemsToDisplay.map((i) => {
|
||||
if (isPlugin(i)) {
|
||||
return (
|
||||
<MarketplaceItemPlugin
|
||||
key={i.manifest.id}
|
||||
id={i.manifest.id}
|
||||
name={i.manifest.name}
|
||||
description={i.manifest.description}
|
||||
version={i.manifest.version}
|
||||
homepageUrl={i.homepage_url}
|
||||
releaseNotesUrl={i.release_notes_url}
|
||||
labels={i.labels}
|
||||
iconData={i.icon_data}
|
||||
installedVersion={i.installed_version}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MarketplaceItemApp
|
||||
key={i.manifest.app_id}
|
||||
id={i.manifest.app_id}
|
||||
name={i.manifest.display_name}
|
||||
description={i.manifest.description}
|
||||
homepageUrl={i.manifest.homepage_url}
|
||||
iconURL={i.icon_url}
|
||||
installed={i.installed}
|
||||
labels={i.labels}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
<NavigationRow
|
||||
page={this.state.page}
|
||||
total={this.props.listing.length}
|
||||
maximumPerPage={ITEMS_PER_PAGE}
|
||||
onNextPageButtonClick={this.nextPage}
|
||||
onPreviousPageButtonClick={this.previousPage}
|
||||
/>
|
||||
return (listing.length === 0 ? (
|
||||
<div className='no_plugins'>
|
||||
<PluginIcon className='icon__plugin'/>
|
||||
<div className='no_plugins__message'>
|
||||
{getNoResultsMessage()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
{noResultsAction && (
|
||||
<button
|
||||
className='no_plugins__action'
|
||||
onClick={noResultsAction.onClick}
|
||||
data-testid='Install-Plugins-button'
|
||||
>
|
||||
{noResultsAction.label}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
ref={listRef}
|
||||
className='more-modal__list'
|
||||
>
|
||||
{pageItems}
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
export default MarketplaceList;
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/marketplace/navigation_row should not render any buttons 1`] = `
|
||||
<div
|
||||
className="navigation-row"
|
||||
>
|
||||
<div
|
||||
className="col-xs-2"
|
||||
/>
|
||||
<div
|
||||
className="col-xs-8 count"
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.6)",
|
||||
}
|
||||
}
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="{startCount, number} - {endCount, number} {total, plural, one {plugin} other {plugins}} of {total, number} total"
|
||||
id="marketplace_list.count_total_page"
|
||||
values={
|
||||
Object {
|
||||
"endCount": 15,
|
||||
"startCount": 1,
|
||||
"total": 15,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="col-xs-2"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/marketplace/navigation_row should render next and previous buttons 1`] = `
|
||||
<div
|
||||
className="navigation-row"
|
||||
>
|
||||
<div
|
||||
className="col-xs-2"
|
||||
>
|
||||
<NavigationButton
|
||||
defaultMessage="Previous"
|
||||
messageId="more_channels.prev"
|
||||
onClick={[MockFunction]}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="col-xs-8 count"
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.6)",
|
||||
}
|
||||
}
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="{startCount, number} - {endCount, number} {total, plural, one {plugin} other {plugins}} of {total, number} total"
|
||||
id="marketplace_list.count_total_page"
|
||||
values={
|
||||
Object {
|
||||
"endCount": 30,
|
||||
"startCount": 16,
|
||||
"total": 32,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="col-xs-2"
|
||||
>
|
||||
<NavigationButton
|
||||
defaultMessage="Next"
|
||||
messageId="more_channels.next"
|
||||
onClick={[MockFunction]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/marketplace/navigation_row should render only next button 1`] = `
|
||||
<div
|
||||
className="navigation-row"
|
||||
>
|
||||
<div
|
||||
className="col-xs-2"
|
||||
/>
|
||||
<div
|
||||
className="col-xs-8 count"
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.6)",
|
||||
}
|
||||
}
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="{startCount, number} - {endCount, number} {total, plural, one {plugin} other {plugins}} of {total, number} total"
|
||||
id="marketplace_list.count_total_page"
|
||||
values={
|
||||
Object {
|
||||
"endCount": 15,
|
||||
"startCount": 1,
|
||||
"total": 32,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="col-xs-2"
|
||||
>
|
||||
<NavigationButton
|
||||
defaultMessage="Next"
|
||||
messageId="more_channels.next"
|
||||
onClick={[MockFunction]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/marketplace/navigation_row should render only previous button 1`] = `
|
||||
<div
|
||||
className="navigation-row"
|
||||
>
|
||||
<div
|
||||
className="col-xs-2"
|
||||
>
|
||||
<NavigationButton
|
||||
defaultMessage="Previous"
|
||||
messageId="more_channels.prev"
|
||||
onClick={[MockFunction]}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="col-xs-8 count"
|
||||
style={
|
||||
Object {
|
||||
"color": "rgba(255,255,255,0.6)",
|
||||
}
|
||||
}
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="{startCount, number} - {endCount, number} {total, plural, one {plugin} other {plugins}} of {total, number} total"
|
||||
id="marketplace_list.count_total_page"
|
||||
values={
|
||||
Object {
|
||||
"endCount": 32,
|
||||
"startCount": 31,
|
||||
"total": 32,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="col-xs-2"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,17 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
import NavigationRow from './navigation_row';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
return {
|
||||
theme: getTheme(state),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(NavigationRow);
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
type NavigationButtonProps = {
|
||||
onClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
messageId: string;
|
||||
defaultMessage: string;
|
||||
};
|
||||
|
||||
export default class NavigationButton extends React.PureComponent <NavigationButtonProps> {
|
||||
onClick = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>): void => {
|
||||
event.preventDefault();
|
||||
this.props.onClick(event);
|
||||
};
|
||||
|
||||
render(): JSX.Element {
|
||||
const {onClick, messageId, defaultMessage} = this.props;
|
||||
return (
|
||||
<button
|
||||
className='btn btn-link'
|
||||
onClick={onClick}
|
||||
>
|
||||
<FormattedMessage
|
||||
id={messageId}
|
||||
defaultMessage={defaultMessage}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
|
||||
import NavigationRow, {NavigationRowProps} from './navigation_row';
|
||||
|
||||
describe('components/marketplace/navigation_row', () => {
|
||||
const baseProps: NavigationRowProps = {
|
||||
page: 0,
|
||||
total: 32,
|
||||
maximumPerPage: 15,
|
||||
onNextPageButtonClick: jest.fn(),
|
||||
onPreviousPageButtonClick: jest.fn(),
|
||||
theme: {centerChannelColor: '#fff'} as Theme,
|
||||
};
|
||||
|
||||
it('should render only next button', () => {
|
||||
const wrapper = shallow<NavigationRow>(
|
||||
<NavigationRow {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find('NavigationButton')).toHaveLength(1);
|
||||
|
||||
wrapper.find('NavigationButton').simulate('click', {preventDefault: jest.fn});
|
||||
|
||||
expect(wrapper.instance().props.onNextPageButtonClick).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.instance().props.onPreviousPageButtonClick).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should render next and previous buttons', () => {
|
||||
const props = {...baseProps, page: 1};
|
||||
const wrapper = shallow<NavigationRow>(
|
||||
<NavigationRow{...props}/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find('NavigationButton')).toHaveLength(2);
|
||||
|
||||
wrapper.find('NavigationButton').at(0).simulate('click', {preventDefault: jest.fn});
|
||||
wrapper.find('NavigationButton').at(1).simulate('click', {preventDefault: jest.fn});
|
||||
|
||||
expect(wrapper.instance().props.onNextPageButtonClick).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.instance().props.onPreviousPageButtonClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should render only previous button', () => {
|
||||
const props = {...baseProps, page: 2};
|
||||
const wrapper = shallow<NavigationRow>(
|
||||
<NavigationRow{...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find('NavigationButton')).toHaveLength(1);
|
||||
|
||||
wrapper.find('NavigationButton').simulate('click', {preventDefault: jest.fn});
|
||||
|
||||
expect(wrapper.instance().props.onNextPageButtonClick).toHaveBeenCalledTimes(0);
|
||||
expect(wrapper.instance().props.onPreviousPageButtonClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not render any buttons', () => {
|
||||
const props = {...baseProps, page: 0, total: 15};
|
||||
const wrapper = shallow<NavigationRow>(
|
||||
<NavigationRow{...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(wrapper.find('NavigationButton')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -1,87 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {changeOpacity, makeStyleFromTheme} from 'mattermost-redux/utils/theme_utils';
|
||||
|
||||
import NavigationButton from './navigation_button';
|
||||
|
||||
export type NavigationRowProps = {
|
||||
page: number;
|
||||
total: number;
|
||||
maximumPerPage: number;
|
||||
onNextPageButtonClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
onPreviousPageButtonClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
theme: Theme;
|
||||
};
|
||||
|
||||
export default class NavigationRow extends React.PureComponent <NavigationRowProps> {
|
||||
canShowNextButton = (): boolean => {
|
||||
const {page, maximumPerPage, total} = this.props;
|
||||
const totalPages = Math.trunc((total - 1) / maximumPerPage);
|
||||
|
||||
return totalPages > page;
|
||||
};
|
||||
|
||||
renderCount = (): JSX.Element => {
|
||||
const {page, total, maximumPerPage} = this.props;
|
||||
const startCount = page * maximumPerPage;
|
||||
const endCount = Math.min(startCount + maximumPerPage, total);
|
||||
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='marketplace_list.count_total_page'
|
||||
defaultMessage='{startCount, number} - {endCount, number} {total, plural, one {plugin} other {plugins}} of {total, number} total'
|
||||
values={{
|
||||
startCount: startCount + 1,
|
||||
endCount,
|
||||
total,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render(): JSX.Element {
|
||||
const style = getStyle(this.props.theme);
|
||||
|
||||
return (
|
||||
<div className='navigation-row'>
|
||||
<div className='col-xs-2'>
|
||||
{(this.props.page > 0) && (
|
||||
<NavigationButton
|
||||
onClick={this.props.onPreviousPageButtonClick}
|
||||
messageId={'more_channels.prev'}
|
||||
defaultMessage={'Previous'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className='col-xs-8 count'
|
||||
style={style.count}
|
||||
>
|
||||
{this.renderCount()}
|
||||
</div>
|
||||
<div className='col-xs-2'>
|
||||
{this.canShowNextButton() && (
|
||||
<NavigationButton
|
||||
onClick={this.props.onNextPageButtonClick}
|
||||
messageId={'more_channels.next'}
|
||||
defaultMessage={'Next'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyle = makeStyleFromTheme((theme) => {
|
||||
return {
|
||||
count: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -1,21 +1,11 @@
|
||||
@import 'utils/variables';
|
||||
@import 'utils/mixins';
|
||||
|
||||
.modal-marketplace {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 50px;
|
||||
color: var(--center-channel-color);
|
||||
font-size: 16px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
.marketplace-modal {
|
||||
width: 800px;
|
||||
|
||||
div.navigation-row {
|
||||
overflow: unset;
|
||||
margin-top: 10px;
|
||||
|
||||
div {
|
||||
@@ -29,7 +19,7 @@
|
||||
}
|
||||
|
||||
div.count {
|
||||
padding-top: 8px;
|
||||
padding: 9px 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,33 +35,47 @@
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
font-size: 14px;
|
||||
padding: 0 32px;
|
||||
margin: 0 0 8px;
|
||||
|
||||
> li {
|
||||
> a {
|
||||
padding: 10px 16px;
|
||||
li {
|
||||
margin-right: 0;
|
||||
|
||||
a {
|
||||
padding: 13px 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: none;
|
||||
color: var(--center-channel-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.active > a {
|
||||
color: var(--center-channel-color);
|
||||
&.active {
|
||||
border-bottom: 2px solid var(--denim-button-bg);
|
||||
|
||||
a {
|
||||
color: var(--denim-button-bg);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 8px 0 24px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: 300;
|
||||
|
||||
@@ -81,8 +85,14 @@
|
||||
}
|
||||
|
||||
.more-modal__list {
|
||||
height: 390px;
|
||||
margin: 0 6px 8px 12px;
|
||||
overflow-y: scroll;
|
||||
|
||||
.more-modal__row {
|
||||
align-items: normal;
|
||||
min-height: 80px;
|
||||
padding: 16px 20px;
|
||||
border-bottom: none;
|
||||
|
||||
.marketplace__tag {
|
||||
margin-left: 6px;
|
||||
@@ -94,25 +104,91 @@
|
||||
margin: 10px 10px 0 0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.more-modal__details {
|
||||
padding-left: 16px;
|
||||
|
||||
.more-modal__row--link {
|
||||
color: var(--center-channel-color);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.more-modal__description {
|
||||
margin: 2px 0 0;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.more-modal__actions {
|
||||
padding-left: 16px;
|
||||
margin: 0;
|
||||
|
||||
.plugin-configure,
|
||||
.app-installed {
|
||||
@include secondary-button;
|
||||
@include button-medium;
|
||||
}
|
||||
|
||||
.plugin-install,
|
||||
.app-install {
|
||||
@include primary-button;
|
||||
@include button-medium;
|
||||
}
|
||||
|
||||
a {
|
||||
&:hover,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.more-modal__description {
|
||||
margin: 2px 0 0;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.icon__plugin {
|
||||
display: flex;
|
||||
height: 48px;
|
||||
flex: 0 0 48px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: $white;
|
||||
border-radius: 50%;
|
||||
|
||||
padding-bottom: 80px;
|
||||
svg,
|
||||
img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: var(--button-bg);
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search_input {
|
||||
width: 720px;
|
||||
height: 40px;
|
||||
flex: 1;
|
||||
margin-top: 28px;
|
||||
margin-right: 16px;
|
||||
margin-bottom: 20px;
|
||||
margin-left: 16px;
|
||||
box-shadow: none;
|
||||
.marketplace-modal-search {
|
||||
padding: 24px 0 0;
|
||||
|
||||
.search_input {
|
||||
width: 100%;
|
||||
border: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
@@ -130,8 +206,9 @@
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
width: 720px;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
@@ -160,67 +237,41 @@
|
||||
}
|
||||
}
|
||||
|
||||
.icon__plugin {
|
||||
.no_plugins {
|
||||
display: flex;
|
||||
height: 42px;
|
||||
flex: 0 0 42px;
|
||||
height: 390px;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 4px;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 8px;
|
||||
|
||||
svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
fill: var(--button-bg);
|
||||
&__message {
|
||||
margin-top: 20px;
|
||||
color: var(--center-channel-color);
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon__plugin--background {
|
||||
padding: 6px;
|
||||
background-color: $white;
|
||||
&__action {
|
||||
@include primary-button;
|
||||
@include button-medium;
|
||||
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.no_plugins_div {
|
||||
text-align: center;
|
||||
.icon__plugin {
|
||||
svg {
|
||||
fill: var(--button-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item_error {
|
||||
background-color: rgba(var(--error-text-rgb), 0.08);
|
||||
background-color: rgba(var(--error-text-color-rgb), 0.08);
|
||||
}
|
||||
|
||||
.error_text {
|
||||
color: var(--error-text);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.error-bar {
|
||||
position: fixed;
|
||||
z-index: 8;
|
||||
top: 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
min-height: $announcement-bar-height;
|
||||
max-height: $announcement-bar-height;
|
||||
padding: 5px 30px;
|
||||
background-color: var(--center-channel-bg);
|
||||
color: var(--error-text);
|
||||
|
||||
.error-bar__content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(var(--error-text-rgb), 0.12);
|
||||
.loading {
|
||||
height: 390px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,19 +5,21 @@ import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
|
||||
import {AuthorType, MarketplacePlugin, ReleaseStage} from '@mattermost/types/marketplace';
|
||||
import type {PluginStatusRedux} from '@mattermost/types/plugins';
|
||||
|
||||
import {trackEvent} from 'actions/telemetry_actions.jsx';
|
||||
import {ActionFunc} from 'mattermost-redux/types/actions';
|
||||
|
||||
import MarketplaceModal, {AllListing, InstalledListing, MarketplaceModalProps} from './marketplace_modal';
|
||||
import {GlobalState} from 'types/store';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
jest.mock('actions/telemetry_actions.jsx', () => {
|
||||
const original = jest.requireActual('actions/telemetry_actions.jsx');
|
||||
return {
|
||||
...original,
|
||||
trackEvent: jest.fn(),
|
||||
};
|
||||
});
|
||||
import MarketplaceModal, {OpenedFromType} from './marketplace_modal';
|
||||
|
||||
let mockState: GlobalState;
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux') as typeof import('react-redux'),
|
||||
useSelector: (selector: (state: typeof mockState) => unknown) => selector(mockState),
|
||||
useDispatch: jest.fn(() => (action: ActionFunc) => action),
|
||||
}));
|
||||
|
||||
describe('components/marketplace/', () => {
|
||||
const samplePlugin: MarketplacePlugin = {
|
||||
@@ -52,161 +54,106 @@ describe('components/marketplace/', () => {
|
||||
installed_version: '1.0.3',
|
||||
};
|
||||
|
||||
describe('AllListing', () => {
|
||||
it('should render with no plugins', () => {
|
||||
const wrapper = shallow(
|
||||
<AllListing listing={[]}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
const defaultProps = {
|
||||
openedFrom: 'actions_menu' as OpenedFromType,
|
||||
};
|
||||
|
||||
it('should render with one plugin', () => {
|
||||
const wrapper = shallow(
|
||||
<AllListing listing={[samplePlugin]}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render with plugins', () => {
|
||||
const wrapper = shallow(
|
||||
<AllListing listing={[samplePlugin, sampleInstalledPlugin]}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('InstalledPlugins', () => {
|
||||
const baseProps = {
|
||||
changeTab: jest.fn(),
|
||||
};
|
||||
|
||||
it('should render with no plugins', () => {
|
||||
const wrapper = shallow(
|
||||
<InstalledListing
|
||||
{...baseProps}
|
||||
installedItems={[]}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render with one plugin', () => {
|
||||
const wrapper = shallow(
|
||||
<InstalledListing
|
||||
{...baseProps}
|
||||
installedItems={[sampleInstalledPlugin]}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render with multiple plugins', () => {
|
||||
const wrapper = shallow(
|
||||
<InstalledListing
|
||||
{...baseProps}
|
||||
installedItems={[sampleInstalledPlugin, sampleInstalledPlugin]}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MarketplaceModal', () => {
|
||||
const baseProps: MarketplaceModalProps = {
|
||||
show: true,
|
||||
listing: [samplePlugin],
|
||||
installedListing: [],
|
||||
pluginStatuses: {},
|
||||
siteURL: 'http://example.com',
|
||||
firstAdminVisitMarketplaceStatus: false,
|
||||
openedFrom: 'actions_menu',
|
||||
actions: {
|
||||
closeModal: jest.fn(),
|
||||
fetchListing: jest.fn(() => {
|
||||
return Promise.resolve({});
|
||||
}),
|
||||
filterListing: jest.fn(() => {
|
||||
return Promise.resolve({});
|
||||
}),
|
||||
setFirstAdminVisitMarketplaceStatus: jest.fn(),
|
||||
getPluginStatuses: jest.fn(),
|
||||
beforeEach(() => {
|
||||
mockState = {
|
||||
views: {
|
||||
modals: {
|
||||
modalState: {
|
||||
[ModalIdentifiers.PLUGIN_MARKETPLACE]: {
|
||||
open: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
marketplace: {
|
||||
plugins: [],
|
||||
apps: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
entities: {
|
||||
general: {
|
||||
firstAdminCompleteSetup: false,
|
||||
},
|
||||
admin: {
|
||||
pluginStatuses: {},
|
||||
},
|
||||
},
|
||||
} as unknown as GlobalState;
|
||||
});
|
||||
|
||||
test('should render with no plugins installed', () => {
|
||||
const wrapper = shallow<MarketplaceModal>(
|
||||
<MarketplaceModal {...baseProps}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
test('should render default', () => {
|
||||
const wrapper = shallow(
|
||||
<MarketplaceModal {...defaultProps}/>,
|
||||
);
|
||||
|
||||
test('should render with plugins installed', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
plugins: [
|
||||
...baseProps.listing,
|
||||
sampleInstalledPlugin,
|
||||
],
|
||||
installedListing: [
|
||||
sampleInstalledPlugin,
|
||||
],
|
||||
};
|
||||
expect(wrapper.shallow()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
const wrapper = shallow<MarketplaceModal>(
|
||||
<MarketplaceModal {...props}/>,
|
||||
);
|
||||
test('should render with no plugins available', () => {
|
||||
const setState = jest.fn();
|
||||
const useStateSpy = jest.spyOn(React, 'useState');
|
||||
useStateSpy.mockImplementationOnce(() => [false, setState]);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
const wrapper = shallow(
|
||||
<MarketplaceModal {...defaultProps}/>,
|
||||
);
|
||||
|
||||
test('should fetch plugins when plugin status is changed', () => {
|
||||
const fetchListing = baseProps.actions.fetchListing;
|
||||
const wrapper = shallow<MarketplaceModal>(<MarketplaceModal {...baseProps}/>);
|
||||
wrapper.update();
|
||||
|
||||
expect(fetchListing).toBeCalledTimes(1);
|
||||
wrapper.setProps({...baseProps});
|
||||
expect(fetchListing).toBeCalledTimes(1);
|
||||
expect(wrapper.shallow()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
const status = {
|
||||
id: 'test',
|
||||
} as PluginStatusRedux;
|
||||
wrapper.setProps({...baseProps, pluginStatuses: {test: status}});
|
||||
expect(fetchListing).toBeCalledTimes(2);
|
||||
});
|
||||
test('should render with plugins available', () => {
|
||||
const setState = jest.fn();
|
||||
const useStateSpy = jest.spyOn(React, 'useState');
|
||||
useStateSpy.mockImplementationOnce(() => [false, setState]);
|
||||
|
||||
test('should render with error banner', () => {
|
||||
const wrapper = shallow<MarketplaceModal>(
|
||||
<MarketplaceModal {...baseProps}/>,
|
||||
);
|
||||
mockState.views.marketplace.plugins = [
|
||||
samplePlugin,
|
||||
];
|
||||
|
||||
wrapper.setState({serverError: {name: 'some.error', message: 'Error test'}});
|
||||
const wrapper = shallow(
|
||||
<MarketplaceModal {...defaultProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
test('Should call for track event when searching', () => {
|
||||
const wrapper = shallow<MarketplaceModal>(
|
||||
<MarketplaceModal {...baseProps}/>,
|
||||
);
|
||||
expect(wrapper.shallow()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
wrapper.setState({filter: 'nps'});
|
||||
wrapper.instance().doSearch();
|
||||
test('should render with plugins installed', () => {
|
||||
const setState = jest.fn();
|
||||
const useStateSpy = jest.spyOn(React, 'useState');
|
||||
useStateSpy.mockImplementationOnce(() => [false, setState]);
|
||||
|
||||
expect(trackEvent).toHaveBeenCalledWith('plugins', 'ui_marketplace_opened', {from: 'actions_menu'});
|
||||
expect(trackEvent).toHaveBeenCalledWith('plugins', 'ui_marketplace_search', {filter: 'nps'});
|
||||
});
|
||||
mockState.views.marketplace.plugins = [
|
||||
samplePlugin,
|
||||
sampleInstalledPlugin,
|
||||
];
|
||||
|
||||
test('Should call for opened track event on mount', () => {
|
||||
const openedFrom = 'actions_menu';
|
||||
const wrapper = shallow(
|
||||
<MarketplaceModal {...defaultProps}/>,
|
||||
);
|
||||
|
||||
shallow<MarketplaceModal>(
|
||||
<MarketplaceModal
|
||||
{...baseProps}
|
||||
openedFrom={openedFrom}
|
||||
/>,
|
||||
);
|
||||
wrapper.update();
|
||||
|
||||
expect(trackEvent).toHaveBeenCalledWith('plugins', 'ui_marketplace_opened', {from: openedFrom});
|
||||
});
|
||||
expect(wrapper.shallow()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should render with error banner', () => {
|
||||
const setState = jest.fn();
|
||||
const useStateSpy = jest.spyOn(React, 'useState');
|
||||
useStateSpy.mockImplementation(() => [true, setState]);
|
||||
|
||||
const wrapper = shallow(
|
||||
<MarketplaceModal {...defaultProps}/>,
|
||||
);
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.shallow()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,283 +1,269 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import debounce from 'lodash/debounce';
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {Tabs, Tab, SelectCallback} from 'react-bootstrap';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import {Link} from 'react-router-dom';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
import {PluginStatusRedux} from '@mattermost/types/plugins';
|
||||
import type {MarketplaceApp, MarketplacePlugin} from '@mattermost/types/marketplace';
|
||||
import {MagnifyIcon} from '@mattermost/compass-icons/components';
|
||||
|
||||
import FullScreenModal from 'components/widgets/modals/full_screen_modal';
|
||||
import RootPortal from 'components/root_portal';
|
||||
import QuickInput from 'components/quick_input';
|
||||
import LocalizedInput from 'components/localized_input/localized_input';
|
||||
import PluginIcon from 'components/widgets/icons/plugin_icon';
|
||||
import LoadingScreen from 'components/loading_screen';
|
||||
import FormattedMarkdownMessage from 'components/formatted_markdown_message';
|
||||
import {FooterPagination} from '@mattermost/components';
|
||||
import {getPluginStatuses} from 'mattermost-redux/actions/admin';
|
||||
import {setFirstAdminVisitMarketplaceStatus} from 'mattermost-redux/actions/general';
|
||||
import {getFirstAdminVisitMarketplaceStatus} from 'mattermost-redux/selectors/entities/general';
|
||||
import {ActionResult} from 'mattermost-redux/types/actions';
|
||||
|
||||
import {fetchListing, filterListing} from 'actions/marketplace';
|
||||
import {trackEvent} from 'actions/telemetry_actions.jsx';
|
||||
import {t} from 'utils/i18n';
|
||||
import {localizeMessage} from 'utils/utils';
|
||||
import {closeModal} from 'actions/views/modals';
|
||||
|
||||
import GenericModal from 'components/generic_modal';
|
||||
import LoadingScreen from 'components/loading_screen';
|
||||
import Input, {SIZE} from 'components/widgets/inputs/input/input';
|
||||
|
||||
import {getListing, getInstalledListing} from 'selectors/views/marketplace';
|
||||
import {isModalOpen} from 'selectors/views/modals';
|
||||
import {GlobalState} from 'types/store';
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
import './marketplace_modal.scss';
|
||||
import MarketplaceList from './marketplace_list/marketplace_list';
|
||||
|
||||
import MarketplaceList, {ITEMS_PER_PAGE} from './marketplace_list/marketplace_list';
|
||||
|
||||
const MarketplaceTabs = {
|
||||
ALL_LISTING: 'allListing',
|
||||
ALL_LISTING: 'all',
|
||||
INSTALLED_LISTING: 'installed',
|
||||
};
|
||||
|
||||
const SEARCH_TIMEOUT_MILLISECONDS = 200;
|
||||
|
||||
const linkConsole = (msg: string) => (
|
||||
<Link to='/admin_console/plugins/plugin_management'>
|
||||
{msg}
|
||||
</Link>
|
||||
);
|
||||
|
||||
export type OpenedFromType = 'actions_menu' | 'app_bar' | 'channel_header' | 'command' | 'open_plugin_install_post' | 'product_menu';
|
||||
|
||||
type AllListingProps = {
|
||||
listing: Array<MarketplacePlugin | MarketplaceApp>;
|
||||
};
|
||||
|
||||
// AllListing renders the contents of the all listing tab.
|
||||
export const AllListing = ({listing}: AllListingProps): JSX.Element => {
|
||||
if (listing.length === 0) {
|
||||
return (
|
||||
<div className='no_plugins_div'>
|
||||
<br/>
|
||||
<PluginIcon className='icon__plugin'/>
|
||||
<div className='mt-3 light'>
|
||||
<FormattedMessage
|
||||
id='marketplace_modal.no_plugins'
|
||||
defaultMessage='There are no plugins available at this time.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <MarketplaceList listing={listing}/>;
|
||||
};
|
||||
|
||||
type InstalledListingProps = {
|
||||
installedItems: Array<MarketplacePlugin | MarketplaceApp>;
|
||||
changeTab: SelectCallback;
|
||||
};
|
||||
|
||||
// InstalledListing renders the contents of the installed listing tab.
|
||||
export const InstalledListing = ({installedItems, changeTab}: InstalledListingProps): JSX.Element => {
|
||||
if (installedItems.length === 0) {
|
||||
return (
|
||||
<div className='no_plugins_div'>
|
||||
<br/>
|
||||
<PluginIcon className='icon__plugin'/>
|
||||
<div className='mt-3 light'>
|
||||
<FormattedMessage
|
||||
id='marketplace_modal.no_plugins_installed'
|
||||
defaultMessage='You do not have any plugins installed.'
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className='mt-5 style--none color--link'
|
||||
onClick={() => changeTab(MarketplaceTabs.ALL_LISTING)}
|
||||
data-testid='Install-Plugins-button'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='marketplace_modal.install_plugins'
|
||||
defaultMessage='Install Plugins'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <MarketplaceList listing={installedItems}/>;
|
||||
};
|
||||
|
||||
export type MarketplaceModalProps = {
|
||||
show: boolean;
|
||||
listing: Array<MarketplacePlugin | MarketplaceApp>;
|
||||
installedListing: Array<MarketplacePlugin | MarketplaceApp>;
|
||||
siteURL: string;
|
||||
pluginStatuses?: Record<string, PluginStatusRedux>;
|
||||
firstAdminVisitMarketplaceStatus: boolean;
|
||||
type MarketplaceModalProps = {
|
||||
openedFrom: OpenedFromType;
|
||||
actions: {
|
||||
closeModal: () => void;
|
||||
fetchListing(localOnly?: boolean): Promise<{error?: Error}>;
|
||||
filterListing(filter: string): Promise<{error?: Error}>;
|
||||
setFirstAdminVisitMarketplaceStatus(): Promise<void>;
|
||||
getPluginStatuses(): Promise<void>;
|
||||
};
|
||||
};
|
||||
|
||||
type MarketplaceModalState = {
|
||||
tabKey: unknown;
|
||||
loading: boolean;
|
||||
serverError?: Error;
|
||||
filter: string;
|
||||
};
|
||||
|
||||
// MarketplaceModal is the marketplace modal.
|
||||
export default class MarketplaceModal extends React.PureComponent<MarketplaceModalProps, MarketplaceModalState> {
|
||||
private filterRef: React.RefObject<HTMLInputElement>;
|
||||
|
||||
constructor(props: MarketplaceModalProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
tabKey: MarketplaceTabs.ALL_LISTING,
|
||||
loading: true,
|
||||
serverError: undefined,
|
||||
filter: '',
|
||||
};
|
||||
|
||||
this.filterRef = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
trackEvent('plugins', 'ui_marketplace_opened', {from: this.props.openedFrom});
|
||||
|
||||
this.fetchListing();
|
||||
this.props.actions.getPluginStatuses();
|
||||
if (!this.props.firstAdminVisitMarketplaceStatus) {
|
||||
trackEvent('plugins', 'ui_first_admin_visit_marketplace_status');
|
||||
|
||||
this.props.actions.setFirstAdminVisitMarketplaceStatus();
|
||||
}
|
||||
|
||||
this.filterRef.current?.focus();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: MarketplaceModalProps): void {
|
||||
// Automatically refresh the component when a plugin is installed or uninstalled.
|
||||
if (this.props.pluginStatuses !== prevProps.pluginStatuses) {
|
||||
this.fetchListing();
|
||||
}
|
||||
}
|
||||
|
||||
fetchListing = async (): Promise<void> => {
|
||||
const {error} = await this.props.actions.fetchListing();
|
||||
this.setState({loading: false, serverError: error});
|
||||
}
|
||||
|
||||
close = (): void => {
|
||||
trackEvent('plugins', 'ui_marketplace_closed');
|
||||
this.props.actions.closeModal();
|
||||
}
|
||||
|
||||
changeTab: SelectCallback = (tabKey: any): void => {
|
||||
this.setState({tabKey});
|
||||
}
|
||||
|
||||
onInput = (): void => {
|
||||
if (this.filterRef.current) {
|
||||
this.setState({filter: this.filterRef.current.value});
|
||||
|
||||
this.debouncedSearch();
|
||||
}
|
||||
}
|
||||
|
||||
handleClearSearch = (): void => {
|
||||
if (this.filterRef.current) {
|
||||
this.filterRef.current.value = '';
|
||||
this.setState({filter: this.filterRef.current.value}, this.doSearch);
|
||||
}
|
||||
}
|
||||
|
||||
doSearch = async (): Promise<void> => {
|
||||
trackEvent('plugins', 'ui_marketplace_search', {filter: this.state.filter});
|
||||
|
||||
const {error} = await this.props.actions.filterListing(this.state.filter);
|
||||
|
||||
this.setState({serverError: error});
|
||||
}
|
||||
|
||||
debouncedSearch = debounce(this.doSearch, SEARCH_TIMEOUT_MILLISECONDS);
|
||||
|
||||
render(): JSX.Element {
|
||||
const input = (
|
||||
<div className='filter-row filter-row--full'>
|
||||
<div className='col-sm-12'>
|
||||
<QuickInput
|
||||
id='searchMarketplaceTextbox'
|
||||
ref={this.filterRef}
|
||||
className='form-control filter-textbox search_input'
|
||||
placeholder={{id: t('marketplace_modal.search'), defaultMessage: 'Search Marketplace'}}
|
||||
inputComponent={LocalizedInput}
|
||||
onInput={this.onInput}
|
||||
value={this.state.filter}
|
||||
clearable={true}
|
||||
onClear={this.handleClearSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
let errorBanner = null;
|
||||
if (this.state.serverError) {
|
||||
errorBanner = (
|
||||
<div
|
||||
className='error-bar'
|
||||
id='error_bar'
|
||||
>
|
||||
<div className='error-bar__content'>
|
||||
<FormattedMarkdownMessage
|
||||
id='app.plugin.marketplace_plugins.app_error'
|
||||
defaultMessage='Error connecting to the marketplace server. Please check your settings in the [System Console]({siteURL}/admin_console/plugins/plugin_management).'
|
||||
values={{siteURL: this.props.siteURL}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RootPortal>
|
||||
<FullScreenModal
|
||||
show={this.props.show}
|
||||
onClose={this.close}
|
||||
ariaLabel={localizeMessage('marketplace_modal.title', 'Marketplace')}
|
||||
>
|
||||
{errorBanner}
|
||||
<div
|
||||
className='modal-marketplace'
|
||||
id='modal_marketplace'
|
||||
>
|
||||
<h1>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id='marketplace_modal.title'
|
||||
defaultMessage='Marketplace'
|
||||
/>
|
||||
</strong>
|
||||
</h1>
|
||||
{input}
|
||||
<Tabs
|
||||
id='marketplaceTabs'
|
||||
className='tabs'
|
||||
defaultActiveKey={MarketplaceTabs.ALL_LISTING}
|
||||
activeKey={this.state.tabKey}
|
||||
onSelect={this.changeTab}
|
||||
unmountOnExit={true}
|
||||
>
|
||||
<Tab
|
||||
eventKey={MarketplaceTabs.ALL_LISTING}
|
||||
title={localizeMessage('marketplace_modal.tabs.all_listing', 'All')}
|
||||
>
|
||||
{this.state.loading ? <LoadingScreen/> : <AllListing listing={this.props.listing}/>}
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey={MarketplaceTabs.INSTALLED_LISTING}
|
||||
title={localizeMessage('marketplace_modal.tabs.installed_listing', 'Installed') + ` (${this.props.installedListing.length})`}
|
||||
>
|
||||
<InstalledListing
|
||||
installedItems={this.props.installedListing}
|
||||
changeTab={this.changeTab}
|
||||
/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
</FullScreenModal>
|
||||
</RootPortal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const MarketplaceModal = ({
|
||||
openedFrom,
|
||||
}: MarketplaceModalProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const {formatMessage} = useIntl();
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const show = useSelector((state: GlobalState) => isModalOpen(state, ModalIdentifiers.PLUGIN_MARKETPLACE));
|
||||
const listing = useSelector(getListing);
|
||||
const installedListing = useSelector(getInstalledListing);
|
||||
const pluginStatuses = useSelector((state: GlobalState) => state.entities.admin.pluginStatuses);
|
||||
const hasFirstAdminVisitedMarketplace = useSelector(getFirstAdminVisitMarketplaceStatus);
|
||||
|
||||
const [tabKey, setTabKey] = useState(MarketplaceTabs.ALL_LISTING);
|
||||
const [filter, setFilter] = useState('');
|
||||
const [page, setPage] = useState(0);
|
||||
const [hasLoaded, setHasLoaded] = useState(false);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [serverError, setServerError] = React.useState(false);
|
||||
|
||||
const doFetchListing = useCallback(async () => {
|
||||
const {error} = await dispatch(fetchListing()) as ActionResult;
|
||||
|
||||
if (error) {
|
||||
setServerError(true);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
const doSearch = useCallback(async () => {
|
||||
trackEvent('plugins', 'ui_marketplace_search', {filter});
|
||||
|
||||
const {error} = await dispatch(filterListing(filter)) as ActionResult;
|
||||
|
||||
if (error) {
|
||||
setServerError(true);
|
||||
}
|
||||
}, [filter]);
|
||||
|
||||
const debouncedSearch = debounce(doSearch, SEARCH_TIMEOUT_MILLISECONDS);
|
||||
|
||||
useEffect(() => {
|
||||
async function doFetch() {
|
||||
await dispatch(getPluginStatuses());
|
||||
await doFetchListing();
|
||||
setHasLoaded(true);
|
||||
}
|
||||
|
||||
trackEvent('plugins', 'ui_marketplace_opened', {from: openedFrom});
|
||||
|
||||
if (!hasFirstAdminVisitedMarketplace) {
|
||||
trackEvent('plugins', 'ui_first_admin_visit_marketplace_status');
|
||||
dispatch(setFirstAdminVisitMarketplaceStatus());
|
||||
}
|
||||
|
||||
doFetch();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasLoaded) {
|
||||
doFetchListing();
|
||||
}
|
||||
}, [pluginStatuses]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasLoaded) {
|
||||
debouncedSearch();
|
||||
setPage(0);
|
||||
}
|
||||
}, [filter]);
|
||||
|
||||
const scrollListToTop = useCallback(() => {
|
||||
if (listRef.current) {
|
||||
listRef.current.scrollTop = 0;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleOnClose = () => {
|
||||
trackEvent('plugins', 'ui_marketplace_closed');
|
||||
dispatch(closeModal(ModalIdentifiers.PLUGIN_MARKETPLACE));
|
||||
};
|
||||
|
||||
const handleChangeTab: SelectCallback = useCallback((tabKey) => {
|
||||
setTabKey(tabKey);
|
||||
setPage(0);
|
||||
scrollListToTop();
|
||||
}, [scrollListToTop]);
|
||||
|
||||
const handleOnChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFilter(event.target.value);
|
||||
}, []);
|
||||
|
||||
const handleOnClear = useCallback(() => {
|
||||
setFilter('');
|
||||
}, []);
|
||||
|
||||
const handleOnNextPage = useCallback(() => {
|
||||
setPage(page + 1);
|
||||
scrollListToTop();
|
||||
}, [page, scrollListToTop]);
|
||||
|
||||
const handleOnPreviousPage = useCallback(() => {
|
||||
setPage(page - 1);
|
||||
scrollListToTop();
|
||||
}, [page, scrollListToTop]);
|
||||
|
||||
const handleNoResultsButtonClick = useCallback(() => {
|
||||
handleChangeTab(MarketplaceTabs.ALL_LISTING);
|
||||
}, [handleChangeTab]);
|
||||
|
||||
const getHeaderInput = useCallback(() => (
|
||||
<Input
|
||||
id='searchMarketplaceTextbox'
|
||||
name='searchMarketplaceTextbox'
|
||||
containerClassName='marketplace-modal-search'
|
||||
inputClassName='search_input'
|
||||
type='text'
|
||||
inputSize={SIZE.LARGE}
|
||||
inputPrefix={<MagnifyIcon size={24}/>}
|
||||
placeholder={formatMessage({id: 'marketplace_modal.search', defaultMessage: 'Search marketplace'})}
|
||||
useLegend={false}
|
||||
autoFocus={true}
|
||||
clearable={true}
|
||||
value={filter}
|
||||
onChange={handleOnChange}
|
||||
onClear={handleOnClear}
|
||||
/>
|
||||
), [filter, handleOnChange, handleOnClear]);
|
||||
|
||||
const getFooterContent = useCallback(() => (
|
||||
<FooterPagination
|
||||
page={page}
|
||||
total={tabKey === MarketplaceTabs.ALL_LISTING ? listing.length : installedListing.length}
|
||||
itemsPerPage={ITEMS_PER_PAGE}
|
||||
onNextPage={handleOnNextPage}
|
||||
onPreviousPage={handleOnPreviousPage}
|
||||
/>
|
||||
), [installedListing.length, listing.length, page, handleOnNextPage, handleOnPreviousPage, tabKey]);
|
||||
|
||||
return (
|
||||
<GenericModal
|
||||
id='marketplace-modal'
|
||||
className='marketplace-modal'
|
||||
modalHeaderText={formatMessage({id: 'marketplace_modal.title', defaultMessage: 'App Marketplace'})}
|
||||
ariaLabel={formatMessage({id: 'marketplace_modal.title', defaultMessage: 'App Marketplace'})}
|
||||
errorText={serverError ? (
|
||||
formatMessage(
|
||||
{
|
||||
id: 'marketplace_modal.app_error',
|
||||
defaultMessage: 'Error connecting to the marketplace server. Please check your settings in the <linkConsole>System Console</linkConsole>.',
|
||||
},
|
||||
{linkConsole},
|
||||
)
|
||||
) : undefined}
|
||||
show={show}
|
||||
compassDesign={true}
|
||||
bodyPadding={false}
|
||||
footerDivider={true}
|
||||
onExited={handleOnClose}
|
||||
footerContent={getFooterContent()}
|
||||
headerInput={getHeaderInput()}
|
||||
>
|
||||
<Tabs
|
||||
id='marketplaceTabs'
|
||||
className='tabs'
|
||||
defaultActiveKey={MarketplaceTabs.ALL_LISTING}
|
||||
activeKey={tabKey}
|
||||
onSelect={handleChangeTab}
|
||||
unmountOnExit={true}
|
||||
>
|
||||
<Tab
|
||||
eventKey={MarketplaceTabs.ALL_LISTING}
|
||||
title={formatMessage({id: 'marketplace_modal.tabs.all_listing', defaultMessage: 'All'})}
|
||||
>
|
||||
{loading ? (
|
||||
<LoadingScreen className='loading'/>
|
||||
) : (
|
||||
<MarketplaceList
|
||||
listRef={listRef}
|
||||
listing={listing}
|
||||
page={page}
|
||||
filter={filter}
|
||||
noResultsMessage={formatMessage({id: 'marketplace_modal.no_plugins', defaultMessage: 'No plugins found'})}
|
||||
/>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey={MarketplaceTabs.INSTALLED_LISTING}
|
||||
title={formatMessage(
|
||||
{id: 'marketplace_modal.tabs.installed_listing', defaultMessage: 'Installed ({count})'},
|
||||
{count: installedListing.length},
|
||||
)}
|
||||
>
|
||||
<MarketplaceList
|
||||
listRef={listRef}
|
||||
listing={installedListing}
|
||||
page={page}
|
||||
filter={filter}
|
||||
noResultsMessage={formatMessage({
|
||||
id: 'marketplace_modal.no_plugins_installed',
|
||||
defaultMessage: 'No plugins installed found',
|
||||
})}
|
||||
noResultsAction={{
|
||||
label: formatMessage({id: 'marketplace_modal.install_plugins', defaultMessage: 'Install plugins'}),
|
||||
onClick: handleNoResultsButtonClick,
|
||||
}}
|
||||
/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</GenericModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketplaceModal;
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import {connect, ConnectedProps} from 'react-redux';
|
||||
import {AnyAction, bindActionCreators, Dispatch} from 'redux';
|
||||
|
||||
import {showActionsDropdownPulsatingDot} from 'selectors/actions_menu';
|
||||
import {setActionsMenuInitialisationState} from 'mattermost-redux/actions/preferences';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getPost, makeGetCommentCountForPost, makeIsPostCommentMention, isPostAcknowledgementsEnabled, isPostPriorityEnabled, UserActivityPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
@@ -13,7 +12,6 @@ import {
|
||||
get,
|
||||
getBool,
|
||||
isCollapsedThreadsEnabled,
|
||||
onboardingTourTipsEnabled,
|
||||
} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentTeam, getCurrentTeamId, getTeam, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
@@ -129,8 +127,7 @@ function makeMapStateToProps() {
|
||||
const user = getUser(state, post.user_id);
|
||||
const isBot = Boolean(user && user.is_bot);
|
||||
const highlightedPostId = getHighlightedPostId(state);
|
||||
const showActionsMenuPulsatingDot = showActionsDropdownPulsatingDot(state);
|
||||
const tourTipsEnabled = onboardingTourTipsEnabled(state);
|
||||
|
||||
const selectedCard = getSelectedPostCard(state);
|
||||
|
||||
let emojis: Emoji[] = [];
|
||||
@@ -203,8 +200,7 @@ function makeMapStateToProps() {
|
||||
compactDisplay: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT,
|
||||
colorizeUsernames: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLORIZE_USERNAMES, Preferences.COLORIZE_USERNAMES_DEFAULT) === 'true',
|
||||
shouldShowActionsMenu: shouldShowActionsMenu(state, post),
|
||||
showActionsMenuPulsatingDot,
|
||||
tourTipsEnabled,
|
||||
|
||||
shortcutReactToLastPostEmittedFrom,
|
||||
isBot,
|
||||
collapsedThreadsEnabled: isCollapsedThreadsEnabled(state),
|
||||
|
||||
@@ -114,8 +114,6 @@ export type Props = {
|
||||
isPostAcknowledgementsEnabled: boolean;
|
||||
isPostPriorityEnabled: boolean;
|
||||
isCardOpen?: boolean;
|
||||
shouldShowDotMenu: boolean;
|
||||
tourTipsEnabled: boolean;
|
||||
};
|
||||
|
||||
const PostComponent = (props: Props): JSX.Element => {
|
||||
@@ -148,6 +146,7 @@ const PostComponent = (props: Props): JSX.Element => {
|
||||
|
||||
const handleA11yActivateEvent = () => setA11y(true);
|
||||
const handleA11yDeactivateEvent = () => setA11y(false);
|
||||
const handleAlt = (e: KeyboardEvent) => setAlt(e.altKey);
|
||||
|
||||
useEffect(() => {
|
||||
if (a11yActive) {
|
||||
@@ -156,13 +155,8 @@ const PostComponent = (props: Props): JSX.Element => {
|
||||
}, [a11yActive]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleAlt = (e: KeyboardEvent) => {
|
||||
setAlt(e.altKey);
|
||||
};
|
||||
let removeEventListener: (type: string, listener: EventListener) => void;
|
||||
|
||||
document.addEventListener('keydown', handleAlt);
|
||||
document.addEventListener('keyup', handleAlt);
|
||||
if (postRef.current) {
|
||||
postRef.current.addEventListener(A11yCustomEventTypes.ACTIVATE, handleA11yActivateEvent);
|
||||
postRef.current.addEventListener(A11yCustomEventTypes.DEACTIVATE, handleA11yDeactivateEvent);
|
||||
@@ -170,8 +164,6 @@ const PostComponent = (props: Props): JSX.Element => {
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleAlt);
|
||||
document.removeEventListener('keyup', handleAlt);
|
||||
if (removeEventListener) {
|
||||
removeEventListener(A11yCustomEventTypes.ACTIVATE, handleA11yActivateEvent);
|
||||
removeEventListener(A11yCustomEventTypes.DEACTIVATE, handleA11yDeactivateEvent);
|
||||
@@ -179,6 +171,18 @@ const PostComponent = (props: Props): JSX.Element => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (hover) {
|
||||
document.addEventListener('keydown', handleAlt);
|
||||
document.addEventListener('keyup', handleAlt);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleAlt);
|
||||
document.removeEventListener('keyup', handleAlt);
|
||||
};
|
||||
}, [hover]);
|
||||
|
||||
const hasSameRoot = (props: Props) => {
|
||||
if (props.isFirstReply) {
|
||||
return false;
|
||||
@@ -256,16 +260,16 @@ const PostComponent = (props: Props): JSX.Element => {
|
||||
'current--user': props.currentUserId === post.user_id && !isSystemMessage,
|
||||
'post--system': isSystemMessage || isMeMessage,
|
||||
'post--root': props.hasReplies && !(post.root_id && post.root_id.length > 0),
|
||||
'post--comment': post.root_id && post.root_id.length > 0 && !props.isCollapsedThreadsEnabled,
|
||||
'post--comment': (post.root_id && post.root_id.length > 0 && !props.isCollapsedThreadsEnabled) || (props.location === Locations.RHS_COMMENT),
|
||||
'post--compact': props.compactDisplay,
|
||||
'post--hovered': hovered,
|
||||
'same--user': props.isConsecutivePost && !props.compactDisplay,
|
||||
'same--user': props.isConsecutivePost && (!props.compactDisplay || props.location === Locations.RHS_COMMENT),
|
||||
'cursor--pointer': alt && !props.channelIsArchived,
|
||||
'post--hide-controls': post.failed || post.state === Posts.POST_DELETED,
|
||||
'post--comment same--root': fromAutoResponder,
|
||||
'post--pinned-or-flagged': (post.is_pinned || props.isFlagged) && props.location === Locations.CENTER,
|
||||
'mention-comment': props.isCommentMention,
|
||||
'post--thread': props.location === Locations.RHS_COMMENT || Locations.RHS_ROOT,
|
||||
'post--thread': props.location === Locations.RHS_COMMENT || props.location === Locations.RHS_ROOT,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -380,7 +384,8 @@ const PostComponent = (props: Props): JSX.Element => {
|
||||
|
||||
let profilePic;
|
||||
const hideProfilePicture = hasSameRoot(props) && (!post.root_id && !props.hasReplies) && !PostUtils.isFromBot(post);
|
||||
if (!hideProfilePicture) {
|
||||
const hideProfileCase = !(props.location === Locations.RHS_COMMENT && props.compactDisplay && props.isConsecutivePost);
|
||||
if (!hideProfilePicture && hideProfileCase) {
|
||||
profilePic = (
|
||||
<PostProfilePicture
|
||||
compactDisplay={props.compactDisplay}
|
||||
@@ -530,7 +535,7 @@ const PostComponent = (props: Props): JSX.Element => {
|
||||
isSystemMessage={isSystemMessage}
|
||||
/>
|
||||
<div className='col d-flex align-items-center'>
|
||||
{
|
||||
{((!hideProfilePicture && props.location === Locations.CENTER) || hover || props.location !== Locations.CENTER) &&
|
||||
<PostTime
|
||||
isPermalink={!(Posts.POST_DELETED === post.state || isPostPendingOrFailed(post))}
|
||||
eventTime={post.create_at}
|
||||
|
||||
@@ -5,7 +5,7 @@ import React, {ReactNode, useEffect, useRef, useState} from 'react';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {Posts, Preferences} from 'mattermost-redux/constants/index';
|
||||
import {Posts} from 'mattermost-redux/constants/index';
|
||||
import {isPostEphemeral} from 'mattermost-redux/utils/post_utils';
|
||||
|
||||
import {Locations} from 'utils/constants';
|
||||
@@ -19,6 +19,7 @@ import CommentIcon from 'components/common/comment_icon';
|
||||
|
||||
import {Emoji} from '@mattermost/types/emojis';
|
||||
import {Post} from '@mattermost/types/posts';
|
||||
import classnames from 'classnames';
|
||||
|
||||
type Props = {
|
||||
post: Post;
|
||||
@@ -34,8 +35,6 @@ type Props = {
|
||||
handleDropdownOpened?: (e: boolean) => void;
|
||||
collapsedThreadsEnabled?: boolean;
|
||||
shouldShowActionsMenu?: boolean;
|
||||
showActionsMenuPulsatingDot?: boolean;
|
||||
tourTipsEnabled: boolean;
|
||||
oneClickReactionsEnabled?: boolean;
|
||||
recentEmojis: Emoji[];
|
||||
isExpanded?: boolean;
|
||||
@@ -51,7 +50,6 @@ type Props = {
|
||||
shortcutReactToLastPostEmittedFrom?: string;
|
||||
isPostHeaderVisible?: boolean | null;
|
||||
isPostBeingEdited?: boolean;
|
||||
shouldShowDotMenu: boolean;
|
||||
actions: {
|
||||
emitShortcutReactToLastPostFrom: (emittedFrom: 'CENTER' | 'RHS_ROOT' | 'NO_WHERE') => void;
|
||||
};
|
||||
@@ -63,7 +61,6 @@ const PostOptions = (props: Props): JSX.Element => {
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||
const [showDotMenu, setShowDotMenu] = useState(false);
|
||||
const [showActionsMenu, setShowActionsMenu] = useState(false);
|
||||
const [showActionTip, setShowActionTip] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.isLastPost &&
|
||||
@@ -80,8 +77,6 @@ const PostOptions = (props: Props): JSX.Element => {
|
||||
isReadOnly,
|
||||
post,
|
||||
oneClickReactionsEnabled,
|
||||
showActionsMenuPulsatingDot,
|
||||
tourTipsEnabled,
|
||||
isMobileView,
|
||||
} = props;
|
||||
|
||||
@@ -102,34 +97,14 @@ const PostOptions = (props: Props): JSX.Element => {
|
||||
};
|
||||
|
||||
const handleActionsMenuOpened = (open: boolean) => {
|
||||
if (tourTipsEnabled && showActionsMenuPulsatingDot) {
|
||||
setShowActionTip(true);
|
||||
return;
|
||||
}
|
||||
setShowActionsMenu(open);
|
||||
props.handleDropdownOpened!(open);
|
||||
};
|
||||
|
||||
const handleActionsMenuTipOpened = () => {
|
||||
setShowActionTip(true);
|
||||
props.handleDropdownOpened!(true);
|
||||
};
|
||||
|
||||
const handleActionsMenuGotItClick = () => {
|
||||
props.setActionsMenuInitialisationState?.(({[Preferences.ACTIONS_MENU_VIEWED]: true}));
|
||||
setShowActionTip(false);
|
||||
props.handleDropdownOpened!(false);
|
||||
};
|
||||
|
||||
const handleTipDismissed = () => {
|
||||
setShowActionTip(false);
|
||||
props.handleDropdownOpened!(false);
|
||||
};
|
||||
|
||||
const getDotMenuRef = () => dotMenuRef.current;
|
||||
|
||||
const isPostDeleted = post && post.state === Posts.POST_DELETED;
|
||||
const hoverLocal = props.hover || showEmojiPicker || showDotMenu || showActionsMenu || showActionTip;
|
||||
const hoverLocal = props.hover || showEmojiPicker || showDotMenu || showActionsMenu;
|
||||
const showCommentIcon = isFromAutoResponder || (!systemMessage && (isMobileView ||
|
||||
hoverLocal || (!post.root_id && Boolean(props.hasReplies)) ||
|
||||
props.isFirstReply) && props.location === Locations.CENTER);
|
||||
@@ -197,11 +172,6 @@ const PostOptions = (props: Props): JSX.Element => {
|
||||
location={props.location}
|
||||
handleDropdownOpened={handleActionsMenuOpened}
|
||||
isMenuOpen={showActionsMenu}
|
||||
showPulsatingDot={tourTipsEnabled && showActionsMenuPulsatingDot}
|
||||
showTutorialTip={tourTipsEnabled && showActionTip}
|
||||
handleOpenTip={handleActionsMenuTipOpened}
|
||||
handleNextTip={handleActionsMenuGotItClick}
|
||||
handleDismissTip={handleTipDismissed}
|
||||
/>
|
||||
);
|
||||
const dotMenu = (
|
||||
@@ -231,7 +201,7 @@ const PostOptions = (props: Props): JSX.Element => {
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
} else if (isPostDeleted || !props.shouldShowDotMenu) {
|
||||
} else if (isPostDeleted) {
|
||||
options = null;
|
||||
} else if (props.location === Locations.SEARCH) {
|
||||
const hasCRTFooter = props.collapsedThreadsEnabled && !post.root_id && (post.reply_count > 0 || post.is_following);
|
||||
@@ -266,7 +236,7 @@ const PostOptions = (props: Props): JSX.Element => {
|
||||
<div
|
||||
ref={dotMenuRef}
|
||||
data-testid={`post-menu-${props.post.id}`}
|
||||
className='col post-menu'
|
||||
className={classnames('col post-menu', {'post-menu--position': !hoverLocal && showCommentIcon})}
|
||||
>
|
||||
{!collapsedThreadsEnabled && !showRecentlyUsedReactions && dotMenu}
|
||||
{showRecentReactions}
|
||||
|
||||
@@ -19,12 +19,11 @@ import {getBool} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {Preferences} from 'utils/constants';
|
||||
|
||||
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
|
||||
|
||||
import {Channel} from '@mattermost/types/channels';
|
||||
import {Post} from '@mattermost/types/posts';
|
||||
|
||||
import {getCurrentUserTimezone} from '../../selectors/general';
|
||||
|
||||
import PostMarkdown from './post_markdown';
|
||||
|
||||
export function makeGetMentionKeysForPost(): (
|
||||
@@ -75,7 +74,7 @@ function makeMapStateToProps() {
|
||||
isUserCanManageMembers: channel && canManageMembers(state, channel),
|
||||
mentionKeys: getMentionKeysForPost(state, ownProps.post, channel),
|
||||
isMilitaryTime: getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false),
|
||||
timezone: getCurrentUserTimezone(state),
|
||||
timezone: getCurrentTimezone(state),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,17 +10,16 @@ import {Preferences} from 'mattermost-redux/constants';
|
||||
import {addPostReminder} from 'mattermost-redux/actions/posts';
|
||||
|
||||
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
import {makeAsyncComponent} from 'components/async_load';
|
||||
|
||||
import {getCurrentUserTimezone} from 'selectors/general';
|
||||
|
||||
const PostReminderCustomTimePicker = makeAsyncComponent('PostReminderCustomTimePicker', React.lazy(() => import('./post_reminder_custom_time_picker_modal')));
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const timezone = getCurrentUserTimezone(state);
|
||||
const timezone = getCurrentTimezone(state);
|
||||
const userId = getCurrentUserId(state);
|
||||
const isMilitaryTime = getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false);
|
||||
|
||||
|
||||
@@ -6,18 +6,16 @@ import {connect} from 'react-redux';
|
||||
import {bindActionCreators, Dispatch} from 'redux';
|
||||
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/common';
|
||||
import {makeGetUserTimezone} from 'mattermost-redux/selectors/entities/timezone';
|
||||
import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils';
|
||||
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
import {getPostEditHistory} from 'mattermost-redux/actions/posts';
|
||||
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTimezone, isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone';
|
||||
|
||||
import {Preferences} from 'utils/constants';
|
||||
import {isPostOwner, canEditPost} from 'utils/post_utils';
|
||||
|
||||
import {areTimezonesEnabledAndSupported} from '../../../selectors/general';
|
||||
import {GlobalState} from '../../../types/store';
|
||||
import {Props as TimestampProps} from '../../timestamp/timestamp';
|
||||
|
||||
@@ -49,27 +47,23 @@ type DispatchProps = {
|
||||
|
||||
export type Props = OwnProps & StateProps & DispatchProps;
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const getUserTimezone = makeGetUserTimezone();
|
||||
function mapStateToProps(state: GlobalState, ownProps: OwnProps): StateProps {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const post = ownProps.postId ? getPost(state, ownProps.postId) : undefined;
|
||||
const license = getLicense(state);
|
||||
const config = getConfig(state);
|
||||
const channel = getChannel(state, post?.channel_id || '');
|
||||
|
||||
return (state: GlobalState, ownProps: OwnProps): StateProps => {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const post = ownProps.postId ? getPost(state, ownProps.postId) : undefined;
|
||||
const license = getLicense(state);
|
||||
const config = getConfig(state);
|
||||
const channel = getChannel(state, post?.channel_id || '');
|
||||
let timeZone: TimestampProps['timeZone'];
|
||||
|
||||
let timeZone: TimestampProps['timeZone'];
|
||||
if (isTimezoneEnabled(state)) {
|
||||
timeZone = getCurrentTimezone(state);
|
||||
}
|
||||
const postOwner = post ? isPostOwner(state, post) : undefined;
|
||||
|
||||
if (areTimezonesEnabledAndSupported(state)) {
|
||||
timeZone = getUserCurrentTimezone(getUserTimezone(state, currentUserId)) ?? undefined;
|
||||
}
|
||||
const postOwner = post ? isPostOwner(state, post) : undefined;
|
||||
|
||||
const isMilitaryTime = getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false);
|
||||
const canEdit = post ? canEditPost(state, post, license, config, channel, currentUserId) : false;
|
||||
return {isMilitaryTime, timeZone, postOwner, post, canEdit};
|
||||
};
|
||||
const isMilitaryTime = getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false);
|
||||
const canEdit = post ? canEditPost(state, post, license, config, channel, currentUserId) : false;
|
||||
return {isMilitaryTime, timeZone, postOwner, post, canEdit};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
@@ -81,4 +75,4 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
||||
};
|
||||
}
|
||||
|
||||
export default connect<StateProps, DispatchProps, OwnProps, GlobalState>(makeMapStateToProps, mapDispatchToProps)(PostEditedIndicator);
|
||||
export default connect<StateProps, DispatchProps, OwnProps, GlobalState>(mapStateToProps, mapDispatchToProps)(PostEditedIndicator);
|
||||
|
||||
@@ -396,7 +396,7 @@ function Content(props: ContentProps) {
|
||||
plan='Professional'
|
||||
planSummary={formatMessage({id: 'pricing_modal.planSummary.professional', defaultMessage: 'Scalable solutions for growing teams'})}
|
||||
price={`$${professionalPrice}`}
|
||||
rate={formatMessage({id: 'pricing_modal.rate.userPerMonth', defaultMessage: 'USD per user/month {br}<b>(billed annually)</b>'}, {
|
||||
rate={formatMessage({id: 'pricing_modal.rate.seatPerMonth', defaultMessage: 'USD per seat/month {br}<b>(billed annually)</b>'}, {
|
||||
br: <br/>,
|
||||
b: (chunks: React.ReactNode | React.ReactNodeArray) => (
|
||||
<span style={{fontSize: '14px'}}>
|
||||
|
||||
@@ -211,7 +211,7 @@ function SelfHostedContent(props: ContentProps) {
|
||||
plan='Professional'
|
||||
planSummary={formatMessage({id: 'pricing_modal.planSummary.professional', defaultMessage: 'Scalable solutions for growing teams'})}
|
||||
price={professionalPrice}
|
||||
rate={formatMessage({id: 'pricing_modal.rate.userPerMonth', defaultMessage: 'USD per user/month {br}<b>(billed annually)</b>'}, {
|
||||
rate={formatMessage({id: 'pricing_modal.rate.seatPerMonth', defaultMessage: 'USD per seat/month {br}<b>(billed annually)</b>'}, {
|
||||
br: <br/>,
|
||||
b: (chunks: React.ReactNode | React.ReactNodeArray) => (
|
||||
<span style={{fontSize: '14px'}}>
|
||||
|
||||
@@ -4,6 +4,7 @@ exports[`ProductNoticesModal Match snapshot for single notice 1`] = `
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={false}
|
||||
autoCloseOnConfirmButton={true}
|
||||
bodyPadding={true}
|
||||
cancelButtonText={null}
|
||||
className="productNotices"
|
||||
confirmButtonText={
|
||||
@@ -44,6 +45,7 @@ exports[`ProductNoticesModal Match snapshot for user notice 1`] = `
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={false}
|
||||
autoCloseOnConfirmButton={true}
|
||||
bodyPadding={true}
|
||||
cancelButtonText={
|
||||
<React.Fragment>
|
||||
<PreviousIcon
|
||||
@@ -122,6 +124,7 @@ exports[`ProductNoticesModal Should match snapshot for system admin notice 1`] =
|
||||
<GenericModal
|
||||
autoCloseOnCancelButton={false}
|
||||
autoCloseOnConfirmButton={false}
|
||||
bodyPadding={true}
|
||||
cancelButtonText={null}
|
||||
className="productNotices"
|
||||
confirmButtonText={
|
||||
|
||||
@@ -19,12 +19,12 @@ import {
|
||||
import {getCallsConfig, getCalls} from 'mattermost-redux/selectors/entities/common';
|
||||
import {Action} from 'mattermost-redux/types/actions';
|
||||
import {getTeammateNameDisplaySetting} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentTimezone, isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone';
|
||||
|
||||
import {openDirectChannelToUserId} from 'actions/channel_actions';
|
||||
import {getMembershipForEntities} from 'actions/views/profile_popover';
|
||||
import {closeModal, openModal} from 'actions/views/modals';
|
||||
|
||||
import {areTimezonesEnabledAndSupported, getCurrentUserTimezone} from 'selectors/general';
|
||||
import {getRhsState, getSelectedPost} from 'selectors/rhs';
|
||||
import {getIsMobileView} from 'selectors/views/browser';
|
||||
import {isAnyModalOpen} from 'selectors/views/modals';
|
||||
@@ -98,7 +98,7 @@ function makeMapStateToProps() {
|
||||
return {
|
||||
currentTeamId: team.id,
|
||||
currentUserId,
|
||||
enableTimezone: areTimezonesEnabledAndSupported(state),
|
||||
enableTimezone: isTimezoneEnabled(state),
|
||||
isTeamAdmin,
|
||||
isChannelAdmin,
|
||||
isInCurrentTeam: Boolean(teamMember) && teamMember?.delete_at === 0,
|
||||
@@ -111,7 +111,7 @@ function makeMapStateToProps() {
|
||||
isCustomStatusEnabled: isCustomStatusEnabled(state),
|
||||
isCustomStatusExpired: isCustomStatusExpired(state, customStatus),
|
||||
channelId,
|
||||
currentUserTimezone: getCurrentUserTimezone(state),
|
||||
currentUserTimezone: getCurrentTimezone(state),
|
||||
lastActivityTimestamp,
|
||||
enableLastActiveTime,
|
||||
timestampUnits,
|
||||
|
||||
@@ -40,7 +40,7 @@ import Tooltip from 'components/tooltip';
|
||||
import ProfilePopoverCallButton from 'components/profile_popover_call_button';
|
||||
|
||||
import {ServerError} from '@mattermost/types/errors';
|
||||
import {UserCustomStatus, UserProfile, UserTimezone, CustomStatusDuration} from '@mattermost/types/users';
|
||||
import {UserCustomStatus, UserProfile, CustomStatusDuration} from '@mattermost/types/users';
|
||||
|
||||
import './profile_popover.scss';
|
||||
import BotTag from '../widgets/tag/bot_tag';
|
||||
@@ -587,7 +587,7 @@ class ProfilePopover extends React.PureComponent<ProfilePopoverProps, ProfilePop
|
||||
<Timestamp
|
||||
useRelative={false}
|
||||
useDate={false}
|
||||
userTimezone={this.props.user?.timezone as UserTimezone | undefined}
|
||||
userTimezone={this.props.user?.timezone}
|
||||
useTime={{
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
|
||||
@@ -730,7 +730,7 @@ class PurchaseModal extends React.PureComponent<Props, State> {
|
||||
this.state.selectedProduct ? this.state.selectedProduct.name : '',
|
||||
)}
|
||||
price={yearlyProductMonthlyPrice}
|
||||
rate={formatMessage({id: 'pricing_modal.rate.userPerMonth', defaultMessage: 'USD per user/month {br}<b>(billed annually)</b>'}, {
|
||||
rate={formatMessage({id: 'pricing_modal.rate.seatPerMonth', defaultMessage: 'USD per seat/month {br}<b>(billed annually)</b>'}, {
|
||||
br: <br/>,
|
||||
b: (chunks: React.ReactNode | React.ReactNodeArray) => (
|
||||
<span style={{fontSize: '14px'}}>
|
||||
|
||||
@@ -4,10 +4,7 @@
|
||||
import {Settings} from 'luxon';
|
||||
|
||||
import {getCurrentLocale} from 'selectors/i18n';
|
||||
import {areTimezonesEnabledAndSupported} from 'selectors/general';
|
||||
import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {makeGetUserTimezone} from 'mattermost-redux/selectors/entities/timezone';
|
||||
import {getCurrentTimezone, isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone';
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
let prevTimezone: string | undefined;
|
||||
@@ -19,8 +16,8 @@ export function applyLuxonDefaults(state: GlobalState) {
|
||||
Settings.defaultLocale = locale;
|
||||
}
|
||||
|
||||
if (areTimezonesEnabledAndSupported(state)) {
|
||||
const tz = getUserCurrentTimezone(makeGetUserTimezone()(state, getCurrentUserId(state))) ?? undefined;
|
||||
if (isTimezoneEnabled(state)) {
|
||||
const tz = getCurrentTimezone(state);
|
||||
if (tz !== prevTimezone) {
|
||||
prevTimezone = tz;
|
||||
Settings.defaultZone = tz ?? 'system';
|
||||
|
||||
@@ -88,7 +88,7 @@ function validateSeats(seats: string, annualPricePerSeat: number, minSeats: numb
|
||||
{errorPrefix}
|
||||
<FormattedMessage
|
||||
id='self_hosted_signup.error_max_seats'
|
||||
defaultMessage=' license purchase only supports purchases up to {num} users'
|
||||
defaultMessage=' license purchase only supports purchases up to {num} seats'
|
||||
values={{
|
||||
num: <FormattedNumber value={maxSeats}/>,
|
||||
}}
|
||||
@@ -167,7 +167,7 @@ export default function SeatsCalculator(props: Props) {
|
||||
type='text'
|
||||
value={props.seats.quantity}
|
||||
onChange={onChange}
|
||||
placeholder={intl.formatMessage({id: 'self_hosted_signup.seats', defaultMessage: 'User seats'})}
|
||||
placeholder={intl.formatMessage({id: 'self_hosted_signup.seats', defaultMessage: 'Seats'})}
|
||||
wrapperClassName='user_seats'
|
||||
inputClassName='user_seats'
|
||||
maxLength={maxSeats.toString().length + 1}
|
||||
@@ -197,7 +197,7 @@ export default function SeatsCalculator(props: Props) {
|
||||
<div className='SeatsCalculator__seats-label'>
|
||||
<FormattedMessage
|
||||
id='self_hosted_signup.line_item_subtotal'
|
||||
defaultMessage='{num} users × 12 mo.'
|
||||
defaultMessage='{num} seats × 12 mo.'
|
||||
values={{
|
||||
num: props.seats.quantity || '0',
|
||||
}}
|
||||
|
||||
@@ -255,7 +255,7 @@ describe('SelfHostedPurchaseModal', () => {
|
||||
// check title, and some of the most prominent details and secondary actions
|
||||
screen.getByText('Provide your payment details');
|
||||
screen.getByText('Contact Sales');
|
||||
screen.getByText('USD per user/month', {exact: false});
|
||||
screen.getByText('USD per seat/month', {exact: false});
|
||||
screen.getByText('billed annually', {exact: false});
|
||||
screen.getByText(productName);
|
||||
screen.getByText('You will be billed today. Your license will be applied automatically', {exact: false});
|
||||
|
||||
@@ -80,7 +80,7 @@ export default function SelfHostedCard(props: Props) {
|
||||
topColor='#4A69AC'
|
||||
plan={props.desiredPlanName}
|
||||
price={`${props.desiredProduct?.price_per_seat?.toString()}`}
|
||||
rate={intl.formatMessage({id: 'pricing_modal.rate.userPerMonth', defaultMessage: 'USD per user/month {br}<b>(billed annually)</b>'}, {
|
||||
rate={intl.formatMessage({id: 'pricing_modal.rate.seatPerMonth', defaultMessage: 'USD per seat/month {br}<b>(billed annually)</b>'}, {
|
||||
br: <br/>,
|
||||
b: (chunks: React.ReactNode | React.ReactNodeArray) => (
|
||||
<span style={{fontSize: '14px'}}>
|
||||
|
||||
@@ -11,6 +11,7 @@ exports[`components/new_channel_modal should match snapshot 1`] = `
|
||||
aria-label="Add Channels Dropdown"
|
||||
className="SidebarChannelNavigator__addChannelsCtaLhsButton SidebarChannelNavigator__addChannelsCtaLhsButton--untouched"
|
||||
id="addChannelsCta"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<li
|
||||
aria-label="Add channels"
|
||||
@@ -44,3 +45,23 @@ exports[`components/new_channel_modal should match snapshot 1`] = `
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
`;
|
||||
|
||||
exports[`components/new_channel_modal should match snapshot when user has only join channel permissions 1`] = `
|
||||
<button
|
||||
aria-label="Add Channels Dropdown"
|
||||
className="SidebarChannelNavigator__addChannelsCtaLhsButton SidebarChannelNavigator__addChannelsCtaLhsButton--untouched"
|
||||
id="addChannelsCta"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<li
|
||||
aria-label="Add channels"
|
||||
>
|
||||
<i
|
||||
className="icon-plus-box"
|
||||
/>
|
||||
<span>
|
||||
Add Channels
|
||||
</span>
|
||||
</li>
|
||||
</button>
|
||||
`;
|
||||
|
||||
@@ -80,6 +80,12 @@ describe('components/new_channel_modal', () => {
|
||||
system_user: {
|
||||
permissions: [Permissions.JOIN_PUBLIC_CHANNELS, Permissions.CREATE_PRIVATE_CHANNEL, Permissions.CREATE_PUBLIC_CHANNEL],
|
||||
},
|
||||
system_user_join_permissions: {
|
||||
permissions: [Permissions.JOIN_PUBLIC_CHANNELS],
|
||||
},
|
||||
system_user_create_public_permissions: {
|
||||
permissions: [Permissions.JOIN_PUBLIC_CHANNELS, Permissions.CREATE_PUBLIC_CHANNEL],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -99,6 +105,25 @@ describe('components/new_channel_modal', () => {
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot when user has only join channel permissions', () => {
|
||||
const userWithJoinChannelsPermission = {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {
|
||||
id: 'current_user_id',
|
||||
roles: 'system_user_join_permissions',
|
||||
},
|
||||
},
|
||||
} as unknown as UsersState;
|
||||
mockState = {...mockState, entities: {...mockState.entities, users: userWithJoinChannelsPermission}};
|
||||
|
||||
expect(
|
||||
shallow(
|
||||
<AddChannelsCtaButton/>,
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should find the add channels button when user has permissions', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<AddChannelsCtaButton/>,
|
||||
@@ -145,4 +170,52 @@ describe('components/new_channel_modal', () => {
|
||||
|
||||
expect(trackEvent).toHaveBeenCalledWith('ui', 'add_channels_cta_button_clicked');
|
||||
});
|
||||
|
||||
test('should not display as a Cta Dropdown when user only has permissions to join channels ', () => {
|
||||
const userWithJoinChannelsPermission = {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {
|
||||
id: 'current_user_id',
|
||||
roles: 'system_user_join_permissions',
|
||||
},
|
||||
},
|
||||
} as unknown as UsersState;
|
||||
mockState = {...mockState, entities: {...mockState.entities, users: userWithJoinChannelsPermission}};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<AddChannelsCtaButton/>,
|
||||
);
|
||||
|
||||
// do not find the menu
|
||||
expect(wrapper.find('.AddChannelsCtaDropdown').exists()).toBeFalsy();
|
||||
|
||||
// only find the button
|
||||
const button = wrapper.find('button#addChannelsCta');
|
||||
expect(button.exists()).toBeTruthy();
|
||||
|
||||
button.simulate('click');
|
||||
|
||||
// when clicked show the browse channels modal
|
||||
expect(trackEvent).toHaveBeenCalledWith('ui', 'browse_channels_button_is_clicked');
|
||||
});
|
||||
|
||||
test('should still display as a Cta Dropdown when user has permissions to create at least one form of channel', () => {
|
||||
const userWithJoinChannelsPermission = {
|
||||
currentUserId: 'current_user_id',
|
||||
profiles: {
|
||||
current_user_id: {
|
||||
id: 'current_user_id',
|
||||
roles: 'system_user_create_public_permissions',
|
||||
},
|
||||
},
|
||||
} as unknown as UsersState;
|
||||
mockState = {...mockState, entities: {...mockState.entities, users: userWithJoinChannelsPermission}};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<AddChannelsCtaButton/>,
|
||||
);
|
||||
|
||||
expect(wrapper.find('.AddChannelsCtaDropdown').exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -106,8 +106,28 @@ const AddChannelsCtaButton = (): JSX.Element | null => {
|
||||
);
|
||||
};
|
||||
|
||||
const trackOpen = (opened: boolean) => {
|
||||
openAddChannelsCtaOpen(opened);
|
||||
const addChannelsButton = (btnCallback?: () => void) => {
|
||||
const handleClick = () => btnCallback?.();
|
||||
return (
|
||||
<button
|
||||
className={buttonClass}
|
||||
id={'addChannelsCta'}
|
||||
aria-label={intl.formatMessage({id: 'sidebar_left.add_channel_dropdown.dropdownAriaLabel', defaultMessage: 'Add Channels Dropdown'})}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<li
|
||||
aria-label={intl.formatMessage({id: 'sidebar_left.sidebar_channel_navigator.addChannelsCta', defaultMessage: 'Add channels'})}
|
||||
>
|
||||
<i className='icon-plus-box'/>
|
||||
<span>
|
||||
{intl.formatMessage({id: 'sidebar_left.addChannelsCta', defaultMessage: 'Add Channels'})}
|
||||
</span>
|
||||
</li>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const storePreferencesAndTrackEvent = () => {
|
||||
trackEvent('ui', 'add_channels_cta_button_clicked');
|
||||
if (!touchedAddChannelsCtaButton) {
|
||||
dispatch(savePreferences(
|
||||
@@ -122,26 +142,26 @@ const AddChannelsCtaButton = (): JSX.Element | null => {
|
||||
}
|
||||
};
|
||||
|
||||
const trackOpen = (opened: boolean) => {
|
||||
openAddChannelsCtaOpen(opened);
|
||||
storePreferencesAndTrackEvent();
|
||||
};
|
||||
|
||||
if (!canCreateChannel) {
|
||||
const browseChannelsAction = () => {
|
||||
showMoreChannelsModal();
|
||||
storePreferencesAndTrackEvent();
|
||||
};
|
||||
return addChannelsButton(browseChannelsAction);
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuWrapper
|
||||
className='AddChannelsCtaDropdown'
|
||||
onToggle={trackOpen}
|
||||
open={isAddChannelCtaOpen}
|
||||
>
|
||||
<button
|
||||
className={buttonClass}
|
||||
id={'addChannelsCta'}
|
||||
aria-label={intl.formatMessage({id: 'sidebar_left.add_channel_dropdown.dropdownAriaLabel', defaultMessage: 'Add Channels Dropdown'})}
|
||||
>
|
||||
<li
|
||||
aria-label={intl.formatMessage({id: 'sidebar_left.sidebar_channel_navigator.addChannelsCta', defaultMessage: 'Add channels'})}
|
||||
>
|
||||
<i className='icon-plus-box'/>
|
||||
<span>
|
||||
{intl.formatMessage({id: 'sidebar_left.addChannelsCta', defaultMessage: 'Add Channels'})}
|
||||
</span>
|
||||
</li>
|
||||
</button>
|
||||
{addChannelsButton()}
|
||||
<Menu
|
||||
id='AddChannelCtaDropdown'
|
||||
ariaLabel={intl.formatMessage({id: 'sidebar_left.add_channel_cta_dropdown.dropdownAriaLabel', defaultMessage: 'Add Channels Dropdown'})}
|
||||
|
||||
@@ -93,6 +93,28 @@ exports[`components/signup/Signup should match snapshot for all signup options e
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="newsletter"
|
||||
>
|
||||
<span
|
||||
className="interested"
|
||||
>
|
||||
Interested in receiving Mattermost security updates via newsletter?
|
||||
</span>
|
||||
<span
|
||||
className="link"
|
||||
>
|
||||
Sign up at
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/security-updates/"
|
||||
key=".1"
|
||||
location="signup"
|
||||
>
|
||||
https://mattermost.com/security-updates/
|
||||
</ExternalLink>
|
||||
.
|
||||
</span>
|
||||
</div>
|
||||
<SaveButton
|
||||
btnClass="btn-primary"
|
||||
defaultMessage="Create account"
|
||||
@@ -244,6 +266,28 @@ exports[`components/signup/Signup should match snapshot for all signup options e
|
||||
onChange={[Function]}
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
className="newsletter"
|
||||
>
|
||||
<span
|
||||
className="interested"
|
||||
>
|
||||
Interested in receiving Mattermost security updates via newsletter?
|
||||
</span>
|
||||
<span
|
||||
className="link"
|
||||
>
|
||||
Sign up at
|
||||
<ExternalLink
|
||||
href="https://mattermost.com/security-updates/"
|
||||
key=".1"
|
||||
location="signup"
|
||||
>
|
||||
https://mattermost.com/security-updates/
|
||||
</ExternalLink>
|
||||
.
|
||||
</span>
|
||||
</div>
|
||||
<SaveButton
|
||||
btnClass="btn-primary"
|
||||
defaultMessage="Create account"
|
||||
|
||||
@@ -166,6 +166,22 @@
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.newsletter {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 32px;
|
||||
color: var(--center-channel-color-rgb);
|
||||
font-family: 'Open Sans';
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
|
||||
.interested {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.signup-body-card-form-button-submit {
|
||||
@include primary-button;
|
||||
@include button-large;
|
||||
|
||||
@@ -5,7 +5,7 @@ import React from 'react';
|
||||
import {shallow, ReactWrapper} from 'enzyme';
|
||||
import {IntlProvider} from 'react-intl';
|
||||
import {BrowserRouter} from 'react-router-dom';
|
||||
import {act} from '@testing-library/react';
|
||||
import {act, screen} from '@testing-library/react';
|
||||
|
||||
import * as global_actions from 'actions/global_actions';
|
||||
|
||||
@@ -15,16 +15,18 @@ import Signup from 'components/signup/signup';
|
||||
import Input from 'components/widgets/inputs/input/input';
|
||||
import PasswordInput from 'components/widgets/inputs/password_input/password_input';
|
||||
import SaveButton from 'components/save_button';
|
||||
import * as useCWSAvailabilityCheckAll from 'components/common/hooks/useCWSAvailabilityCheck';
|
||||
|
||||
import {RequestStatus} from 'mattermost-redux/constants';
|
||||
import {ClientConfig} from '@mattermost/types/config';
|
||||
import {GlobalState} from 'types/store';
|
||||
import {WindowSizes} from 'utils/constants';
|
||||
import {renderWithIntlAndStore} from 'tests/react_testing_utils';
|
||||
|
||||
let mockState: GlobalState;
|
||||
let mockLocation = {pathname: '', search: '', hash: ''};
|
||||
const mockHistoryPush = jest.fn();
|
||||
let mockLicense = {IsLicensed: 'true'};
|
||||
let mockLicense = {IsLicensed: 'true', Cloud: 'false'};
|
||||
let mockConfig: Partial<ClientConfig>;
|
||||
let mockDispatch = jest.fn();
|
||||
|
||||
@@ -96,7 +98,7 @@ describe('components/signup/Signup', () => {
|
||||
beforeEach(() => {
|
||||
mockLocation = {pathname: '', search: '', hash: ''};
|
||||
|
||||
mockLicense = {IsLicensed: 'true'};
|
||||
mockLicense = {IsLicensed: 'true', Cloud: 'false'};
|
||||
|
||||
mockState = {
|
||||
entities: {
|
||||
@@ -178,7 +180,7 @@ describe('components/signup/Signup', () => {
|
||||
});
|
||||
|
||||
it('should match snapshot for all signup options enabled with isLicensed disabled', () => {
|
||||
mockLicense = {IsLicensed: 'false'};
|
||||
mockLicense = {IsLicensed: 'false', Cloud: 'false'};
|
||||
|
||||
const wrapper = shallow(
|
||||
<Signup/>,
|
||||
@@ -295,4 +297,45 @@ describe('components/signup/Signup', () => {
|
||||
expect(wrapper.find('.content-layout-column-title').text()).toEqual('This invite link is invalid');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show newsletter check box opt-in for self-hosted non airgapped workspaces', async () => {
|
||||
jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => true);
|
||||
mockLicense = {IsLicensed: 'true', Cloud: 'false'};
|
||||
|
||||
const {container: signupContainer} = renderWithIntlAndStore(
|
||||
<BrowserRouter>
|
||||
<Signup/>
|
||||
</BrowserRouter>, {});
|
||||
|
||||
screen.getByTestId('signup-body-card-form-check-newsletter');
|
||||
const checkInput = screen.getByTestId('signup-body-card-form-check-newsletter');
|
||||
expect(checkInput).toHaveAttribute('type', 'checkbox');
|
||||
|
||||
expect(signupContainer).toHaveTextContent(/I would like to receive Mattermost security updates via newsletter. Data Terms and Conditions apply/);
|
||||
});
|
||||
|
||||
it('should NOT show newsletter check box opt-in for self-hosted AND airgapped workspaces', async () => {
|
||||
jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => false);
|
||||
mockLicense = {IsLicensed: 'true', Cloud: 'false'};
|
||||
|
||||
const {container: signupContainer} = renderWithIntlAndStore(
|
||||
<BrowserRouter>
|
||||
<Signup/>
|
||||
</BrowserRouter>, {});
|
||||
|
||||
expect(() => screen.getByTestId('signup-body-card-form-check-newsletter')).toThrow();
|
||||
expect(signupContainer).toHaveTextContent('Interested in receiving Mattermost security updates via newsletter?Sign up at https://mattermost.com/security-updates/.');
|
||||
});
|
||||
|
||||
it('should not show any newsletter related opt-in or text for cloud', async () => {
|
||||
jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => true);
|
||||
mockLicense = {IsLicensed: 'true', Cloud: 'true'};
|
||||
|
||||
renderWithIntlAndStore(
|
||||
<BrowserRouter>
|
||||
<Signup/>
|
||||
</BrowserRouter>, {});
|
||||
|
||||
expect(() => screen.getByTestId('signup-body-card-form-check-newsletter')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,9 +48,12 @@ import LoginOpenIDIcon from 'components/widgets/icons/login_openid_icon';
|
||||
import LoginOffice365Icon from 'components/widgets/icons/login_office_365_icon';
|
||||
import Input, {CustomMessageInputType, SIZE} from 'components/widgets/inputs/input/input';
|
||||
import PasswordInput from 'components/widgets/inputs/password_input/password_input';
|
||||
import CheckInput from 'components/widgets/inputs/check';
|
||||
import SaveButton from 'components/save_button';
|
||||
import useCWSAvailabilityCheck from 'components/common/hooks/useCWSAvailabilityCheck';
|
||||
import ExternalLink from 'components/external_link';
|
||||
|
||||
import {Constants, ItemStatus, ValidationErrors} from 'utils/constants';
|
||||
import {Constants, HostedCustomerLinks, ItemStatus, ValidationErrors} from 'utils/constants';
|
||||
import {isValidUsername, isValidPassword, getPasswordConfig, getRoleFromTrackFlow, getMediumFromTrackFlow} from 'utils/utils';
|
||||
|
||||
import './signup.scss';
|
||||
@@ -99,7 +102,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
TermsOfServiceLink,
|
||||
PrivacyPolicyLink,
|
||||
} = config;
|
||||
const {IsLicensed} = useSelector(getLicense);
|
||||
const {IsLicensed, Cloud} = useSelector(getLicense);
|
||||
const loggedIn = Boolean(useSelector(getCurrentUserId));
|
||||
const useCaseOnboarding = useSelector(getUseCaseOnboarding);
|
||||
const usedBefore = useSelector((state: GlobalState) => (!inviteId && !loggedIn && token ? getGlobalItem(state, token, null) : undefined));
|
||||
@@ -110,6 +113,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
const passwordInput = useRef<HTMLInputElement>(null);
|
||||
|
||||
const isLicensed = IsLicensed === 'true';
|
||||
const isCloud = Cloud === 'true';
|
||||
const enableOpenServer = EnableOpenServer === 'true';
|
||||
const noAccounts = NoAccounts === 'true';
|
||||
const enableSignUpWithEmail = EnableSignUpWithEmail === 'true';
|
||||
@@ -136,12 +140,24 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
const [teamName, setTeamName] = useState(parsedTeamName ?? '');
|
||||
const [alertBanner, setAlertBanner] = useState<AlertBannerProps | null>(null);
|
||||
const [isMobileView, setIsMobileView] = useState(false);
|
||||
const [subscribeToSecurityNewsletter, setSubscribeToSecurityNewsletter] = useState(false);
|
||||
|
||||
const canReachCWS = useCWSAvailabilityCheck();
|
||||
|
||||
const enableExternalSignup = enableSignUpWithGitLab || enableSignUpWithOffice365 || enableSignUpWithGoogle || enableSignUpWithOpenId || enableLDAP || enableSAML;
|
||||
const hasError = Boolean(emailError || nameError || passwordError || serverError || alertBanner);
|
||||
const canSubmit = Boolean(email && name && password) && !hasError && !loading;
|
||||
const {error: passwordInfo} = isValidPassword('', getPasswordConfig(config), intl);
|
||||
|
||||
const subscribeToSecurityNewsletterFunc = () => {
|
||||
try {
|
||||
Client4.subscribeToNewsletter({email, subscribed_content: 'security_newsletter'});
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const getExternalSignupOptions = () => {
|
||||
const externalLoginOptions: ExternalLoginButtonType[] = [];
|
||||
|
||||
@@ -564,6 +580,9 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
}
|
||||
|
||||
await handleSignupSuccess(user, data as UserProfile);
|
||||
if (subscribeToSecurityNewsletter) {
|
||||
subscribeToSecurityNewsletterFunc();
|
||||
}
|
||||
} else {
|
||||
setIsWaiting(false);
|
||||
}
|
||||
@@ -571,6 +590,60 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
|
||||
const handleReturnButtonOnClick = () => history.replace('/');
|
||||
|
||||
const getNewsletterCheck = () => {
|
||||
if (isCloud) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (canReachCWS) {
|
||||
return (
|
||||
<CheckInput
|
||||
id='signup-body-card-form-check-newsletter'
|
||||
name='newsletter'
|
||||
onChange={() => setSubscribeToSecurityNewsletter(!subscribeToSecurityNewsletter)}
|
||||
text={
|
||||
formatMessage(
|
||||
{id: 'newsletter_optin.checkmark.text', defaultMessage: 'I would like to receive Mattermost security updates via newsletter. Data <a>Terms and Conditions</a> apply'},
|
||||
{
|
||||
a: (chunks: React.ReactNode | React.ReactNodeArray) => (
|
||||
<ExternalLink
|
||||
location='signup-newsletter-checkmark'
|
||||
href={HostedCustomerLinks.SECURITY_UPDATES}
|
||||
>
|
||||
{chunks}
|
||||
</ExternalLink>
|
||||
),
|
||||
},
|
||||
)}
|
||||
checked={subscribeToSecurityNewsletter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className='newsletter'>
|
||||
<span className='interested'>
|
||||
{formatMessage({id: 'newsletter_optin.title', defaultMessage: 'Interested in receiving Mattermost security updates via newsletter?'})}
|
||||
</span>
|
||||
<span className='link'>
|
||||
{formatMessage(
|
||||
{id: 'newsletter_optin.desc', defaultMessage: 'Sign up at <a>{link}</a>.'},
|
||||
{
|
||||
link: HostedCustomerLinks.SECURITY_UPDATES,
|
||||
a: (chunks: React.ReactNode | React.ReactNodeArray) => (
|
||||
<ExternalLink
|
||||
location='signup'
|
||||
href={HostedCustomerLinks.SECURITY_UPDATES}
|
||||
>
|
||||
{chunks}
|
||||
</ExternalLink>
|
||||
),
|
||||
},
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const handleOnBlur = (e: FocusEvent<HTMLInputElement>, inputId: string) => {
|
||||
const text = e.target.value;
|
||||
if (!text) {
|
||||
@@ -736,6 +809,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
error={passwordError}
|
||||
onBlur={(e) => handleOnBlur(e, 'password')}
|
||||
/>
|
||||
{getNewsletterCheck()}
|
||||
<SaveButton
|
||||
extraClasses='signup-body-card-form-button-submit large'
|
||||
saving={isWaiting}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {PureComponent, ReactNode} from 'react';
|
||||
import React, {ButtonHTMLAttributes, PureComponent, ReactNode} from 'react';
|
||||
|
||||
import LoadingWrapper from 'components/widgets/loading/loading_wrapper';
|
||||
|
||||
@@ -9,10 +9,9 @@ type Props = {
|
||||
children?: ReactNode;
|
||||
spinning: boolean;
|
||||
spinningText: ReactNode;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
}
|
||||
|
||||
export default class SpinnerButton extends PureComponent<Props & React.ButtonHTMLAttributes<HTMLButtonElement>> {
|
||||
export default class SpinnerButton extends PureComponent<Props & ButtonHTMLAttributes<HTMLButtonElement>> {
|
||||
public static defaultProps: Partial<Props> = {
|
||||
spinning: false,
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ function StartTrialFormModal(props: Props): JSX.Element | null {
|
||||
company_country: country,
|
||||
company_size: orgSize,
|
||||
};
|
||||
const error = await dispatch(requestTrialLicense(trialRequestBody, props.page || 'license'));
|
||||
const {error, data} = await dispatch(requestTrialLicense(trialRequestBody, props.page || 'license'));
|
||||
if (error) {
|
||||
setLoadStatus(TrialLoadStatus.Failed);
|
||||
let title;
|
||||
@@ -137,7 +137,7 @@ function StartTrialFormModal(props: Props): JSX.Element | null {
|
||||
let buttonText;
|
||||
let onTryAgain = handleErrorModalTryAgain;
|
||||
|
||||
if (error?.data.status === 422) {
|
||||
if (data.status === 422) {
|
||||
title = (<></>);
|
||||
subtitle = (
|
||||
<FormattedMessage
|
||||
|
||||
@@ -11,12 +11,12 @@ import {Client4} from 'mattermost-redux/client';
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
|
||||
import {get, getBool, getInt} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
|
||||
import {getCurrentUser, getStatusForUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
import {setStatusDropdown} from 'actions/views/status_dropdown';
|
||||
|
||||
import {getCurrentUserTimezone} from 'selectors/general';
|
||||
import {makeGetCustomStatus, isCustomStatusEnabled, showStatusDropdownPulsatingDot, isCustomStatusExpired} from 'selectors/views/custom_status';
|
||||
import {isStatusDropdownOpen} from 'selectors/views/status_dropdown';
|
||||
import {GenericAction} from 'mattermost-redux/types/actions';
|
||||
@@ -53,7 +53,7 @@ function makeMapStateToProps() {
|
||||
isStatusDropdownOpen: isStatusDropdownOpen(state),
|
||||
showCustomStatusPulsatingDot: showStatusDropdownPulsatingDot(state),
|
||||
showCompleteYourProfileTour,
|
||||
timezone: getCurrentUserTimezone(state),
|
||||
timezone: getCurrentTimezone(state),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,41 +3,30 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import {makeGetUserTimezone} from 'mattermost-redux/selectors/entities/timezone';
|
||||
import {getCurrentTimezone, isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone';
|
||||
|
||||
import {getCurrentDateForTimezone} from 'utils/timezone';
|
||||
import {areTimezonesEnabledAndSupported} from 'selectors/general';
|
||||
import {getCurrentLocale} from 'selectors/i18n';
|
||||
|
||||
import {GlobalState} from 'types/store';
|
||||
|
||||
import SearchDateSuggestion from './search_date_suggestion';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const getUserTimezone = makeGetUserTimezone();
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const timezone = getCurrentTimezone(state);
|
||||
const locale = getCurrentLocale(state);
|
||||
|
||||
return (state: GlobalState) => {
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const userTimezone = getUserTimezone(state, currentUserId);
|
||||
const locale = getCurrentLocale(state);
|
||||
const enableTimezone = isTimezoneEnabled(state);
|
||||
|
||||
const enableTimezone = areTimezonesEnabledAndSupported(state);
|
||||
let currentDate;
|
||||
if (enableTimezone) {
|
||||
currentDate = getCurrentDateForTimezone(timezone);
|
||||
}
|
||||
|
||||
let currentDate;
|
||||
if (enableTimezone) {
|
||||
if (userTimezone.useAutomaticTimezone) {
|
||||
currentDate = getCurrentDateForTimezone(userTimezone.automaticTimezone);
|
||||
} else {
|
||||
currentDate = getCurrentDateForTimezone(userTimezone.manualTimezone);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
currentDate,
|
||||
locale,
|
||||
};
|
||||
return {
|
||||
currentDate,
|
||||
locale,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps)(SearchDateSuggestion);
|
||||
export default connect(mapStateToProps)(SearchDateSuggestion);
|
||||
|
||||
@@ -44,6 +44,7 @@ export type Props = {
|
||||
onMouseUp?: (e: React.MouseEvent<TextboxElement>) => void;
|
||||
onKeyUp?: (e: React.KeyboardEvent<TextboxElement>) => void;
|
||||
onBlur?: (e: FocusEvent<TextboxElement>) => void;
|
||||
onFocus?: (e: FocusEvent<TextboxElement>) => void;
|
||||
supportsCommands?: boolean;
|
||||
handlePostError?: (message: JSX.Element | null) => void;
|
||||
onPaste?: (e: ClipboardEvent) => void;
|
||||
@@ -312,6 +313,7 @@ export default class Textbox extends React.PureComponent<Props> {
|
||||
onKeyUp={this.handleKeyUp}
|
||||
onComposition={this.props.onComposition}
|
||||
onBlur={this.handleBlur}
|
||||
onFocus={this.props.onFocus}
|
||||
onHeightChange={this.props.onHeightChange}
|
||||
onWidthChange={this.props.onWidthChange}
|
||||
onPaste={this.props.onPaste}
|
||||
|
||||
@@ -27,7 +27,6 @@ type Props = {
|
||||
previousPostId: string;
|
||||
teamId: string;
|
||||
timestampProps?: Partial<TimestampProps>;
|
||||
lastPost: Post;
|
||||
};
|
||||
|
||||
function noop() {}
|
||||
|
||||
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Ссылка в новой задаче
Block a user