From 10ad2505a787269d9a5505f55259778d03b8a397 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Wed, 6 May 2026 17:35:10 -0400 Subject: [PATCH] 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 --- .../policy_details/policy_details.tsx | 3 +- .../cancel_subscription.tsx | 4 +- .../contact_sales_card.tsx | 4 +- .../billing_summary/billing_summary.tsx | 7 +-- .../billing/company_info_display.tsx | 6 ++- .../billing/company_info_edit.tsx | 4 +- .../classification_markings_styled.tsx | 6 ++- .../custom_policy_form/custom_policy_form.tsx | 3 +- .../global_policy_form/global_policy_form.tsx | 3 +- .../feature_discovery/feature_discovery.tsx | 11 ++--- .../team_edition/team_edition_right_panel.tsx | 4 +- .../permission_policy_details.tsx | 3 +- .../permission_system_scheme_settings.tsx | 4 +- .../permission_team_scheme_settings.tsx | 3 +- .../admin_console/save_changes_panel.tsx | 6 ++- .../secure_connection_row.tsx | 4 +- .../secure_connections/secure_connections.tsx | 10 +++-- .../admin_console/server_logs/logs.tsx | 4 +- .../system_role_users/system_role_users.tsx | 3 +- .../system_users_list_actions/index.tsx | 4 +- .../channel/details/channel_groups.tsx | 3 +- .../channel_members/channel_members.tsx | 3 +- .../team_channel_settings/errors.tsx | 3 +- .../team/details/team_groups.tsx | 3 +- .../details/team_members/team_members.tsx | 3 +- .../add_workspace_dropdown.tsx | 4 +- .../components/emoji/add_emoji/add_emoji.tsx | 4 +- .../emoji_picker_custom_emoji_button.tsx | 4 +- .../feature_restricted_modal.tsx | 1 - .../integrations/abstract_command.tsx | 3 +- .../abstract_incoming_webhook.tsx | 3 +- .../integrations/abstract_oauth_app.tsx | 3 +- .../abstract_outgoing_webhook.tsx | 3 +- .../integrations/bots/add_bot/add_bot.tsx | 5 ++- .../confirm_integration.tsx | 3 +- .../abstract_outgoing_oauth_connection.tsx | 4 +- .../start_trial_btn.tsx | 16 ++++--- .../linking_landing_page.tsx | 5 ++- .../add_members_button.tsx | 3 +- .../sidebar_header/sidebar_team_menu.tsx | 3 +- .../security/user_settings_security.tsx | 15 ++++--- .../admin_console/admin_panel_with_link.tsx | 5 ++- .../shared/src/components/button/button.tsx | 41 +++-------------- .../src/components/button/button_classes.ts | 45 +++++++++++++++++++ .../shared/src/components/button/index.ts | 4 +- 45 files changed, 171 insertions(+), 114 deletions(-) create mode 100644 webapp/platform/shared/src/components/button/button_classes.ts diff --git a/webapp/channels/src/components/admin_console/access_control/policy_details/policy_details.tsx b/webapp/channels/src/components/admin_console/access_control/policy_details/policy_details.tsx index 7e6a38e8c49..3195522fb8d 100644 --- a/webapp/channels/src/components/admin_console/access_control/policy_details/policy_details.tsx +++ b/webapp/channels/src/components/admin_console/access_control/policy_details/policy_details.tsx @@ -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({ } /> { { { @@ -126,10 +126,7 @@ export default class FeatureDiscovery extends React.PureComponent // by default we assume is not cloud, so the cta button is Start Trial (which will request a trial license) let ctaPrimaryButton = ( - + ); if (isCloud) { @@ -155,7 +152,7 @@ export default class FeatureDiscovery extends React.PureComponent {ctaPrimaryButton} diff --git a/webapp/channels/src/components/admin_console/license_settings/team_edition/team_edition_right_panel.tsx b/webapp/channels/src/components/admin_console/license_settings/team_edition/team_edition_right_panel.tsx index ece2940fc10..35373776357 100644 --- a/webapp/channels/src/components/admin_console/license_settings/team_edition/team_edition_right_panel.tsx +++ b/webapp/channels/src/components/admin_console/license_settings/team_edition/team_edition_right_panel.tsx @@ -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 = ({ diff --git a/webapp/channels/src/components/admin_console/permission_schemes_settings/permission_team_scheme_settings/permission_team_scheme_settings.tsx b/webapp/channels/src/components/admin_console/permission_schemes_settings/permission_team_scheme_settings/permission_team_scheme_settings.tsx index 7322650449e..da8d2859ff2 100644 --- a/webapp/channels/src/components/admin_console/permission_schemes_settings/permission_team_scheme_settings/permission_team_scheme_settings.tsx +++ b/webapp/channels/src/components/admin_console/permission_schemes_settings/permission_team_scheme_settings/permission_team_scheme_settings.tsx @@ -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 diff --git a/webapp/channels/src/components/admin_console/save_changes_panel.tsx b/webapp/channels/src/components/admin_console/save_changes_panel.tsx index 8f97d087e74..96287ab2c05 100644 --- a/webapp/channels/src/components/admin_console/save_changes_panel.tsx +++ b/webapp/channels/src/components/admin_console/save_changes_panel.tsx @@ -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 ? ( { , 'aria-label': formatMessage({id: 'admin.secure_connection_row.menu-button.aria_label', defaultMessage: 'Connection options for {connection}'}, {connection: rc.display_name}), diff --git a/webapp/channels/src/components/admin_console/secure_connections/secure_connections.tsx b/webapp/channels/src/components/admin_console/secure_connections/secure_connections.tsx index 1d391cb33da..fd4af0147f4 100644 --- a/webapp/channels/src/components/admin_console/secure_connections/secure_connections.tsx +++ b/webapp/channels/src/components/admin_console/secure_connections/secure_connections.tsx @@ -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 /> @@ -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 diff --git a/webapp/channels/src/components/admin_console/server_logs/logs.tsx b/webapp/channels/src/components/admin_console/server_logs/logs.tsx index 892228503bd..4294275d415 100644 --- a/webapp/channels/src/components/admin_console/server_logs/logs.tsx +++ b/webapp/channels/src/components/admin_console/server_logs/logs.tsx @@ -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 { { button={ = (props button={ { button={ {error} { button={ ({ id: `${MENU_ID}-button`, - class: classNames('btn', 'btn-sm', 'btn-tertiary', 'ShareChannelWithWorkspaces__addBtn'), + class: buttonClassNames({emphasis: 'tertiary', size: 'sm'}, 'ShareChannelWithWorkspaces__addBtn'), children: ( <>
diff --git a/webapp/channels/src/components/feature_restricted_modal/feature_restricted_modal.tsx b/webapp/channels/src/components/feature_restricted_modal/feature_restricted_modal.tsx index 19f2e4938cf..fefe895d36f 100644 --- a/webapp/channels/src/components/feature_restricted_modal/feature_restricted_modal.tsx +++ b/webapp/channels/src/components/feature_restricted_modal/feature_restricted_modal.tsx @@ -131,7 +131,6 @@ const FeatureRestrictedModal = ({ const trialBtn = ( ); diff --git a/webapp/channels/src/components/integrations/abstract_command.tsx b/webapp/channels/src/components/integrations/abstract_command.tsx index 1ea5a95bff5..868937f67f9 100644 --- a/webapp/channels/src/components/integrations/abstract_command.tsx +++ b/webapp/channels/src/components/integrations/abstract_command.tsx @@ -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 { errors={[this.props.serverError, this.state.clientError]} /> errors={[this.props.serverError, this.state.clientError]} /> errors={[this.props.serverError, this.state.clientError]} /> { {removeImageIcon}
{ errors={[this.state.error]} /> 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 = ({ ) : ( {btnText} diff --git a/webapp/channels/src/components/linking_landing_page/linking_landing_page.tsx b/webapp/channels/src/components/linking_landing_page/linking_landing_page.tsx index 015d337e994..79f91b8c82a 100644 --- a/webapp/channels/src/components/linking_landing_page/linking_landing_page.tsx +++ b/webapp/channels/src/components/linking_landing_page/linking_landing_page.tsx @@ -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 { 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()} @@ -412,7 +413,7 @@ export default class LinkingLandingPage extends PureComponent { this.setPreference(LandingPreferenceTypes.BROWSER, true); this.setState({navigating: true}); }} - className='btn btn-tertiary btn-lg' + className={buttonClassNames({emphasis: 'tertiary', size: 'lg'})} > {props.currentTeam.display_name} diff --git a/webapp/channels/src/components/user_settings/security/user_settings_security.tsx b/webapp/channels/src/components/user_settings/security/user_settings_security.tsx index e1ca2eff228..656128d8357 100644 --- a/webapp/channels/src/components/user_settings/security/user_settings_security.tsx +++ b/webapp/channels/src/components/user_settings/security/user_settings_security.tsx @@ -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 { gitlabOption = (
e.preventDefault() : () => null} > diff --git a/webapp/platform/shared/src/components/button/button.tsx b/webapp/platform/shared/src/components/button/button.tsx index 1a7dacbd71d..61a9a59d01f 100644 --- a/webapp/platform/shared/src/components/button/button.tsx +++ b/webapp/platform/shared/src/components/button/button.tsx @@ -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 { children: React.ReactNode; @@ -33,46 +30,20 @@ export interface ButtonProps extends React.ButtonHTMLAttributes(({ 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 (