Merge branch 'master' into MM-51842-number-change-detection

Этот коммит содержится в:
Mattermost Build
2023-04-06 17:28:23 +03:00
коммит произвёл GitHub
родитель cb85672bb5 532070fa6d
Коммит 996e519b02
262 изменённых файлов: 4506 добавлений и 3882 удалений

Просмотреть файл

@@ -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}

Просмотреть файл

@@ -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() {}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше