Split out buttonClassNames utility and use for most places Button isn't (#36328)

* Split out buttonClassNames utility and use for most places Button isn't

* Update StartTrialBtn to use buttonClassNames

Ideally, we'd:
1. Use Button, but that requires sorting out the one case that overrides
   btnClass entirely.
2. Use a button for all of these since none of these should have a link
   role, but that's outside of the scope of this ticket.

* Update another new button to use Button
This commit is contained in:
Harrison Healey
2026-05-06 17:35:10 -04:00
committed by GitHub
parent c1ddd77481
commit 10ad2505a7
45 changed files with 171 additions and 114 deletions
@@ -6,6 +6,7 @@ import React, {useState, useEffect, useMemo} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {GenericModal} from '@mattermost/components';
import {buttonClassNames} from '@mattermost/shared/components/button';
import type {AccessControlPolicy, AccessControlPolicyActiveUpdate, AccessControlPolicyRule} from '@mattermost/types/access_control';
import {getMembershipRule, buildRulesWithMembership} from '@mattermost/types/access_control';
import type {ChannelSearchOpts, ChannelWithTeamData} from '@mattermost/types/channels';
@@ -705,7 +706,7 @@ function PolicyDetails({
}
/>
<BlockableLink
className='btn btn-quaternary'
className={buttonClassNames({emphasis: 'quaternary'})}
to='/admin_console/system_attributes/membership_policies'
>
<FormattedMessage
@@ -4,6 +4,8 @@
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {buttonClassNames} from '@mattermost/shared/components/button';
import {useOpenCloudZendeskSupportForm} from 'components/common/hooks/useOpenZendeskForm';
import ExternalLink from 'components/external_link';
@@ -29,7 +31,7 @@ const CancelSubscription = () => {
<ExternalLink
location='cancel_subscription'
href={contactSupportURL}
className='btn btn-secondary btn-sm btn-danger cancelSubscriptionSection__contactUs'
className={buttonClassNames({emphasis: 'secondary', size: 'sm', variant: 'destructive'}, 'cancelSubscriptionSection__contactUs')}
>
<FormattedMessage
id='admin.billing.subscription.cancelSubscriptionSection.contactUs'
@@ -4,7 +4,7 @@
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {Button} from '@mattermost/shared/components/button';
import {Button, buttonClassNames} from '@mattermost/shared/components/button';
import useOpenSalesLink from 'components/common/hooks/useOpenSalesLink';
import CloudTrialSvg from 'components/common/svg_images_components/cloud_trial_svg';
@@ -143,7 +143,7 @@ const ContactSalesCard = (props: Props) => {
<ExternalLink
location='contact_sales_card'
href={contactSalesLink}
className='btn btn-tertiary PrivateCloudCard__actionButton'
className={buttonClassNames({emphasis: 'tertiary'}, 'PrivateCloudCard__actionButton')}
>
<FormattedMessage
id='admin.billing.subscription.privateCloudCard.contactSales'
@@ -6,6 +6,7 @@ import {FormattedDate, FormattedMessage, FormattedNumber, defineMessages} from '
import {useDispatch} from 'react-redux';
import {CheckCircleOutlineIcon} from '@mattermost/compass-icons/components';
import {buttonClassNames} from '@mattermost/shared/components/button';
import {WithTooltip} from '@mattermost/shared/components/tooltip';
import type {Invoice, InvoiceLineItem, Product} from '@mattermost/types/cloud';
@@ -54,7 +55,7 @@ export const noBillingHistory = (
<ExternalLink
location='billing_summary'
href={CloudLinks.BILLING_DOCS}
className='btn btn-primary BillingSummary__noBillingHistory-link'
className={buttonClassNames({emphasis: 'primary'}, 'BillingSummary__noBillingHistory-link')}
>
<FormattedMessage
id='admin.billing.subscriptions.billing_summary.noBillingHistory.link'
@@ -117,7 +118,7 @@ export const FreeTrial = ({daysLeftOnTrial}: FreeTrialProps) => {
<button
type='button'
onClick={() => openSalesLink()}
className='UpgradeMattermostCloud__upgradeButton btn btn-primary'
className={buttonClassNames({emphasis: 'primary'}, 'UpgradeMattermostCloud__upgradeButton')}
>
<FormattedMessage
id='admin.billing.subscription.privateCloudCard.contactSales'
@@ -338,7 +339,7 @@ export const InvoiceInfo = ({invoice, product, fullCharges, partialCharges, hasM
<div className='BillingSummary__lastInvoice-download'>
<button
onClick={openInvoicePreview}
className='BillingSummary__lastInvoice-downloadButton btn btn-primary'
className={buttonClassNames({emphasis: 'primary'}, 'BillingSummary__lastInvoice-downloadButton')}
>
<i className='icon icon-file-pdf-outline'/>
<FormattedMessage
@@ -5,6 +5,8 @@ import React from 'react';
import {FormattedMessage} from 'react-intl';
import {useSelector} from 'react-redux';
import {buttonClassNames} from '@mattermost/shared/components/button';
import BlockableLink from 'components/admin_console/blockable_link';
import CompanySvg from 'components/common/svg_images_components/company_svg';
@@ -16,7 +18,7 @@ const addInfoButton = (
<div className='CompanyInfoDisplay__addInfo'>
<BlockableLink
to='/admin_console/billing/company_info_edit'
className='btn btn-primary CompanyInfoDisplay__addInfoButton'
className={buttonClassNames({emphasis: 'primary'}, 'CompanyInfoDisplay__addInfoButton')}
>
<i className='icon icon-plus'/>
<FormattedMessage
@@ -41,7 +43,7 @@ const noCompanyInfoSection = (
</div>
<BlockableLink
to='/admin_console/billing/company_info_edit'
className='btn btn-primary CompanyInfoDisplay__noCompanyInfo-link'
className={buttonClassNames({emphasis: 'primary'}, 'CompanyInfoDisplay__noCompanyInfo-link')}
>
<FormattedMessage
id='admin.billing.company_info.add'
@@ -7,6 +7,8 @@ import {defineMessage, FormattedMessage} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import {useHistory} from 'react-router-dom';
import {buttonClassNames} from '@mattermost/shared/components/button';
import {getCloudCustomer, updateCloudCustomer, updateCloudCustomerAddress} from 'mattermost-redux/actions/cloud';
import {setNavigationBlocked} from 'actions/admin_actions.jsx';
@@ -284,7 +286,7 @@ const CompanyInfoEdit: React.FC<Props> = () => {
)}
/>
<BlockableLink
className='btn btn-tertiary'
className={buttonClassNames({emphasis: 'tertiary'})}
to='/admin_console/billing/company_info'
>
<FormattedMessage
@@ -3,6 +3,8 @@
import styled from 'styled-components';
import {Button} from '@mattermost/shared/components/button';
import {SectionContent} from '../system_properties/controls';
export const InformationNoticeWrapper = styled.div`
@@ -71,9 +73,9 @@ export const AddLevelButtonRow = styled.div`
margin-left: 16px;
`;
export const AddLevelButton = styled.button.attrs({
export const AddLevelButton = styled(Button).attrs({
type: 'button',
className: 'btn btn-tertiary',
emphasis: 'tertiary',
})`
&& {
padding-inline: 16px;
@@ -4,6 +4,7 @@
import React from 'react';
import {defineMessages, FormattedMessage} from 'react-intl';
import {buttonClassNames} from '@mattermost/shared/components/button';
import type {ChannelWithTeamData} from '@mattermost/types/channels';
import type {
DataRetentionCustomPolicy,
@@ -535,7 +536,7 @@ export default class CustomPolicyForm extends React.PureComponent<Props, State>
)}
/>
<BlockableLink
className='btn btn-tertiary'
className={buttonClassNames({emphasis: 'tertiary'})}
to='/admin_console/compliance/data_retention_settings'
>
<FormattedMessage
@@ -4,6 +4,7 @@
import React from 'react';
import {defineMessages, FormattedMessage} from 'react-intl';
import {buttonClassNames} from '@mattermost/shared/components/button';
import type {AdminConfig, EnvironmentConfig} from '@mattermost/types/config';
import type {DeepPartial} from '@mattermost/types/utilities';
@@ -273,7 +274,7 @@ export default class GlobalPolicyForm extends React.PureComponent<Props, State>
)}
/>
<BlockableLink
className='btn btn-tertiary'
className={buttonClassNames({emphasis: 'tertiary'})}
to='/admin_console/compliance/data_retention_settings'
>
<FormattedMessage
@@ -5,7 +5,7 @@ import React from 'react';
import type {MessageDescriptor} from 'react-intl';
import {FormattedMessage} from 'react-intl';
import {Button} from '@mattermost/shared/components/button';
import {Button, buttonClassNames} from '@mattermost/shared/components/button';
import type {AnalyticsState} from '@mattermost/types/admin';
import type {CloudCustomer} from '@mattermost/types/cloud';
import type {ClientLicense} from '@mattermost/types/config';
@@ -106,7 +106,7 @@ export default class FeatureDiscovery extends React.PureComponent<Props, State>
</Button>
<ExternalLink
location='feature_discovery'
className='btn btn-tertiary btn-lg'
className={buttonClassNames({emphasis: 'tertiary', size: 'lg'})}
href={learnMoreURL}
data-testid='featureDiscovery_secondaryCallToAction'
>
@@ -126,10 +126,7 @@ export default class FeatureDiscovery extends React.PureComponent<Props, State>
// by default we assume is not cloud, so the cta button is Start Trial (which will request a trial license)
let ctaPrimaryButton = (
<StartTrialBtn
btnClass='btn btn-primary'
renderAsButton={true}
/>
<StartTrialBtn renderAsButton={true}/>
);
if (isCloud) {
@@ -155,7 +152,7 @@ export default class FeatureDiscovery extends React.PureComponent<Props, State>
{ctaPrimaryButton}
<ExternalLink
location='feature_discovery'
className='btn btn-secondary'
className={buttonClassNames({emphasis: 'secondary'})}
href={learnMoreURL}
data-testid='featureDiscovery_secondaryCallToAction'
>
@@ -4,7 +4,7 @@
import React from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {Button} from '@mattermost/shared/components/button';
import {Button, buttonClassNames} from '@mattermost/shared/components/button';
import SetupSystemSvg from 'components/common/svg_images_components/setup_system_svg';
import ExternalLink from 'components/external_link';
@@ -64,7 +64,7 @@ const TeamEditionRightPanel: React.FC<TeamEditionRightPanelProps> = ({
<ExternalLink
href={LicenseLinks.UNSUPPORTED_UPGRADE_LINK}
location='team_edition_right_panel'
className='btn btn-tertiary'
className={buttonClassNames({emphasis: 'tertiary'})}
role='button'
>
<FormattedMessage
@@ -6,6 +6,7 @@ import {FormattedMessage, defineMessages, useIntl} from 'react-intl';
import type {MessageDescriptor} from 'react-intl';
import {GenericModal} from '@mattermost/components';
import {buttonClassNames} from '@mattermost/shared/components/button';
import type {AccessControlPolicy, AccessControlPolicyRule} from '@mattermost/types/access_control';
import type {AccessControlSettings} from '@mattermost/types/config';
import type {UserPropertyField} from '@mattermost/types/properties';
@@ -741,7 +742,7 @@ function PermissionPolicyDetails({
}
/>
<BlockableLink
className='btn btn-quaternary'
className={buttonClassNames({emphasis: 'quaternary'})}
to='/admin_console/system_attributes/permission_policies'
>
<FormattedMessage
@@ -5,7 +5,7 @@ import React from 'react';
import type {WrappedComponentProps} from 'react-intl';
import {FormattedMessage, defineMessage, injectIntl} from 'react-intl';
import {Button} from '@mattermost/shared/components/button';
import {Button, buttonClassNames} from '@mattermost/shared/components/button';
import type {ClientConfig, ClientLicense} from '@mattermost/types/config';
import type {Role} from '@mattermost/types/roles';
@@ -537,7 +537,7 @@ export class PermissionSystemSchemeSettings extends React.PureComponent<Props, S
savingMessage={this.props.intl.formatMessage({id: 'admin.saving', defaultMessage: 'Saving Config...'})}
/>
<BlockableLink
className='btn btn-tertiary'
className={buttonClassNames({emphasis: 'tertiary'})}
to='/admin_console/user_management/permissions'
data-testid='permission-scheme-cancel-button'
>
@@ -6,6 +6,7 @@ import {defineMessage, FormattedMessage} from 'react-intl';
import type {WrappedComponentProps} from 'react-intl';
import type {RouteComponentProps} from 'react-router-dom';
import {buttonClassNames} from '@mattermost/shared/components/button';
import type {ClientConfig, ClientLicense} from '@mattermost/types/config';
import type {Role} from '@mattermost/types/roles';
import type {Scheme, SchemePatch} from '@mattermost/types/schemes';
@@ -815,7 +816,7 @@ export default class PermissionTeamSchemeSettings extends React.PureComponent<Pr
}
/>
<BlockableLink
className='btn btn-tertiary'
className={buttonClassNames({emphasis: 'tertiary'})}
to='/admin_console/user_management/permissions'
data-testid='permission-scheme-cancel-button'
>
@@ -4,6 +4,8 @@
import React from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {buttonClassNames} from '@mattermost/shared/components/button';
import BlockableButton from 'components/admin_console/blockable_button';
import BlockableLink from 'components/admin_console/blockable_link';
import SaveButton from 'components/save_button';
@@ -34,7 +36,7 @@ const SaveChangesPanel = ({saveNeeded, onClick, saving, serverError, cancelLink,
{cancelLink ? (
<BlockableLink
id='cancelButtonSettings'
className='btn btn-quaternary'
className={buttonClassNames({emphasis: 'quaternary'})}
to={cancelLink}
>
<FormattedMessage
@@ -45,7 +47,7 @@ const SaveChangesPanel = ({saveNeeded, onClick, saving, serverError, cancelLink,
) : onCancel && (
<BlockableButton
id='cancelButtonSettings'
className='btn btn-quaternary'
className={buttonClassNames({emphasis: 'quaternary'})}
onCancelConfirmed={onCancel}
>
<FormattedMessage
@@ -1,13 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import classNames from 'classnames';
import React from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {Link, useHistory} from 'react-router-dom';
import styled from 'styled-components';
import {DotsHorizontalIcon, CodeTagsIcon, PencilOutlineIcon, TrashCanOutlineIcon} from '@mattermost/compass-icons/components';
import {buttonClassNames} from '@mattermost/shared/components/button';
import type {RemoteCluster} from '@mattermost/types/remote_clusters';
import * as Menu from 'components/menu';
@@ -65,7 +65,7 @@ const RowMenu = ({remoteCluster: rc, onDeleteSuccess, disabled}: Props) => {
<Menu.Container
menuButton={{
id: `${menuId}-button-${rc.remote_id}`,
class: classNames('btn btn-tertiary btn-sm connection-row-menu-button', {disabled}),
class: buttonClassNames({emphasis: 'tertiary', size: 'sm'}, 'connection-row-menu-button', {disabled}),
disabled,
children: !disabled && <DotsHorizontalIcon size={16}/>,
'aria-label': formatMessage({id: 'admin.secure_connection_row.menu-button.aria_label', defaultMessage: 'Connection options for {connection}'}, {connection: rc.display_name}),
@@ -1,12 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import classNames from 'classnames';
import type {ReactNode} from 'react';
import React from 'react';
import {useIntl, FormattedMessage, defineMessages} from 'react-intl';
import {useHistory} from 'react-router-dom';
import type {ButtonEmphasis} from '@mattermost/shared/components/button';
import {buttonClassNames} from '@mattermost/shared/components/button';
import LoadingScreen from 'components/loading_screen';
import * as Menu from 'components/menu';
import SectionNotice from 'components/section_notice';
@@ -103,7 +105,7 @@ const Placeholder = ({disabled, serviceNotRunning}: {disabled: boolean; serviceN
/>
</hgroup>
<AddMenu
buttonClassNames='btn-tertiary'
buttonEmphasis='tertiary'
disabled={disabled}
/>
</PlaceholderContainer>
@@ -113,7 +115,7 @@ const Placeholder = ({disabled, serviceNotRunning}: {disabled: boolean; serviceN
const menuId = 'secure_connections_add_menu';
const AddMenu = ({buttonClassNames, disabled}: {buttonClassNames?: string; disabled: boolean}) => {
const AddMenu = ({buttonEmphasis = 'primary', disabled}: {buttonEmphasis?: ButtonEmphasis; disabled: boolean}) => {
const {formatMessage} = useIntl();
const history = useHistory();
const {promptAcceptInvite} = useRemoteClusterAcceptInvite();
@@ -133,7 +135,7 @@ const AddMenu = ({buttonClassNames, disabled}: {buttonClassNames?: string; disab
<Menu.Container
menuButton={{
id: `${menuId}-button`,
class: classNames('btn', buttonClassNames ?? 'btn-primary', {disabled}),
class: buttonClassNames({emphasis: buttonEmphasis}, {disabled}),
disabled,
children: (
<>
@@ -5,7 +5,7 @@ import debounce from 'lodash/debounce';
import React from 'react';
import {FormattedMessage, defineMessages} from 'react-intl';
import {Button} from '@mattermost/shared/components/button';
import {Button, buttonClassNames} from '@mattermost/shared/components/button';
import type {
LogFilter,
LogLevels,
@@ -244,7 +244,7 @@ export default class Logs extends React.PureComponent<Props, State> {
</Button>
<ExternalLink
location='download_logs'
className='btn btn-primary'
className={buttonClassNames({emphasis: 'primary'})}
href={Client4.getUrl() + '/api/v4/logs/download'}
>
<FormattedMessage
@@ -4,6 +4,7 @@
import React from 'react';
import {FormattedMessage, defineMessage} from 'react-intl';
import {buttonClassNames} from '@mattermost/shared/components/button';
import type {Role} from '@mattermost/types/roles';
import type {UserProfile, UsersStats, GetFilteredUsersStatsOpts} from '@mattermost/types/users';
@@ -248,7 +249,7 @@ export default class SystemRoleUsers extends React.PureComponent<Props, State> {
button={
<ToggleModalButton
id='addRoleMembers'
className='btn btn-primary'
className={buttonClassNames({emphasis: 'primary'})}
modalId={ModalIdentifiers.ADD_USER_TO_ROLE}
dialogType={AddUsersToRoleModal}
disabled={readOnly}
@@ -1,11 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import classNames from 'classnames';
import React, {useCallback, useMemo} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import {buttonClassNames} from '@mattermost/shared/components/button';
import type {ServerError} from '@mattermost/types/errors';
import type {UserProfile} from '@mattermost/types/users';
@@ -349,7 +349,7 @@ export function SystemUsersListAction({user, currentUser, tableId, rowIndex, onE
<Menu.Container
menuButton={{
id: menuButtonId,
class: classNames('btn btn-quaternary btn-sm', {
class: buttonClassNames({emphasis: 'quaternary', size: 'sm'}, {
disabled: disableEditingOtherUsers,
}),
disabled: disableEditingOtherUsers,
@@ -4,6 +4,7 @@
import React from 'react';
import {FormattedMessage, defineMessage} from 'react-intl';
import {buttonClassNames} from '@mattermost/shared/components/button';
import type {Channel} from '@mattermost/types/channels';
import type {Group} from '@mattermost/types/groups';
@@ -43,7 +44,7 @@ export const ChannelGroups: React.FunctionComponent<ChannelGroupsProps> = (props
button={
<ToggleModalButton
id='addGroupsToChannelToggle'
className='btn btn-primary'
className={buttonClassNames({emphasis: 'primary'})}
modalId={ModalIdentifiers.ADD_GROUPS_TO_CHANNEL}
dialogType={AddGroupsToChannelModal}
dialogProps={{
@@ -4,6 +4,7 @@
import React from 'react';
import {FormattedMessage, defineMessage} from 'react-intl';
import {buttonClassNames} from '@mattermost/shared/components/button';
import type {Channel, ChannelMembership} from '@mattermost/types/channels';
import type {UserProfile, GetFilteredUsersStatsOpts} from '@mattermost/types/users';
@@ -239,7 +240,7 @@ export default class ChannelMembers extends React.PureComponent<Props, State> {
button={
<ToggleModalButton
id='addChannelMembers'
className='btn btn-primary'
className={buttonClassNames({emphasis: 'primary'})}
modalId={ModalIdentifiers.CHANNEL_INVITE}
dialogType={ChannelInviteModal}
disabled={isDisabled}
@@ -4,6 +4,7 @@
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {buttonClassNames} from '@mattermost/shared/components/button';
import type {UserProfile} from '@mattermost/types/users';
import FormError from 'components/form_error';
@@ -88,7 +89,7 @@ export const UsersWillBeRemovedError = ({users, total, scope, scopeId}: UsersWil
<span>
{error}
<ToggleModalButton
className='btn btn-tertiary'
className={buttonClassNames({emphasis: 'tertiary'})}
modalId={ModalIdentifiers.USERS_TO_BE_REMOVED}
dialogType={UsersToBeRemovedModal}
dialogProps={{total, users, scope, scopeId}}
@@ -4,6 +4,7 @@
import React from 'react';
import {FormattedMessage, defineMessage} from 'react-intl';
import {buttonClassNames} from '@mattermost/shared/components/button';
import type {Group} from '@mattermost/types/groups';
import type {Team} from '@mattermost/types/teams';
@@ -43,7 +44,7 @@ export const TeamGroups = ({onGroupRemoved, syncChecked, team, onAddCallback, to
button={
<ToggleModalButton
id='addGroupsToTeamToggle'
className='btn btn-primary'
className={buttonClassNames({emphasis: 'primary'})}
modalId={ModalIdentifiers.ADD_GROUPS_TO_TEAM}
dialogType={AddGroupsToTeamModal}
dialogProps={{
@@ -4,6 +4,7 @@
import React from 'react';
import {FormattedMessage, defineMessage} from 'react-intl';
import {buttonClassNames} from '@mattermost/shared/components/button';
import type {TeamMembership, Team} from '@mattermost/types/teams';
import type {UserProfile, GetFilteredUsersStatsOpts} from '@mattermost/types/users';
@@ -240,7 +241,7 @@ export default class TeamMembers extends React.PureComponent<Props, State> {
button={
<ToggleModalButton
id='addTeamMembers'
className='btn btn-primary'
className={buttonClassNames({emphasis: 'primary'})}
modalId={ModalIdentifiers.ADD_USER_TO_TEAM}
dialogType={AddUsersToTeamModal}
disabled={isDisabled}
@@ -1,10 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import classNames from 'classnames';
import React, {useCallback, useMemo} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {buttonClassNames} from '@mattermost/shared/components/button';
import type {RemoteCluster} from '@mattermost/types/remote_clusters';
import * as Menu from 'components/menu';
@@ -41,7 +41,7 @@ export default function AddWorkspaceDropdown({
const menuButton = useMemo(() => ({
id: `${MENU_ID}-button`,
class: classNames('btn', 'btn-sm', 'btn-tertiary', 'ShareChannelWithWorkspaces__addBtn'),
class: buttonClassNames({emphasis: 'tertiary', size: 'sm'}, 'ShareChannelWithWorkspaces__addBtn'),
children: (
<>
<i
@@ -6,7 +6,7 @@ import type {ChangeEvent, FormEvent, SyntheticEvent} from 'react';
import {defineMessage, FormattedMessage} from 'react-intl';
import {Link} from 'react-router-dom';
import {Button} from '@mattermost/shared/components/button';
import {Button, buttonClassNames} from '@mattermost/shared/components/button';
import type {CustomEmoji} from '@mattermost/types/emojis';
import type {Team} from '@mattermost/types/teams';
import type {UserProfile} from '@mattermost/types/users';
@@ -376,7 +376,7 @@ export default class AddEmoji extends React.PureComponent<AddEmojiProps, AddEmoj
error={this.state.error}
/>
<Link
className='btn btn-tertiary'
className={buttonClassNames({emphasis: 'tertiary'})}
to={'/' + this.props.team.name + '/emoji'}
>
<FormattedMessage
@@ -5,6 +5,8 @@ import React, {memo} from 'react';
import {FormattedMessage} from 'react-intl';
import {Link} from 'react-router-dom';
import {buttonClassNames} from '@mattermost/shared/components/button';
import Permissions from 'mattermost-redux/constants/permissions';
import AnyTeamPermissionGate from 'components/permissions_gates/any_team_permission_gate';
@@ -28,7 +30,7 @@ function EmojiPickerCustomEmojiButton({customEmojisEnabled, currentTeamName, onC
<AnyTeamPermissionGate permissions={[Permissions.CREATE_EMOJIS]}>
<div className='emoji-picker__custom'>
<Link
className='btn btn-tertiary btn-sm'
className={buttonClassNames({emphasis: 'tertiary', size: 'sm'})}
to={`/${currentTeamName}/emoji`}
onClick={onClick}
>
@@ -131,7 +131,6 @@ const FeatureRestrictedModal = ({
const trialBtn = (
<StartTrialBtn
onClick={dismissAction}
btnClass='btn btn-primary'
renderAsButton={true}
/>
);
@@ -6,6 +6,7 @@ import type {ChangeEvent} from 'react';
import {defineMessage, FormattedMessage, type MessageDescriptor} from 'react-intl';
import {Link} from 'react-router-dom';
import {buttonClassNames} from '@mattermost/shared/components/button';
import type {Command} from '@mattermost/types/integrations';
import type {Team} from '@mattermost/types/teams';
@@ -678,7 +679,7 @@ export default class AbstractCommand extends React.PureComponent<Props, State> {
errors={[this.props.serverError, this.state.clientError]}
/>
<Link
className='btn btn-tertiary'
className={buttonClassNames({emphasis: 'tertiary'})}
to={'/' + this.props.team.name + '/integrations/commands'}
>
<FormattedMessage
@@ -7,6 +7,7 @@ import {FormattedMessage} from 'react-intl';
import type {MessageDescriptor} from 'react-intl';
import {Link} from 'react-router-dom';
import {buttonClassNames} from '@mattermost/shared/components/button';
import type {IncomingWebhook} from '@mattermost/types/integrations';
import type {Team} from '@mattermost/types/teams';
@@ -376,7 +377,7 @@ export default class AbstractIncomingWebhook extends PureComponent<Props, State>
errors={[this.props.serverError, this.state.clientError]}
/>
<Link
className='btn btn-tertiary'
className={buttonClassNames({emphasis: 'tertiary'})}
to={`/${this.props.team.name}/integrations/incoming_webhooks`}
>
<FormattedMessage
@@ -7,6 +7,7 @@ import {FormattedMessage} from 'react-intl';
import type {MessageDescriptor} from 'react-intl';
import {Link} from 'react-router-dom';
import {buttonClassNames} from '@mattermost/shared/components/button';
import type {OAuthApp} from '@mattermost/types/integrations';
import type {Team} from '@mattermost/types/teams';
@@ -530,7 +531,7 @@ export default class AbstractOAuthApp extends React.PureComponent<Props, State>
errors={[this.props.serverError, this.state.clientError]}
/>
<Link
className='btn btn-tertiary'
className={buttonClassNames({emphasis: 'tertiary'})}
to={`/${this.props.team.name}/integrations/oauth2-apps`}
>
<FormattedMessage
@@ -7,6 +7,7 @@ import {FormattedMessage} from 'react-intl';
import type {MessageDescriptor} from 'react-intl';
import {Link} from 'react-router-dom';
import {buttonClassNames} from '@mattermost/shared/components/button';
import type {OutgoingWebhook} from '@mattermost/types/integrations';
import type {Team} from '@mattermost/types/teams';
@@ -582,7 +583,7 @@ export default class AbstractOutgoingWebhook extends React.PureComponent<Props,
errors={[this.props.serverError, this.state.clientError]}
/>
<Link
className='btn btn-tertiary'
className={buttonClassNames({emphasis: 'tertiary'})}
to={`/${this.props.team.name}/integrations/outgoing_webhooks`}
>
<FormattedMessage
@@ -6,6 +6,7 @@ import type {ChangeEvent, FormEvent} from 'react';
import {FormattedMessage} from 'react-intl';
import {Link} from 'react-router-dom';
import {buttonClassNames} from '@mattermost/shared/components/button';
import {WithTooltip} from '@mattermost/shared/components/tooltip';
import type {Bot, BotPatch} from '@mattermost/types/bots';
import type {Team} from '@mattermost/types/teams';
@@ -517,7 +518,7 @@ export default class AddBot extends React.PureComponent<Props, State> {
{removeImageIcon}
</div>
<div
className='btn btn-primary btn-file'
className={buttonClassNames({emphasis: 'primary'}, 'btn-file')}
>
<FormattedMessage
id='bots.image.upload'
@@ -714,7 +715,7 @@ export default class AddBot extends React.PureComponent<Props, State> {
errors={[this.state.error]}
/>
<Link
className='btn btn-tertiary'
className={buttonClassNames({emphasis: 'tertiary'})}
to={`/${this.props.team.name}/integrations/bots`}
>
<FormattedMessage
@@ -5,6 +5,7 @@ import React, {useEffect} from 'react';
import {defineMessages, FormattedMessage} from 'react-intl';
import {Link, useHistory} from 'react-router-dom';
import {buttonClassNames} from '@mattermost/shared/components/button';
import type {Bot} from '@mattermost/types/bots';
import type {Command, IncomingWebhook, OAuthApp, OutgoingOAuthConnection, OutgoingWebhook} from '@mattermost/types/integrations';
import type {Team} from '@mattermost/types/teams';
@@ -493,7 +494,7 @@ const ConfirmIntegration = ({team, location, commands, oauthApps, incomingHooks,
{tokenText}
<div className='backstage-form__footer'>
<Link
className='btn btn-primary'
className={buttonClassNames({emphasis: 'primary'})}
type='submit'
to={'/' + team.name + '/integrations/' + type}
id='doneButton'
@@ -9,7 +9,7 @@ import {useDispatch} from 'react-redux';
import {Link} from 'react-router-dom';
import {AlertOutlineIcon, CheckCircleOutlineIcon} from '@mattermost/compass-icons/components';
import {Button} from '@mattermost/shared/components/button';
import {Button, buttonClassNames} from '@mattermost/shared/components/button';
import type {OutgoingOAuthConnection} from '@mattermost/types/integrations';
import type {Team} from '@mattermost/types/teams';
@@ -502,7 +502,7 @@ export default function AbstractOutgoingOAuthConnection(props: Props) {
errors={[props.serverError, storedError]}
/>
<Link
className='btn btn-tertiary'
className={buttonClassNames({emphasis: 'tertiary'})}
to={`/${props.team.name}/integrations/outgoing-oauth2-connections`}
>
<FormattedMessage
@@ -4,19 +4,25 @@
import React from 'react';
import {useIntl} from 'react-intl';
import {buttonClassNames} from '@mattermost/shared/components/button';
import useOpenStartTrialFormModal from 'components/common/hooks/useOpenStartTrialFormModal';
import './start_trial_btn.scss';
export type StartTrialBtnProps = {
onClick?: () => void;
btnClass?: string;
renderAsButton?: boolean;
disabled?: boolean;
};
} & ({
btnClass?: string;
renderAsButton: true;
} | {
btnClass?: never;
renderAsButton?: false;
});
const StartTrialBtn = ({
btnClass,
btnClass = buttonClassNames({emphasis: 'primary'}),
onClick,
disabled = false,
renderAsButton = false,
@@ -52,7 +58,7 @@ const StartTrialBtn = ({
) : (
<a
id={id}
className='btn btn-secondary'
className={buttonClassNames({emphasis: 'secondary'})}
onClick={startTrial}
>
{btnText}
@@ -4,6 +4,7 @@
import React, {PureComponent} from 'react';
import {FormattedMessage} from 'react-intl';
import {buttonClassNames} from '@mattermost/shared/components/button';
import * as UserAgent from '@mattermost/shared/utils/user_agent';
import BrowserStore from 'stores/browser_store';
@@ -211,7 +212,7 @@ export default class LinkingLandingPage extends PureComponent<Props, State> {
window.location.replace(this.state.nativeLocation);
}
}}
className='btn btn-primary btn-lg get-app__download'
className={buttonClassNames({emphasis: 'primary', size: 'lg'}, 'get-app__download')}
>
{this.renderSystemDialogMessage()}
</a>
@@ -412,7 +413,7 @@ export default class LinkingLandingPage extends PureComponent<Props, State> {
this.setPreference(LandingPreferenceTypes.BROWSER, true);
this.setState({navigating: true});
}}
className='btn btn-tertiary btn-lg'
className={buttonClassNames({emphasis: 'tertiary', size: 'lg'})}
>
<FormattedMessage
id='get_app.continueToBrowser'
@@ -5,6 +5,7 @@ import React from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {useSelector} from 'react-redux';
import {buttonClassNames} from '@mattermost/shared/components/button';
import type {Channel} from '@mattermost/types/channels';
import {Permissions} from 'mattermost-redux/constants';
@@ -66,7 +67,7 @@ const LessThanMaxFreeUsers = ({pluginButtons}: {pluginButtons: React.ReactNode})
<div className='LessThanMaxFreeUsers'>
<ToggleModalButton
id='introTextInvite'
className='btn btn-sm btn-primary'
className={buttonClassNames({emphasis: 'primary', size: 'sm'})}
modalId={ModalIdentifiers.INVITATION}
dialogType={InvitationModal}
dialogProps={{focusOriginElement: 'browseOrAddChannelMenuButton'}}
@@ -17,6 +17,7 @@ import {
PlusIcon,
MonitorAccountIcon,
} from '@mattermost/compass-icons/components';
import {buttonClassNames} from '@mattermost/shared/components/button';
import type {Team} from '@mattermost/types/teams';
import {Permissions} from 'mattermost-redux/constants';
@@ -72,7 +73,7 @@ export default function SidebarTeamMenu(props: Props) {
<Menu.Container
menuButton={{
id: 'sidebarTeamMenuButton',
class: 'btn btn-sm btn-quaternary btn-inverted',
class: buttonClassNames({emphasis: 'quaternary', size: 'sm'}, 'btn-inverted'),
children: (
<>
<span>{props.currentTeam.display_name}</span>
@@ -8,6 +8,7 @@ import type {IntlShape} from 'react-intl';
import {FormattedDate, FormattedMessage, FormattedTime, injectIntl} from 'react-intl';
import {Link} from 'react-router-dom';
import {buttonClassNames} from '@mattermost/shared/components/button';
import type {OAuthApp} from '@mattermost/types/integrations';
import type {UserProfile} from '@mattermost/types/users';
@@ -544,7 +545,7 @@ export class SecurityTab extends React.PureComponent<Props, State> {
gitlabOption = (
<div className='pb-3'>
<Link
className='btn btn-primary'
className={buttonClassNames({emphasis: 'primary'})}
to={
'/claim/email_to_oauth?email=' +
encodeURIComponent(user.email) +
@@ -568,7 +569,7 @@ export class SecurityTab extends React.PureComponent<Props, State> {
googleOption = (
<div className='pb-3'>
<Link
className='btn btn-primary'
className={buttonClassNames({emphasis: 'primary'})}
to={
'/claim/email_to_oauth?email=' +
encodeURIComponent(user.email) +
@@ -592,7 +593,7 @@ export class SecurityTab extends React.PureComponent<Props, State> {
office365Option = (
<div className='pb-3'>
<Link
className='btn btn-primary'
className={buttonClassNames({emphasis: 'primary'})}
to={
'/claim/email_to_oauth?email=' +
encodeURIComponent(user.email) +
@@ -616,7 +617,7 @@ export class SecurityTab extends React.PureComponent<Props, State> {
openidOption = (
<div className='pb-3'>
<Link
className='btn btn-primary'
className={buttonClassNames({emphasis: 'primary'})}
to={
'/claim/email_to_oauth?email=' +
encodeURIComponent(user.email) +
@@ -640,7 +641,7 @@ export class SecurityTab extends React.PureComponent<Props, State> {
ldapOption = (
<div className='pb-3'>
<Link
className='btn btn-primary'
className={buttonClassNames({emphasis: 'primary'})}
to={
'/claim/email_to_ldap?email=' +
encodeURIComponent(user.email)
@@ -660,7 +661,7 @@ export class SecurityTab extends React.PureComponent<Props, State> {
samlOption = (
<div className='pb-3'>
<Link
className='btn btn-primary'
className={buttonClassNames({emphasis: 'primary'})}
to={
'/claim/email_to_oauth?email=' +
encodeURIComponent(user.email) +
@@ -696,7 +697,7 @@ export class SecurityTab extends React.PureComponent<Props, State> {
emailOption = (
<div className='pb-3'>
<Link
className='btn btn-primary'
className={buttonClassNames({emphasis: 'primary'})}
to={link}
>
<FormattedMessage
@@ -1,12 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import classNames from 'classnames';
import React from 'react';
import type {MessageDescriptor} from 'react-intl';
import {FormattedMessage} from 'react-intl';
import {Link} from 'react-router-dom';
import {buttonClassNames} from '@mattermost/shared/components/button';
import AdminPanel from './admin_panel';
type Props = {
@@ -35,7 +36,7 @@ const AdminPanelWithLink = ({
const button = (
<Link
data-testid={`${id}-link`}
className={classNames(['btn', 'btn-primary', {disabled}])}
className={buttonClassNames({emphasis: 'primary'}, {disabled})}
to={url}
onClick={disabled ? (e) => e.preventDefault() : () => null}
>
@@ -1,12 +1,9 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import classNames from 'classnames';
import React from 'react';
export type ButtonEmphasis = 'primary' | 'secondary' | 'tertiary' | 'quaternary';
export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg';
export type ButtonVariant = '' | 'destructive' | 'inverted';
import {buttonClassNames, type ButtonEmphasis, type ButtonSize, type ButtonVariant} from './button_classes';
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
@@ -33,46 +30,20 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
variant?: ButtonVariant;
}
const emphasisClasses = {
primary: 'btn-primary',
secondary: 'btn-secondary',
tertiary: 'btn-tertiary',
quaternary: 'btn-quaternary',
};
const sizeClasses = {
xs: 'btn-xs',
sm: 'btn-sm',
md: '',
lg: 'btn-lg',
};
const variantClasses = {
'': '',
destructive: 'btn-danger',
inverted: 'btn-inverted',
};
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({
children,
className,
emphasis = 'primary',
size = 'md',
variant = '',
emphasis,
size,
variant,
...otherProps
}, ref) => {
let emphasisClass = emphasisClasses[emphasis];
const sizeClass = sizeClasses[size];
const variantClass = variantClasses[variant];
if (emphasis === 'primary' && variant === 'destructive') {
// TODO in the current CSS, btn-primary overrides btn-danger
emphasisClass = '';
}
return (
<button
ref={ref}
className={classNames('btn', emphasisClass, sizeClass, variantClass, className)}
className={buttonClassNames({emphasis, size, variant}, className)}
{...otherProps}
>
{children}
@@ -0,0 +1,45 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import classNames from 'classnames';
export type ButtonEmphasis = 'primary' | 'secondary' | 'tertiary' | 'quaternary';
export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg';
export type ButtonVariant = '' | 'destructive' | 'inverted';
const emphasisClasses = {
primary: 'btn-primary',
secondary: 'btn-secondary',
tertiary: 'btn-tertiary',
quaternary: 'btn-quaternary',
};
const sizeClasses = {
xs: 'btn-xs',
sm: 'btn-sm',
md: '',
lg: 'btn-lg',
};
const variantClasses = {
'': '',
destructive: 'btn-danger',
inverted: 'btn-inverted',
};
export interface ButtonStyles {
emphasis?: ButtonEmphasis;
size?: ButtonSize;
variant?: ButtonVariant;
}
export function buttonClassNames({emphasis = 'primary', size = 'md', variant = ''}: ButtonStyles, ...other: classNames.ArgumentArray) {
let emphasisClass = emphasisClasses[emphasis];
const sizeClass = sizeClasses[size];
const variantClass = variantClasses[variant];
if (emphasis === 'primary' && variant === 'destructive') {
// TODO in the current CSS, btn-primary overrides btn-danger
emphasisClass = '';
}
return classNames('btn', emphasisClass, sizeClass, variantClass, other);
}
@@ -2,4 +2,6 @@
// See LICENSE.txt for license information.
export {Button} from './button';
export type {ButtonEmphasis, ButtonProps, ButtonSize, ButtonVariant} from './button';
export type {ButtonProps} from './button';
export {buttonClassNames} from './button_classes';
export type {ButtonEmphasis, ButtonSize, ButtonVariant} from './button_classes';