Migrate Toggle to Compass Switch (#39038)

* Migrate Toggle component to Compass Switch adapter

Rewrite components/toggle.tsx to wrap @mattermost/compass-ui Switch with
prop mapping and sibling labels for onText/offText. Update Jest tests and
snapshots for switch/checkbox semantics (role=switch, checked state).

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Place Toggle onText/offText on Compass Switch label

Pass state text through Switch children so Public/Private sits
left of the control and remains clickable for accessibility.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Use medium Compass Switch for plugin enable control

PluginEnableButton Toggle size btn-sm → btn-md (Compass Switch medium).

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Stop forwarding legacy btn-toggle classes onto Compass Switch

Toggle is a Compass Switch adapter; call sites still passed
toggleClassName='btn-toggle-primary', which painted the old gray
rectangular track chrome from _toggle.scss behind the Switch.
Remove the prop, drop those call-site classNames, and delete the
unused legacy toggle stylesheet.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Fix post priority Switch double-toggle in menu items

Clicking the Compass Switch inside ToggleItem fired both Switch onChange
and MenuItem onClick for the same gesture, so the handler ran twice and
the control appeared stuck. Stop click propagation on the switch and add
a unit test covering switch and row activation.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Use Compass Switch settings-row for channel settings toggles

Replace custom __text + Toggle header rows with Switch children +
secondaryLabel for configuration, discoverable, and shared-channels
sections. Drop obsolete header SCSS; keep existing testids/behavior.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Migrate Admin Console LineSwitch rows to Compass Switch

Replace Toggle-based LineSwitch with AdminSettingsSwitch (children +
secondaryLabel), covering group mentions, team modes, and channel modes.
Public/Private uses state text as the primary label; domain input stays
as a footer under the switch. Delete unused LineSwitch styles and update
Cypress selectors for role=switch inputs.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Migrate remaining Toggle call sites to Compass Switch

Replace production Toggle adapters with direct @mattermost/compass-ui
Switch usage across new channel modal, post priority, recaps, plugin
enable, user properties, auto-translation, IP filtering, and content
flagging reviewers. Delete the unused Toggle component and its tests.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Document medium Switch size for Admin Console Public/Private row

AdminSettingsSwitch intentionally uses Compass medium (14px) + semiBold
so short state labels like Public/Private match Channel Settings and do
not inherit the old Toggle btn-lg → large (16px) default.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Fix Team Modes separator when Sync Group Members is on

Omit the Sync row section separator only when no later row is visible.
ABAC still renders disabled while synced, so last must not track isSynced alone.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Use static Public/Private Switch title in Channel Management

Stop swapping Switch children between "Public" and "Private". Keep a
static "Public channel or private channel" label with help text as
secondaryLabel; checked remains public and unchecked private. Update
Cypress assertions and channel_modes snapshots accordingly.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Show current Public/Private mode on channel Switch label

Use a dynamic primary label (Public channel / Private channel) so
checked state is scannable without a segmented control.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Update Cypress for dynamic Public/Private Switch labels

Assert Public channel when checked and Private channel when unchecked.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Fix plugin enable Switch alignment next to actions menu

Zero Bootstrap label margin-bottom and Switch width:100% on the
PluginMetadataPanel enable control so it lines up with the ⋯ button.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Use segmented Public/Private control in Channel Management

Replace the ambiguous Switch for channel privacy with a temporary
AdminSettingsSegmentedRow (Public | Private) until Compass ships a
segmented control. Update Cypress selectors accordingly.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Fix IP Filtering enable Switch not revealing edit config

Use event.checked for the Compass Switch, skip the filterToggle sync
effect while ipFilters is still null so an empty [] does not race the
fetch, and show EditSection whenever filters are loaded and the toggle
is on (do not wait on currentUsersIP).

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Increase Public/Private segmented control border radii

Round the track to 8px and active segments to 4px for a softer
Channel Management Public/Private control.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Increase Public/Private segmented control track padding

Bump &__segments padding from 2px to 4px.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Soften Public/Private segmented control border

Lower track border opacity from 0.16 to 0.08.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Darken Public/Private segmented control track background

Raise &__segments background opacity from 0.04 to 0.08.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Address review comments on Switch a11y and formatting

Align LDAP Cypress private-toggle assertion indentation, add
aria-labels on IP Filtering and Auto-translation switches, and fix
AdminSettingsSwitch import order for eslint import/order.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Fix eslint failures blocking Web App CI check-lint

Add blank line before comment in channel_modes and reorder imports
in post_priority_picker_item.test to satisfy import/order.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Align en catalogue isPublicDescr with segmented control copy

Update admin.channel_settings.channel_details.isPublicDescr to say
"Use this control" instead of "Use this switch", matching channel_modes.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Fix Switch migration CI failures in unit and Playwright tests

Update recap configuration queries to role=switch, refresh
channel_details snapshots for the Public/Private segmented control,
and assert/set Compass Switch via aria-checked check/uncheck in
Playwright helpers and related specs.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Stabilize Switch snapshots and fix Playwright check interception

Mock Compass Switch CSS-module styles in Jest so snapshots no longer
embed install-specific content hashes, and use force:true on Switch
check/uncheck so the wrapping label does not intercept pointer events.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Update Channel Management Public/Private docs for segmented control

Replace toggle wording with select Public or Private, and refresh the
System Console screenshot to match AdminSettingsSegmentedRow.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Revert Public/Private docs wording; keep screenshot refresh only

The step text can stay as toggle between Public or Private; only the
stale screenshot needed updating for the segmented control.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Refresh Sync Group Members channel docs screenshot for Compass Switch

Replace the stale Channel Management flip-toggle image with an updated
System Console capture showing Sync Group Members enabled.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Refresh Anyone can join this team docs screenshot for Compass Switch

Replace the stale Team Management flip-toggle image with an updated
System Console capture showing Anyone can join enabled.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Refresh specific email domains team docs screenshot for Compass Switch

Replace the stale Team Management flip-toggle image with an updated
System Console capture showing email-domain restriction enabled.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Refresh Public/Private channel docs screenshot for segmented control

Replace the Channel Management Public/Private docs image with an updated
System Console capture showing the Public|Private segmented row.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Refresh Sync Group Members team docs screenshot for Compass Switch

Replace the stale Team Management flip-toggle image with an updated
System Console capture showing Sync Group Members enabled.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Fix Playwright Switch assertions for join/leave and scheduled recaps

Assert join/leave defaults with toBeChecked instead of the legacy active
CSS class, and pause/resume scheduled recaps via click plus polled
expect helpers so async-controlled Compass Switches can settle.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Bump @mattermost/compass-ui to 0.1.0-alpha.15

Upgrade the Compass UI dependency used by Switch and related admin
console controls.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Restore currentUsersIP gate for IP Filtering EditSection

Keep the Toggle→Switch migration scoped to the adapter swap. Showing
EditSection still requires the user's IP to be loaded, matching prior
behavior.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Fix plugin install Cypress asserts for Compass Switch aria-checked

Plugin enable controls use Switch (aria-checked), not segmented buttons
(aria-pressed). Update MM-T5714/MM-T5715 assertions accordingly.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

* Revert IP Filtering filterToggle sync to master behavior

The ipFilters === null early return and consolidated map were outside
the Toggle→Switch swap scope and mildly changed load-time behavior.

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
Matthew BirtchandCursor Agent authored and GitHub committed 2026-10-21 08:18:05 -04:00
1 parent d87f9beedb
commit e3d074ff02
76 files changed
+1973 -2040

No files matched your search

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 118 KiB

@@ -14,12 +14,10 @@ export function enableGroupMention(groupName: string, groupID: string, userEmail
}
cy.get('#group_profile').scrollIntoView().wait(TIMEOUTS.TWO_SEC);
// # Click the allow reference button
cy.findByTestId('allowReferenceSwitch').then((el) => {
const button = el.find('button');
const classAttribute = button[0]?.getAttribute('class') ?? '';
if (!classAttribute.includes('active')) {
button[0].click();
// # Enable group mentions if not already on
cy.findByTestId('allowReferenceSwitch-button').then(($input) => {
if (!$input.prop('checked')) {
cy.wrap($input).click({force: true});
}
});
@@ -131,9 +131,8 @@ context('ldap', () => {
cy.wait(TIMEOUTS.TWO_SEC);
// # Turn on sync group members
cy.findByTestId('syncGroupSwitch').
cy.findByTestId('syncGroupSwitch-button').
scrollIntoView().
findByRole('button').
click({force: true});
// # Add board group to team
@@ -211,17 +210,21 @@ context('ldap', () => {
cy.wait(TIMEOUTS.TWO_SEC);
// Make it private and then cancel
cy.findByTestId('allow-all-toggle').click();
cy.findByTestId('allow-all-toggle-private').click({force: true});
cy.get('#cancelButtonSettings').click();
cy.get('#confirmModalButton').click();
cy.visit(`/admin_console/user_management/channels/${testChannel.id}`);
cy.get('.admin-console__header', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible').and('have.text', 'Channel Configuration');
// * Ensure it still public
cy.findByTestId('allow-all-toggle').should('has.have.text', 'Public');
cy.findByTestId('allow-all-toggle').within(() => {
cy.findByText('Public channel or private channel').should('be.visible');
});
cy.findByTestId('allow-all-toggle-public').should('have.attr', 'aria-pressed', 'true');
cy.findByTestId('allow-all-toggle-private').should('have.attr', 'aria-pressed', 'false');
// Make it private and save
cy.findByTestId('allow-all-toggle').click();
cy.findByTestId('allow-all-toggle-private').click({force: true});
cy.get('#saveSetting').should('be.enabled').click();
cy.get('#confirmModalButton').click();
@@ -230,7 +233,11 @@ context('ldap', () => {
cy.get('.admin-console__header', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible').and('have.text', 'Channel Configuration');
// * Ensure it is Private
cy.findByTestId('allow-all-toggle').should('has.have.text', 'Private');
cy.findByTestId('allow-all-toggle').within(() => {
cy.findByText('Public channel or private channel').should('be.visible');
});
cy.findByTestId('allow-all-toggle-public').should('have.attr', 'aria-pressed', 'false');
cy.findByTestId('allow-all-toggle-private').should('have.attr', 'aria-pressed', 'true');
// # Go to team page to look for this channel in public channel directory
cy.visit(`/${testTeam.name}`);
@@ -256,7 +263,7 @@ context('ldap', () => {
cy.get('.admin-console__header', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible').and('have.text', 'Channel Configuration');
// Make it public and then cancel
cy.findByTestId('allow-all-toggle').click();
cy.findByTestId('allow-all-toggle-public').click({force: true});
cy.get('#cancelButtonSettings').click();
cy.get('#confirmModalButton').click();
cy.get('.admin-console__header', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible').and('have.text', 'Mattermost Channels');
@@ -268,7 +275,11 @@ context('ldap', () => {
// Make it public and save
// * Ensure it still showing the channel as private
cy.findByTestId('allow-all-toggle').should('has.have.text', 'Private').click();
cy.findByTestId('allow-all-toggle').within(() => {
cy.findByText('Public channel or private channel').should('be.visible');
});
cy.findByTestId('allow-all-toggle-private').should('have.attr', 'aria-pressed', 'true');
cy.findByTestId('allow-all-toggle-public').click({force: true});
cy.get('#saveSetting').should('be.enabled').click();
cy.get('#confirmModalButton').click();
cy.get('.admin-console__header', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible').and('have.text', 'Mattermost Channels');
@@ -277,8 +288,12 @@ context('ldap', () => {
cy.visit(`/admin_console/user_management/channels/${privateChannel.id}`);
cy.get('.admin-console__header', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible').and('have.text', 'Channel Configuration');
// * Ensure it still showing the channel as private
cy.findByTestId('allow-all-toggle').should('has.have.text', 'Public');
// * Ensure it is showing the channel as public
cy.findByTestId('allow-all-toggle').within(() => {
cy.findByText('Public channel or private channel').should('be.visible');
});
cy.findByTestId('allow-all-toggle-public').should('have.attr', 'aria-pressed', 'true');
cy.findByTestId('allow-all-toggle-private').should('have.attr', 'aria-pressed', 'false');
// # Ensure the last message in the message says that it was converted to a public channel
cy.visit(`/${testTeam.name}/channels/${privateChannel.name}`);
@@ -303,7 +318,8 @@ context('ldap', () => {
cy.get('.admin-console__header', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible').and('have.text', 'Channel Configuration');
// * Ensure the toggle to private/public is disabled
cy.findByTestId('allow-all-toggle-button').should('be.disabled');
cy.findByTestId('allow-all-toggle-public').should('be.disabled');
cy.findByTestId('allow-all-toggle-private').should('be.disabled');
});
});
@@ -200,7 +200,7 @@ describe('LDAP guest', () => {
cy.get('.admin-console__header', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible').and('have.text', 'Team Configuration');
// # Turn on sync group members
cy.findByTestId('syncGroupSwitch').scrollIntoView().click();
cy.findByTestId('syncGroupSwitch-button').scrollIntoView().click({force: true});
// # Add board group to team
cy.get('#addGroupsToTeamToggle').scrollIntoView().click();
@@ -35,7 +35,7 @@ describe('LDAP Group Sync - Test channel public/private toggle', () => {
assert(channel.type === 'O');
cy.visit(`/admin_console/user_management/channels/${channel.id}`);
cy.get('#channel_profile').contains(channel.display_name);
cy.get('#channel_manage .group-teams-and-channels--body').find('button').eq(1).click();
cy.findByTestId('allow-all-toggle-private').click({force: true});
cy.get('#saveSetting').click();
cy.get('#confirmModalButton').click();
return cy.apiGetChannel(channel.id);
@@ -43,7 +43,7 @@ describe('LDAP Group Sync - Test channel public/private toggle', () => {
assert(channel.type === 'P');
cy.visit(`/admin_console/user_management/channels/${channel.id}`);
cy.get('#channel_profile').contains(channel.display_name);
cy.get('#channel_manage .group-teams-and-channels--body').find('button').eq(1).click();
cy.findByTestId('allow-all-toggle-public').click({force: true});
cy.get('#saveSetting').click();
cy.get('#confirmModalButton').click();
return cy.apiGetChannel(channel.id);
@@ -57,13 +57,20 @@ describe('LDAP Group Sync - Test channel public/private toggle', () => {
assert(channel.type === 'O');
cy.visit(`/admin_console/user_management/channels/${channel.id}`);
cy.get('#channel_profile').contains(channel.display_name);
cy.get('#channel_manage .group-teams-and-channels--body').find('button').eq(0).click();
cy.get('#channel_manage .group-teams-and-channels--body').find('button').eq(0).click();
cy.get('#channel_manage .group-teams-and-channels--body').find('button').eq(1).contains('Public');
cy.get('#channel_manage .group-teams-and-channels--body').find('button').eq(1).click();
cy.get('#channel_manage .group-teams-and-channels--body').find('button').eq(0).click();
cy.get('#channel_manage .group-teams-and-channels--body').find('button').eq(0).click();
cy.get('#channel_manage .group-teams-and-channels--body').find('button').eq(1).contains('Private');
cy.findByTestId('syncGroupSwitch-button').click({force: true});
cy.findByTestId('syncGroupSwitch-button').click({force: true});
cy.findByTestId('allow-all-toggle').within(() => {
cy.findByText('Public channel or private channel').should('be.visible');
});
cy.findByTestId('allow-all-toggle-public').should('have.attr', 'aria-pressed', 'true');
cy.findByTestId('allow-all-toggle-private').click({force: true});
cy.findByTestId('syncGroupSwitch-button').click({force: true});
cy.findByTestId('syncGroupSwitch-button').click({force: true});
cy.findByTestId('allow-all-toggle').within(() => {
cy.findByText('Public channel or private channel').should('be.visible');
});
cy.findByTestId('allow-all-toggle-public').should('have.attr', 'aria-pressed', 'false');
cy.findByTestId('allow-all-toggle-private').should('have.attr', 'aria-pressed', 'true');
});
});
@@ -73,13 +80,14 @@ describe('LDAP Group Sync - Test channel public/private toggle', () => {
cy.visit(`/admin_console/user_management/channels/${id}`);
cy.get('#channel_profile').contains('Town Square');
cy.get('#channel_manage').scrollIntoView().should('be.visible').within(() => {
cy.get('.line-switch').first().within(() => {
cy.findByTestId('syncGroupSwitch').within(() => {
cy.findByText('Sync Group Members').should('be.visible');
cy.findByTestId('syncGroupSwitch-button').should('be.disabled');
});
cy.get('.line-switch').last().within(() => {
cy.findByTestId('allow-all-toggle').within(() => {
cy.findByText('Public channel or private channel').should('be.visible');
cy.findByTestId('allow-all-toggle-button').should('be.disabled');
cy.findByTestId('allow-all-toggle-public').should('be.disabled');
cy.findByTestId('allow-all-toggle-private').should('be.disabled');
});
});
});
@@ -187,33 +187,29 @@ describe('System Console', () => {
// # Login as sysadmin and navigate to board group page
navigateToGroup(groupID);
// # Click the allow reference button
cy.findByTestId('allowReferenceSwitch').then((el) => {
el.find('button').click();
// # Enable group mentions
cy.findByTestId('allowReferenceSwitch-button').click({force: true});
// # Give the group a custom name different from its DisplayName attribute
cy.get('#groupMention').find('input').clear().type(groupName);
// # Give the group a custom name different from its DisplayName attribute
cy.get('#groupMention').find('input').clear().type(groupName);
// # Click save button
saveConfig();
// # Click save button
saveConfig();
// * Assert that the group mention works as expected since the group is enabled and sysadmin always has permission to mention
assertGroupMentionEnabled(groupName);
// * Assert that the group mention works as expected since the group is enabled and sysadmin always has permission to mention
assertGroupMentionEnabled(groupName);
// # Login as sysadmin and navigate to board group page
navigateToGroup(groupID);
// # Login as sysadmin and navigate to board group page
navigateToGroup(groupID);
// # Click the allow reference button
cy.findByTestId('allowReferenceSwitch').then((elSwitch) => {
elSwitch.find('button').click();
// # Disable group mentions
cy.findByTestId('allowReferenceSwitch-button').click({force: true});
// # Click save button
saveConfig();
// # Click save button
saveConfig();
// * Assert that the group mention does not do anything since the group is disabled even though sysadmin has permission to mention
assertGroupMentionDisabled(groupName);
});
});
// * Assert that the group mention does not do anything since the group is disabled even though sysadmin has permission to mention
assertGroupMentionDisabled(groupName);
});
it('MM-23937 - Can restrict users from mentioning a group through the use_group_mentions permission', () => {
@@ -49,9 +49,8 @@ describe('Channel members test', () => {
cy.get('#channelMembers').scrollIntoView().should('be.visible');
// # Click the sync group members switch
cy.findByTestId('syncGroupSwitch').
cy.findByTestId('syncGroupSwitch-button').
scrollIntoView().
findByRole('button').
click({force: true});
// * Assert that the members block is no longer visible
@@ -182,27 +182,39 @@ describe('MM-23102 - Channel Moderation - Higher Scoped Scheme', () => {
// # Visit Channel page and Search for the channel.
// * Ensure message is disabled and manage members for members is disabled
visitChannelConfigPage(testChannel);
cy.findByTestId('allow-all-toggle').should('has.have.text', 'Public');
cy.findByTestId('allow-all-toggle').within(() => {
cy.findByText('Public channel or private channel').should('be.visible');
});
cy.findByTestId('allow-all-toggle-public').should('have.attr', 'aria-pressed', 'true');
cy.findByTestId('allow-all-toggle-private').should('have.attr', 'aria-pressed', 'false');
cy.findByTestId('admin-channel_settings-channel_moderation-manageMembers-disabledMember').
should('have.text', `Manage members for members are disabled in ${teamOverrideSchemeName} Team Scheme.`);
cy.findByTestId(checkboxesTitleToIdMap.MANAGE_MEMBERS_MEMBERS).should('be.disabled');
// # Turn channel into a private channel
cy.findByTestId('allow-all-toggle').click();
cy.findByTestId('allow-all-toggle-private').click({force: true});
saveConfigForChannel(testChannel.display_name, true);
// * Ensure it is private and no error message is shown and that manage members for members is not disabled
cy.findByTestId('allow-all-toggle').should('has.have.text', 'Private');
cy.findByTestId('allow-all-toggle').within(() => {
cy.findByText('Public channel or private channel').should('be.visible');
});
cy.findByTestId('allow-all-toggle-public').should('have.attr', 'aria-pressed', 'false');
cy.findByTestId('allow-all-toggle-private').should('have.attr', 'aria-pressed', 'true');
cy.findByTestId('admin-channel_settings-channel_moderation-manageMembers-disabledMember').
should('not.have.text', `Manage members for members are disabled in ${teamOverrideSchemeName} Team Scheme.`);
cy.findByTestId(checkboxesTitleToIdMap.MANAGE_MEMBERS_MEMBERS).should('not.be.disabled');
// # Turn channel back to public channel
cy.findByTestId('allow-all-toggle').click();
cy.findByTestId('allow-all-toggle-public').click({force: true});
saveConfigForChannel(testChannel.display_name, true);
// * ensure it got reverted back to a Public channel
cy.findByTestId('allow-all-toggle').should('has.have.text', 'Public');
cy.findByTestId('allow-all-toggle').within(() => {
cy.findByText('Public channel or private channel').should('be.visible');
});
cy.findByTestId('allow-all-toggle-public').should('have.attr', 'aria-pressed', 'true');
cy.findByTestId('allow-all-toggle-private').should('have.attr', 'aria-pressed', 'false');
});
it('MM-T1579 Manage Members removed for Private Channels / Permissions inherited when channel converted from Public to Private', () => {
@@ -220,27 +232,39 @@ describe('MM-23102 - Channel Moderation - Higher Scoped Scheme', () => {
visitChannelConfigPage(testChannel);
// * Ensure that error message is not displayed and manage members for members is not disabled
cy.findByTestId('allow-all-toggle').should('has.have.text', 'Public');
cy.findByTestId('allow-all-toggle').within(() => {
cy.findByText('Public channel or private channel').should('be.visible');
});
cy.findByTestId('allow-all-toggle-public').should('have.attr', 'aria-pressed', 'true');
cy.findByTestId('allow-all-toggle-private').should('have.attr', 'aria-pressed', 'false');
cy.findByTestId('admin-channel_settings-channel_moderation-manageMembers-disabledMember').
should('not.have.text', `Manage members for members are disabled in ${teamOverrideSchemeName} Team Scheme.`);
cy.findByTestId(checkboxesTitleToIdMap.MANAGE_MEMBERS_MEMBERS).should('not.be.disabled');
// # Turn it into a private channel
cy.findByTestId('allow-all-toggle').click();
cy.findByTestId('allow-all-toggle-private').click({force: true});
saveConfigForChannel(testChannel.display_name, true);
// * Ensure it is a private channel and that a message is disabled and also manage members for members is disabled
cy.findByTestId('allow-all-toggle').should('has.have.text', 'Private');
cy.findByTestId('allow-all-toggle').within(() => {
cy.findByText('Public channel or private channel').should('be.visible');
});
cy.findByTestId('allow-all-toggle-public').should('have.attr', 'aria-pressed', 'false');
cy.findByTestId('allow-all-toggle-private').should('have.attr', 'aria-pressed', 'true');
cy.findByTestId('admin-channel_settings-channel_moderation-manageMembers-disabledMember').
should('have.text', `Manage members for members are disabled in ${teamOverrideSchemeName} Team Scheme.`);
cy.findByTestId(checkboxesTitleToIdMap.MANAGE_MEMBERS_MEMBERS).should('be.disabled');
// # Turn channel back to public channel
cy.findByTestId('allow-all-toggle').click();
cy.findByTestId('allow-all-toggle-public').click({force: true});
saveConfigForChannel(testChannel.display_name, true);
// * Ensure it got reset back to a public channel
cy.findByTestId('allow-all-toggle').should('has.have.text', 'Public');
cy.findByTestId('allow-all-toggle').within(() => {
cy.findByText('Public channel or private channel').should('be.visible');
});
cy.findByTestId('allow-all-toggle-public').should('have.attr', 'aria-pressed', 'true');
cy.findByTestId('allow-all-toggle-private').should('have.attr', 'aria-pressed', 'false');
});
it('MM-T1581 Check if user is allowed to Edit or Delete their own posts on a Read-Only channel', () => {
@@ -57,9 +57,8 @@ describe('Team members test', () => {
cy.get('#teamMembers').scrollIntoView().should('be.visible');
// # Click the sync group members switch
cy.findByTestId('syncGroupSwitch').
cy.findByTestId('syncGroupSwitch-button').
scrollIntoView().
findByRole('button').
click({force: true});
// * Assert that the members block is no longer visible
@@ -106,7 +106,7 @@ describe('Plugins Management', () => {
// * Verify the enable toggle reflects the running state
cy.findByTestId('plugin-metadata-panel', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible');
cy.findByTestId(pluginEnableToggleTestId(demoPlugin.id)).
should('have.attr', 'aria-pressed', 'true').
should('have.attr', 'aria-checked', 'true').
and('have.attr', 'aria-label', 'Disable plugin');
// # Disable the plugin from the settings page
@@ -115,7 +115,7 @@ describe('Plugins Management', () => {
// * Verify it disables in place, without needing Save
cy.findByTestId('plugin-metadata-status').should('contain.text', 'Not running');
cy.findByTestId(pluginEnableToggleTestId(demoPlugin.id)).
should('have.attr', 'aria-pressed', 'false').
should('have.attr', 'aria-checked', 'false').
and('have.attr', 'aria-label', 'Enable plugin');
// # Re-enable the plugin from the settings page
@@ -151,7 +151,7 @@ describe('Plugins Management', () => {
// * Verify the action is blocked with the expected warning, and the plugin stays enabled
cy.findByText('Please save unsaved changes first').should('be.visible');
cy.findByTestId(pluginEnableToggleTestId(demoPlugin.id)).should('have.attr', 'aria-pressed', 'true');
cy.findByTestId(pluginEnableToggleTestId(demoPlugin.id)).should('have.attr', 'aria-checked', 'true');
// # Attempt to uninstall the plugin while unsaved changes exist
cy.get(`#plugin-actions-menu-button-${demoPlugin.id}`).click();
@@ -61,33 +61,29 @@ export default class ConfigurationSettings {
async enableChannelBanner() {
const toggleButton = this.container.getByTestId('channelBannerToggle-button');
const classes = await toggleButton.getAttribute('class');
if (!classes?.includes('active')) {
await toggleButton.click();
if (!(await toggleButton.isChecked())) {
await toggleButton.check({force: true});
}
}
async disableChannelBanner() {
const toggleButton = this.container.getByTestId('channelBannerToggle-button');
const classes = await toggleButton.getAttribute('class');
if (classes?.includes('active')) {
await toggleButton.click();
if (await toggleButton.isChecked()) {
await toggleButton.uncheck({force: true});
}
}
async enableChannelAutotranslation() {
const toggleButton = this.container.getByTestId('channelTranslationToggle-button');
const classes = await toggleButton.getAttribute('class');
if (!classes?.includes('active')) {
await toggleButton.click();
if (!(await toggleButton.isChecked())) {
await toggleButton.check({force: true});
}
}
async disableChannelAutotranslation() {
const toggleButton = this.container.getByTestId('channelTranslationToggle-button');
const classes = await toggleButton.getAttribute('class');
if (classes?.includes('active')) {
await toggleButton.click();
if (await toggleButton.isChecked()) {
await toggleButton.uncheck({force: true});
}
}
@@ -110,17 +106,15 @@ export default class ConfigurationSettings {
async enableJoinLeaveMessages() {
const toggleButton = this.container.getByTestId('channelJoinLeaveMessagesToggle-button');
const classes = await toggleButton.getAttribute('class');
if (!classes?.includes('active')) {
await toggleButton.click();
if (!(await toggleButton.isChecked())) {
await toggleButton.check({force: true});
}
}
async disableJoinLeaveMessages() {
const toggleButton = this.container.getByTestId('channelJoinLeaveMessagesToggle-button');
const classes = await toggleButton.getAttribute('class');
if (classes?.includes('active')) {
await toggleButton.click();
if (await toggleButton.isChecked()) {
await toggleButton.uncheck({force: true});
}
}
@@ -200,19 +194,15 @@ export default class ConfigurationSettings {
async enableShareWithWorkspaces() {
const toggle = this.shareWithWorkspacesToggle;
const ariaPressed = await toggle.getAttribute('aria-pressed');
const classes = await toggle.getAttribute('class');
if (ariaPressed !== 'true' && !classes?.includes('active')) {
await toggle.click();
if (!(await toggle.isChecked())) {
await toggle.check({force: true});
}
}
async disableShareWithWorkspaces() {
const toggle = this.shareWithWorkspacesToggle;
const ariaPressed = await toggle.getAttribute('aria-pressed');
const classes = await toggle.getAttribute('class');
if (ariaPressed === 'true' || classes?.includes('active')) {
await toggle.click();
if (await toggle.isChecked()) {
await toggle.uncheck({force: true});
}
}
@@ -29,7 +29,7 @@ export default class Localization {
this.header = container.getByText('Localization', {exact: true});
this.featureDiscoveryBlock = container.getByText('Remove language barriers with auto-translation');
this.autoTranslationSection = container.getByTestId('autotranslation-section-header');
this.autoTranslationToggle = container.getByTestId('autotranslation-section-toggle').locator('button');
this.autoTranslationToggle = container.getByTestId('autotranslation-section-toggle').getByRole('switch');
this.providerDropdown = container.getByTestId('Providerdropdown');
this.mattermostAgentsInactiveNotice = container.getByText(
'LLMs must first be configured in the Agents plugin.',
@@ -56,7 +56,7 @@ export default class Localization {
async turnOnAutoTranslation() {
const on = await this.isToggleOn();
if (!on) {
await this.autoTranslationToggle.click();
await this.autoTranslationToggle.check({force: true});
}
}
@@ -55,8 +55,8 @@ class CreateRecapModal {
async enableRunOnce() {
const runOnce = this.container.locator('#run-once-toggle');
await expect(runOnce).toBeVisible();
if ((await runOnce.getAttribute('aria-pressed')) !== 'true') {
await runOnce.click();
if (!(await runOnce.isChecked())) {
await runOnce.check({force: true});
}
}
@@ -236,7 +236,7 @@ class ScheduledRecapItem {
) {
this.menuButton = container.getByRole('button', {name: /Options for /});
this.schedulePattern = container.locator('.schedule-pattern');
this.toggleButton = container.locator('.scheduled-recap-toggle button');
this.toggleButton = container.locator('.scheduled-recap-toggle').getByRole('switch');
}
async toBeVisible() {
@@ -253,24 +253,27 @@ class ScheduledRecapItem {
}
async expectActive() {
await expect(this.toggleButton).toHaveAttribute('aria-pressed', 'true');
await expect(this.toggleButton).toBeChecked();
await expect(this.toggleButton).toHaveAccessibleName('Active - click to pause');
}
async expectPaused() {
await expect(this.toggleButton).toHaveAttribute('aria-pressed', 'false');
await expect(this.toggleButton).not.toBeChecked();
await expect(this.toggleButton).toHaveAccessibleName('Paused - click to resume');
}
async pause() {
await this.expectActive();
await this.toggleButton.click();
// Click instead of uncheck: the Switch is controlled by async Redux/API
// updates, so Playwright's checked-state verification fails before the
// store refreshes. expectPaused() polls until aria-checked flips.
await this.toggleButton.click({force: true});
await this.expectPaused();
}
async resume() {
await this.expectPaused();
await this.toggleButton.click();
await this.toggleButton.click({force: true});
await this.expectActive();
}
@@ -164,10 +164,7 @@ test.describe('Channel attribute banner composition', {tag: ['@channel_attribute
const settings = await channelsPage.openChannelSettings();
const configuration = await settings.openConfigurationTab();
await expect(configuration.container.getByTestId('channelBannerToggle-button')).toHaveAttribute(
'aria-pressed',
'true',
);
await expect(configuration.container.getByTestId('channelBannerToggle-button')).toBeChecked();
await expect(configuration.bannerTextEditor).toBeVisible();
// * Opening the tab changes nothing, so it must not offer to save
@@ -255,10 +255,7 @@ test.describe('Channel attribute banner settings', {tag: ['@channel_attributes']
// # Switch the banner off
let settings = await channelsPage.openChannelSettings();
let configuration = await settings.openConfigurationTab();
await expect(configuration.container.getByTestId('channelBannerToggle-button')).toHaveAttribute(
'aria-pressed',
'true',
);
await expect(configuration.container.getByTestId('channelBannerToggle-button')).toBeChecked();
await configuration.disableChannelBanner();
await configuration.save();
await settings.close();
@@ -270,10 +267,7 @@ test.describe('Channel attribute banner settings', {tag: ['@channel_attributes']
// # Switch it back on
settings = await channelsPage.openChannelSettings();
configuration = await settings.openConfigurationTab();
await expect(configuration.container.getByTestId('channelBannerToggle-button')).toHaveAttribute(
'aria-pressed',
'false',
);
await expect(configuration.container.getByTestId('channelBannerToggle-button')).not.toBeChecked();
await configuration.enableChannelBanner();
await configuration.save();
await settings.close();
@@ -81,7 +81,7 @@ test.describe('Channel Classification - New channel creation', () => {
// Enable classification toggle
const classificationToggle = channelsPage.page.getByTestId('channelClassificationToggle-button');
await classificationToggle.click();
await classificationToggle.check({force: true});
// Create button should be disabled (no classification level selected, no banner text)
await expect(newChannelModal.createButton).toBeDisabled();
@@ -98,7 +98,7 @@ test.describe('Channel Classification - New channel creation', () => {
// Enable classification toggle
const classificationToggle = channelsPage.page.getByTestId('channelClassificationToggle-button');
await classificationToggle.click();
await classificationToggle.check({force: true});
// Open the classification dropdown
const dropdownContainer = channelsPage.page.getByTestId('channelClassificationLevel');
@@ -126,7 +126,7 @@ test.describe('Channel Classification - New channel creation', () => {
// Enable classification toggle
const classificationToggle = channelsPage.page.getByTestId('channelClassificationToggle-button');
await classificationToggle.click();
await classificationToggle.check({force: true});
// Select a classification level
const dropdownContainer = channelsPage.page.getByTestId('channelClassificationLevel');
@@ -164,7 +164,7 @@ test.describe('Channel Classification - New channel creation', () => {
// Enable classification toggle
const classificationToggle = channelsPage.page.getByTestId('channelClassificationToggle-button');
await classificationToggle.click();
await classificationToggle.check({force: true});
// Select the classification level
const dropdownContainer = channelsPage.page.getByTestId('channelClassificationLevel');
@@ -220,13 +220,12 @@ test.describe('Channel Classification - Existing channel settings', () => {
await expect(classificationToggle).toBeVisible();
// Toggle it on
const classes = await classificationToggle.getAttribute('class');
if (!classes?.includes('active')) {
await classificationToggle.click();
if (!(await classificationToggle.isChecked())) {
await classificationToggle.check({force: true});
}
// Toggle should now be active
await expect(classificationToggle).toHaveClass(/active/);
await expect(classificationToggle).toBeChecked();
});
test('Classification level can be set once toggle is enabled', async ({pw}) => {
@@ -246,7 +245,7 @@ test.describe('Channel Classification - Existing channel settings', () => {
// Enable classification toggle
const classificationToggle = channelsPage.page.getByTestId('channelClassificationToggle-button');
await classificationToggle.click();
await classificationToggle.check({force: true});
// Classification level dropdown should be visible
const dropdownContainer = channelsPage.page.getByTestId('channelClassificationLevel');
@@ -282,7 +281,7 @@ test.describe('Channel Classification - Existing channel settings', () => {
// Enable classification and select a level
const classificationToggle = channelsPage.page.getByTestId('channelClassificationToggle-button');
await classificationToggle.click();
await classificationToggle.check({force: true});
const dropdownContainer = channelsPage.page.getByTestId('channelClassificationLevel');
await dropdownContainer.click();
@@ -295,7 +294,7 @@ test.describe('Channel Classification - Existing channel settings', () => {
// The channel banner toggle should now be forced active and disabled
const bannerToggle = channelsPage.page.getByTestId('channelBannerToggle-button');
await expect(bannerToggle).toBeVisible();
await expect(bannerToggle).toHaveClass(/active/);
await expect(bannerToggle).toBeChecked();
await expect(bannerToggle).toBeDisabled();
// Banner color input should be locked to the classification color
@@ -324,7 +323,7 @@ test.describe('Channel Classification - Existing channel settings', () => {
const configurationTab = await channelSettingsModal.openConfigurationTab();
const classificationToggle = channelsPage.page.getByTestId('channelClassificationToggle-button');
await classificationToggle.click();
await classificationToggle.check({force: true});
const dropdownContainer = channelsPage.page.getByTestId('channelClassificationLevel');
await dropdownContainer.click();
@@ -337,7 +336,7 @@ test.describe('Channel Classification - Existing channel settings', () => {
// longer auto-enables banner_info.enabled when classification is on, so the banner
// section body (and its color input) is hidden after toggling classification off
// until a manual banner is enabled.
await classificationToggle.click();
await classificationToggle.uncheck({force: true});
await configurationTab.enableChannelBanner();
const customColor = 'aa00aa';
await configurationTab.setChannelBannerBackgroundColor(customColor);
@@ -350,7 +349,7 @@ test.describe('Channel Classification - Existing channel settings', () => {
expect(persisted.banner_info?.background_color?.toLowerCase().replace('#', '')).toBe(customColor);
// Symptom 2 guard: re-enable classification → Save button reappears (panel transitions out of 'saved')
await classificationToggle.click();
await classificationToggle.check({force: true});
const saveButton = channelsPage.page.getByTestId('SaveChangesPanel__save-btn');
await expect(saveButton).toBeVisible();
await expect(saveButton).toBeEnabled();
@@ -375,7 +374,7 @@ test.describe('Channel Classification - Existing channel settings', () => {
// Enable classification and select a level
const classificationToggle = channelsPage.page.getByTestId('channelClassificationToggle-button');
await classificationToggle.click();
await classificationToggle.check({force: true});
const dropdownContainer = channelsPage.page.getByTestId('channelClassificationLevel');
await dropdownContainer.click();
@@ -6,8 +6,8 @@
* the Configuration tab toggle in Channel Settings.
*
* The toggle controls `DisableJoinLeaveMessages` on the channel:
* - Toggle ON (active class) → messages shown (DisableJoinLeaveMessages = false)
* - Toggle OFF (no active class) → messages hidden (DisableJoinLeaveMessages = true)
* - Toggle ON (aria-checked=true) → messages shown (DisableJoinLeaveMessages = false)
* - Toggle OFF (aria-checked=false) → messages hidden (DisableJoinLeaveMessages = true)
*
* Join system posts are generated server-side when a user is added to a
* channel, so tests use `adminClient.addToChannel()` to produce them.
@@ -45,9 +45,8 @@ test.describe('Channel Settings Modal - Join/Leave System Messages', () => {
// * Toggle is visible
await expect(configSettings.joinLeaveMessagesToggle).toBeVisible();
// * Toggle defaults to ON (active class present = messages shown)
const classes = await configSettings.joinLeaveMessagesToggle.getAttribute('class');
expect(classes).toContain('active');
// * Toggle defaults to ON (messages shown)
await expect(configSettings.joinLeaveMessagesToggle).toBeChecked();
await channelSettings.close();
});
@@ -199,7 +199,7 @@ test.describe('Shared channel configuration', () => {
await expect(configurationTab.shareWithWorkspacesToggle).toBeVisible();
// When sharing is disabled and no workspaces are configured, the toggle is simply off.
await expect(configurationTab.shareWithWorkspacesToggle).toHaveAttribute('aria-pressed', 'false');
await expect(configurationTab.shareWithWorkspacesToggle).not.toBeChecked();
await channelSettingsModal.close();
});
@@ -311,7 +311,7 @@ test.describe('Shared channel configuration', () => {
await channelsPage.toBeVisible();
channelSettingsModal = await channelsPage.openChannelSettings();
configurationTab = await channelSettingsModal.openConfigurationTab();
await expect(configurationTab.shareWithWorkspacesToggle).toHaveAttribute('aria-pressed', 'true');
await expect(configurationTab.shareWithWorkspacesToggle).toBeChecked();
await channelSettingsModal.close();
// Disable sharing via API (uninvite workspaces) to avoid async UI race conditions.
@@ -331,7 +331,7 @@ test.describe('Shared channel configuration', () => {
await channelsPage.toBeVisible();
channelSettingsModal = await channelsPage.openChannelSettings();
configurationTab = await channelSettingsModal.openConfigurationTab();
await expect(configurationTab.shareWithWorkspacesToggle).toHaveAttribute('aria-pressed', 'false');
await expect(configurationTab.shareWithWorkspacesToggle).not.toBeChecked();
await channelSettingsModal.close();
});
@@ -110,9 +110,8 @@ test.describe('ABAC Policies - Channel Integration', () => {
const abacToggle = systemConsolePage.page.locator('[data-testid="policy-enforce-toggle-button"]');
await abacToggle.waitFor({state: 'visible', timeout: 5000});
const isEnabled = await abacToggle.getAttribute('aria-pressed');
if (isEnabled !== 'true') {
await abacToggle.click();
if (!(await abacToggle.isChecked())) {
await abacToggle.check({force: true});
}
await systemConsolePage.page.waitForTimeout(500);
@@ -102,9 +102,10 @@ test.describe('ABAC - Team Membership console', {tag: ['@abac', '@team_membershi
async function setToggle(page: Page, on: boolean): Promise<void> {
const toggle = page.locator('[data-testid="policy-enforce-toggle-button"]');
await toggle.waitFor({state: 'visible', timeout: 10000});
const pressed = (await toggle.getAttribute('aria-pressed')) === 'true';
if (pressed !== on) {
await toggle.click();
if (on) {
await toggle.check({force: true});
} else {
await toggle.uncheck({force: true});
}
}
@@ -285,10 +286,7 @@ test.describe('ABAC - Team Membership console', {tag: ['@abac', '@team_membershi
await expect(disconnectModal).not.toBeVisible({timeout: 5000});
// * Custom rule still governs, so enforcement stays on
await expect(page.locator('[data-testid="policy-enforce-toggle-button"]')).toHaveAttribute(
'aria-pressed',
'true',
);
await expect(page.locator('[data-testid="policy-enforce-toggle-button"]')).toBeChecked();
// * Save goes straight through: removal is not new criteria, so no apply modal
await page.getByRole('button', {name: 'Save'}).click();
+2
View File
@@ -29,6 +29,8 @@ const config = {
'^.+\\.(jpg|jpeg|png|apng|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/src/tests/image_url_mock.json',
'^.+\\.(css|less|scss)$': 'identity-obj-proxy',
// Stabilize Compass Switch CSS-module hashes across environments.
'Switch\\.module\\.scss\\.(js|cjs)$': '<rootDir>/src/tests/mocks/compass_switch_styles.js',
'^.*i18n.*\\.(json)$': '<rootDir>/src/tests/i18n_mock.json',
},
moduleDirectories: ['node_modules'],
@@ -189,7 +189,7 @@ describe('TeamReviewersSection', () => {
expect(teamNameCells[1]).toHaveTextContent('Team Two');
});
const toggle = screen.getByRole('button', {name: /enable or disable content reviewers for team Team One/i});
const toggle = screen.getByRole('switch', {name: /enable or disable content reviewers for team Team One/i});
await userEvent.click(toggle);
expect(onChange).toHaveBeenCalledWith({
@@ -207,8 +207,8 @@ describe('TeamReviewersSection', () => {
expect(mockSearchTeams).toHaveBeenCalledWith('', {page: 0, per_page: 10});
});
expect(await screen.findByRole('button', {name: 'Enable or disable content reviewers for team Team One'})).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Enable or disable content reviewers for team Team Two'})).toBeInTheDocument();
expect(await screen.findByRole('switch', {name: 'Enable or disable content reviewers for team Team One'})).toBeInTheDocument();
expect(screen.getByRole('switch', {name: 'Enable or disable content reviewers for team Team Two'})).toBeInTheDocument();
});
test('should handle toggle functionality with existing settings', async () => {
@@ -241,7 +241,7 @@ describe('TeamReviewersSection', () => {
expect(teamNameCells[1]).toHaveTextContent('Team Two');
});
const toggle = screen.getByRole('button', {name: /enable or disable content reviewers for team Team One/i});
const toggle = screen.getByRole('switch', {name: /enable or disable content reviewers for team Team One/i});
await userEvent.click(toggle);
expect(onChange).toHaveBeenCalledWith({
@@ -395,7 +395,7 @@ describe('TeamReviewersSection', () => {
expect(teamNameCells[1]).toHaveTextContent('Team Two');
});
const toggle = screen.getByRole('button', {name: /enable or disable content reviewers for team Team One/i});
const toggle = screen.getByRole('switch', {name: /enable or disable content reviewers for team Team One/i});
// First click - enable
await userEvent.click(toggle);
@@ -423,7 +423,7 @@ describe('TeamReviewersSection', () => {
expect(mockSearchTeams).toHaveBeenCalledWith('', {page: 0, per_page: 10});
});
const updatedToggle = screen.getAllByRole('button', {name: /enable or disable content reviewers for team Team One/i})[0];
const updatedToggle = screen.getAllByRole('switch', {name: /enable or disable content reviewers for team Team One/i})[0];
// Second click - disable
await userEvent.click(updatedToggle);
@@ -5,13 +5,13 @@ import React, {useCallback, useEffect, useMemo, useState, type JSX} from 'react'
import {useIntl} from 'react-intl';
import {useDispatch} from 'react-redux';
import {Switch} from '@mattermost/compass-ui/components/switch';
import type {TeamReviewerSetting} from '@mattermost/types/config';
import type {Team, TeamSearchOpts} from '@mattermost/types/teams';
import {searchTeams} from 'mattermost-redux/actions/teams';
import DataGrid from 'components/admin_console/data_grid/data_grid';
import Toggle from 'components/toggle';
import {TeamIcon} from 'components/widgets/team_icon/team_icon';
import * as Utils from 'utils/utils';
@@ -151,13 +151,15 @@ export default function TeamReviewers({teamReviewersSetting, onChange, disabled}
/>
),
enabled: (
<Toggle
<Switch
id={`team_content_reviewer_toggle_${team.id}`}
ariaLabel={intl.formatMessage({id: 'admin.dataSpillage.reviewerSettings.toggle', defaultMessage: 'Enable or disable content reviewers for team {teamName}'}, {teamName: team.display_name})}
size='btn-md'
onToggle={getHandleToggle(team.id)}
toggled={teamReviewersSetting[team.id]?.Enabled || false}
size='medium'
onChange={getHandleToggle(team.id)}
checked={teamReviewersSetting[team.id]?.Enabled || false}
disabled={disabled}
data-testid={`team_content_reviewer_toggle_${team.id}-button`}
tabIndex={0}
aria-label={intl.formatMessage({id: 'admin.dataSpillage.reviewerSettings.toggle', defaultMessage: 'Enable or disable content reviewers for team {teamName}'}, {teamName: team.display_name})}
/>
),
},
@@ -28,11 +28,13 @@ describe('components/admin_console/custom_plugin_settings/PluginEnableButton', (
);
expect(screen.getByText('Disabled')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', {name: 'Enable plugin'}));
const enableSwitch = screen.getByRole('switch', {name: 'Enable plugin'});
expect(enableSwitch.closest('label')?.className).toMatch(/switch--size-medium/);
await userEvent.click(enableSwitch);
expect(enablePlugin).toHaveBeenCalledWith('com.mattermost.calls');
expect(disablePlugin).not.toHaveBeenCalled();
expect(screen.getByRole('button', {name: 'Enable plugin'}).closest('.col-sm-offset-4')).toBeNull();
expect(screen.getByRole('switch', {name: 'Enable plugin'}).closest('.col-sm-offset-4')).toBeNull();
});
it('disables the plugin when clicked while enabled', async () => {
@@ -50,11 +52,11 @@ describe('components/admin_console/custom_plugin_settings/PluginEnableButton', (
);
expect(screen.getByText('Enabled')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', {name: 'Disable plugin'}));
await userEvent.click(screen.getByRole('switch', {name: 'Disable plugin'}));
expect(disablePlugin).toHaveBeenCalledWith('com.mattermost.calls');
expect(enablePlugin).not.toHaveBeenCalled();
expect(screen.getByRole('button', {name: 'Disable plugin'})).toHaveAttribute('aria-pressed', 'true');
expect(screen.getByRole('switch', {name: 'Disable plugin'})).toBeChecked();
await userEvent.click(screen.getByRole('button', {name: 'Plugin actions'}));
expect(screen.getByRole('menuitem', {name: 'Uninstall plugin'})).toBeInTheDocument();
@@ -107,11 +109,11 @@ describe('components/admin_console/custom_plugin_settings/PluginEnableButton', (
/>,
);
await userEvent.click(screen.getByRole('button', {name: 'Enable plugin'}));
await userEvent.click(screen.getByRole('switch', {name: 'Enable plugin'}));
expect(screen.getByText('Enabling')).toBeInTheDocument();
expect(screen.getByTestId('loadingSpinner')).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Enable plugin'})).toBeDisabled();
expect(screen.getByRole('switch', {name: 'Enable plugin'})).toBeDisabled();
expect(screen.getByRole('button', {name: 'Plugin actions'})).toBeDisabled();
expect(screen.queryByText('Enabled')).not.toBeInTheDocument();
@@ -120,7 +122,7 @@ describe('components/admin_console/custom_plugin_settings/PluginEnableButton', (
await waitFor(() => {
expect(screen.queryByText('Enabling')).not.toBeInTheDocument();
});
expect(screen.getByRole('button', {name: 'Enable plugin'})).toBeEnabled();
expect(screen.getByRole('switch', {name: 'Enable plugin'})).toBeEnabled();
expect(screen.getByText('Disabled')).toBeInTheDocument();
});
@@ -138,7 +140,7 @@ describe('components/admin_console/custom_plugin_settings/PluginEnableButton', (
/>,
);
await userEvent.click(screen.getByRole('button', {name: 'Enable plugin'}));
await userEvent.click(screen.getByRole('switch', {name: 'Enable plugin'}));
await waitFor(() => {
expect(screen.getByText('Unable to enable plugin')).toBeInTheDocument();
@@ -160,7 +162,7 @@ describe('components/admin_console/custom_plugin_settings/PluginEnableButton', (
/>,
);
await userEvent.click(screen.getByRole('button', {name: 'Enable plugin'}));
await userEvent.click(screen.getByRole('switch', {name: 'Enable plugin'}));
expect(screen.getByText('Please save unsaved changes first')).toBeInTheDocument();
expect(enablePlugin).not.toHaveBeenCalled();
@@ -8,13 +8,13 @@ import {bindActionCreators} from 'redux';
import type {Dispatch} from 'redux';
import {DotsHorizontalIcon, OpenInNewIcon, TrashCanOutlineIcon} from '@mattermost/compass-icons/components';
import {Switch} from '@mattermost/compass-ui/components/switch';
import {disablePlugin, enablePlugin, removePlugin} from 'mattermost-redux/actions/admin';
import type {ActionResult} from 'mattermost-redux/types/actions';
import ConfirmModal from 'components/confirm_modal';
import * as Menu from 'components/menu';
import Toggle from 'components/toggle';
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
import {getHistory} from 'utils/browser_history';
@@ -195,14 +195,15 @@ export function PluginEnableButton({actions, disabled, homepageUrl, id, saveNeed
defaultMessage={pluginEnabled ? 'Enabled' : 'Disabled'}
/>
)}
<Toggle
size='btn-sm'
disabled={actionsDisabled}
toggled={pluginEnabled}
<Switch
id={id}
ariaLabel={toggleAriaLabel}
toggleClassName='btn-toggle-primary'
onToggle={handleTogglePlugin}
size='medium'
disabled={actionsDisabled}
checked={pluginEnabled}
onChange={handleTogglePlugin}
data-testid={`${id}-button`}
tabIndex={0}
aria-label={toggleAriaLabel}
/>
</div>
<Menu.Container
@@ -98,7 +98,7 @@ describe('custom plugin sections and settings', () => {
const expectPluginActionsInMetadataPanel = () => {
const wrapper = document.querySelector('.PluginMetadataPanel__actionsPanel');
const toggleButton = screen.queryByRole('button', {name: 'Enable plugin'}) || screen.getByRole('button', {name: 'Disable plugin'});
const toggleButton = screen.queryByRole('switch', {name: 'Enable plugin'}) || screen.getByRole('switch', {name: 'Disable plugin'});
expect(wrapper).toContainElement(toggleButton);
expect(wrapper).toContainElement(screen.getByRole('button', {name: 'Plugin actions'}));
};
@@ -112,7 +112,7 @@ describe('custom plugin sections and settings', () => {
{...baseState});
expectPluginPageTitle('testplugin', 'testplugin');
expect(screen.getByRole('button', {name: 'Disable plugin'})).toBeInTheDocument();
expect(screen.getByRole('switch', {name: 'Disable plugin'})).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Plugin actions'})).toBeInTheDocument();
expectPluginActionsInMetadataPanel();
const runningBadge = screen.getByTestId('plugin-metadata-status');
@@ -564,7 +564,7 @@ describe('custom plugin sections and settings', () => {
{...state});
expectPluginPageTitle('testplugin', 'testplugin');
expect(screen.getByRole('button', {name: 'Enable plugin'})).toBeInTheDocument();
expect(screen.getByRole('switch', {name: 'Enable plugin'})).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Plugin actions'})).toBeInTheDocument();
expect(screen.queryByTestId('PluginSettings.PluginStates.testplugin.Enabletrue')).not.toBeInTheDocument();
expect(screen.queryByTestId('PluginSettings.PluginStates.testplugin.Enablefalse')).not.toBeInTheDocument();
@@ -653,7 +653,7 @@ describe('custom plugin sections and settings', () => {
{...state});
expectPluginPageTitle('testplugin', 'testplugin');
expect(screen.getByRole('button', {name: 'Enable plugin'})).toBeInTheDocument();
expect(screen.getByRole('switch', {name: 'Enable plugin'})).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Plugin actions'})).toBeInTheDocument();
expect(screen.queryByTestId('PluginSettings.PluginStates.testplugin.Enabletrue')).not.toBeInTheDocument();
expect(screen.queryByTestId('PluginSettings.PluginStates.testplugin.Enablefalse')).not.toBeInTheDocument();
@@ -708,7 +708,7 @@ describe('custom plugin sections and settings', () => {
state,
);
expect(screen.getByRole('button', {name: 'Enable plugin'})).toBeInTheDocument();
expect(screen.getByRole('switch', {name: 'Enable plugin'})).toBeInTheDocument();
updateStoreState({
entities: {
@@ -742,8 +742,8 @@ describe('custom plugin sections and settings', () => {
/>,
);
expect(screen.queryByRole('button', {name: 'Enable plugin'})).not.toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Disable plugin'})).toBeInTheDocument();
expect(screen.queryByRole('switch', {name: 'Enable plugin'})).not.toBeInTheDocument();
expect(screen.getByRole('switch', {name: 'Disable plugin'})).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Plugin actions'})).toBeInTheDocument();
expect(screen.queryByTestId('PluginSettings.PluginStates.testplugin.Enabletrue')).not.toBeInTheDocument();
});
@@ -1001,11 +1001,11 @@ describe('custom plugin sections and settings', () => {
{...state});
expectPluginPageTitle('testplugin', 'testplugin');
expect(screen.getByRole('button', {name: 'Disable plugin'})).toBeInTheDocument();
expect(screen.getByRole('switch', {name: 'Disable plugin'})).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Plugin actions'})).toBeInTheDocument();
expect(screen.queryByTestId('PluginSettings.PluginStates.testplugin.Enabletrue')).not.toBeInTheDocument();
expect(screen.queryByTestId('PluginSettings.PluginStates.testplugin.Enablefalse')).not.toBeInTheDocument();
expect(screen.queryByRole('button', {name: 'Enable plugin'})).not.toBeInTheDocument();
expect(screen.queryByRole('switch', {name: 'Enable plugin'})).not.toBeInTheDocument();
expect(screen.queryByText('In order to view and configure plugin settings, enable the plugin.')).not.toBeInTheDocument();
expect(screen.getByText('Custom Component Section 1')).toBeInTheDocument();
expect(screen.getByText('Custom Component Section 2')).toBeInTheDocument();
@@ -94,46 +94,45 @@ exports[`components/admin_console/group_settings/group_details/GroupDetails shou
class="separator__hr"
/>
</div>
<div>
<div
class="line-switch d-flex flex-sm-column flex-md-row align-items-sm-start align-items-center"
<div
data-testid="allowReferenceSwitch"
>
<label
class="switch switch--size-medium switch--semi-bold switch--with-secondary"
for="allowReferenceSwitch"
>
<label
class="line-switch__label"
<span
class="switch__labels"
>
Enable Group Mentions
</label>
<div
class="line-switch__toggle"
data-testid="allowReferenceSwitch"
>
<button
aria-pressed="false"
class="btn btn-lg btn-toggle"
data-testid="allowReferenceSwitch-button"
id="allowReferenceSwitch"
tabindex="0"
type="button"
<span
class="switch__label"
>
<div
class="handle"
/>
</button>
</div>
</div>
<div
class="row"
>
<div
class="col-sm-10"
>
<div
class="help-text-small help-text-no-padding "
Enable Group Mentions
</span>
<span
class="switch__secondary-label"
>
When enabled, this group can be mentioned in other channels and teams. This may result in the group member list being visible to all users.
</div>
</div>
</div>
</span>
</span>
<span
class="switch__track"
>
<input
aria-checked="false"
class="switch__input"
data-testid="allowReferenceSwitch-button"
id="allowReferenceSwitch"
role="switch"
tabindex="0"
type="checkbox"
/>
<span
aria-hidden="true"
class="switch__knob"
/>
</span>
</label>
</div>
</div>
</div>
@@ -505,46 +504,45 @@ exports[`components/admin_console/group_settings/group_details/GroupDetails shou
class="separator__hr"
/>
</div>
<div>
<div
class="line-switch d-flex flex-sm-column flex-md-row align-items-sm-start align-items-center"
<div
data-testid="allowReferenceSwitch"
>
<label
class="switch switch--size-medium switch--semi-bold switch--with-secondary"
for="allowReferenceSwitch"
>
<label
class="line-switch__label"
<span
class="switch__labels"
>
Enable Group Mentions
</label>
<div
class="line-switch__toggle"
data-testid="allowReferenceSwitch"
>
<button
aria-pressed="false"
class="btn btn-lg btn-toggle"
data-testid="allowReferenceSwitch-button"
id="allowReferenceSwitch"
tabindex="0"
type="button"
<span
class="switch__label"
>
<div
class="handle"
/>
</button>
</div>
</div>
<div
class="row"
>
<div
class="col-sm-10"
>
<div
class="help-text-small help-text-no-padding "
Enable Group Mentions
</span>
<span
class="switch__secondary-label"
>
When enabled, this group can be mentioned in other channels and teams. This may result in the group member list being visible to all users.
</div>
</div>
</div>
</span>
</span>
<span
class="switch__track"
>
<input
aria-checked="false"
class="switch__input"
data-testid="allowReferenceSwitch-button"
id="allowReferenceSwitch"
role="switch"
tabindex="0"
type="checkbox"
/>
<span
aria-hidden="true"
class="switch__knob"
/>
</span>
</label>
</div>
</div>
</div>
@@ -914,46 +912,45 @@ exports[`components/admin_console/group_settings/group_details/GroupDetails shou
class="separator__hr"
/>
</div>
<div>
<div
class="line-switch d-flex flex-sm-column flex-md-row align-items-sm-start align-items-center"
<div
data-testid="allowReferenceSwitch"
>
<label
class="switch switch--size-medium switch--semi-bold switch--with-secondary"
for="allowReferenceSwitch"
>
<label
class="line-switch__label"
<span
class="switch__labels"
>
Enable Group Mentions
</label>
<div
class="line-switch__toggle"
data-testid="allowReferenceSwitch"
>
<button
aria-pressed="false"
class="btn btn-lg btn-toggle"
data-testid="allowReferenceSwitch-button"
id="allowReferenceSwitch"
tabindex="0"
type="button"
<span
class="switch__label"
>
<div
class="handle"
/>
</button>
</div>
</div>
<div
class="row"
>
<div
class="col-sm-10"
>
<div
class="help-text-small help-text-no-padding "
Enable Group Mentions
</span>
<span
class="switch__secondary-label"
>
When enabled, this group can be mentioned in other channels and teams. This may result in the group member list being visible to all users.
</div>
</div>
</div>
</span>
</span>
<span
class="switch__track"
>
<input
aria-checked="false"
class="switch__input"
data-testid="allowReferenceSwitch-button"
id="allowReferenceSwitch"
role="switch"
tabindex="0"
type="checkbox"
/>
<span
aria-hidden="true"
class="switch__knob"
/>
</span>
</label>
</div>
</div>
</div>
@@ -1323,46 +1320,45 @@ exports[`components/admin_console/group_settings/group_details/GroupDetails shou
class="separator__hr"
/>
</div>
<div>
<div
class="line-switch d-flex flex-sm-column flex-md-row align-items-sm-start align-items-center"
<div
data-testid="allowReferenceSwitch"
>
<label
class="switch switch--size-medium switch--semi-bold switch--with-secondary"
for="allowReferenceSwitch"
>
<label
class="line-switch__label"
<span
class="switch__labels"
>
Enable Group Mentions
</label>
<div
class="line-switch__toggle"
data-testid="allowReferenceSwitch"
>
<button
aria-pressed="false"
class="btn btn-lg btn-toggle"
data-testid="allowReferenceSwitch-button"
id="allowReferenceSwitch"
tabindex="0"
type="button"
<span
class="switch__label"
>
<div
class="handle"
/>
</button>
</div>
</div>
<div
class="row"
>
<div
class="col-sm-10"
>
<div
class="help-text-small help-text-no-padding "
Enable Group Mentions
</span>
<span
class="switch__secondary-label"
>
When enabled, this group can be mentioned in other channels and teams. This may result in the group member list being visible to all users.
</div>
</div>
</div>
</span>
</span>
<span
class="switch__track"
>
<input
aria-checked="false"
class="switch__input"
data-testid="allowReferenceSwitch-button"
id="allowReferenceSwitch"
role="switch"
tabindex="0"
type="checkbox"
/>
<span
aria-hidden="true"
class="switch__knob"
/>
</span>
</label>
</div>
</div>
</div>
@@ -62,46 +62,45 @@ exports[`components/admin_console/group_settings/group_details/GroupProfileAndSe
class="separator__hr"
/>
</div>
<div>
<div
class="line-switch d-flex flex-sm-column flex-md-row align-items-sm-start align-items-center"
<div
data-testid="allowReferenceSwitch"
>
<label
class="switch switch--size-medium switch--semi-bold switch--with-secondary"
for="allowReferenceSwitch"
>
<label
class="line-switch__label"
<span
class="switch__labels"
>
Enable Group Mentions
</label>
<div
class="line-switch__toggle"
data-testid="allowReferenceSwitch"
>
<button
aria-pressed="false"
class="btn btn-lg btn-toggle"
data-testid="allowReferenceSwitch-button"
id="allowReferenceSwitch"
tabindex="0"
type="button"
<span
class="switch__label"
>
<div
class="handle"
/>
</button>
</div>
</div>
<div
class="row"
>
<div
class="col-sm-10"
>
<div
class="help-text-small help-text-no-padding "
Enable Group Mentions
</span>
<span
class="switch__secondary-label"
>
When enabled, this group can be mentioned in other channels and teams. This may result in the group member list being visible to all users.
</div>
</div>
</div>
</span>
</span>
<span
class="switch__track"
>
<input
aria-checked="false"
class="switch__input"
data-testid="allowReferenceSwitch-button"
id="allowReferenceSwitch"
role="switch"
tabindex="0"
type="checkbox"
/>
<span
aria-hidden="true"
class="switch__knob"
/>
</span>
</label>
</div>
</div>
</div>
@@ -171,46 +170,46 @@ exports[`components/admin_console/group_settings/group_details/GroupProfileAndSe
class="separator__hr"
/>
</div>
<div>
<div
class="line-switch d-flex flex-sm-column flex-md-row align-items-sm-start align-items-center"
<div
data-testid="allowReferenceSwitch"
>
<label
class="switch switch--size-medium switch--semi-bold switch--with-secondary"
for="allowReferenceSwitch"
>
<label
class="line-switch__label"
<span
class="switch__labels"
>
Enable Group Mentions
</label>
<div
class="line-switch__toggle"
data-testid="allowReferenceSwitch"
>
<button
aria-pressed="true"
class="btn btn-lg btn-toggle active"
data-testid="allowReferenceSwitch-button"
id="allowReferenceSwitch"
tabindex="0"
type="button"
<span
class="switch__label"
>
<div
class="handle"
/>
</button>
</div>
</div>
<div
class="row"
>
<div
class="col-sm-10"
>
<div
class="help-text-small help-text-no-padding "
Enable Group Mentions
</span>
<span
class="switch__secondary-label"
>
When enabled, this group can be mentioned in other channels and teams. This may result in the group member list being visible to all users.
</div>
</div>
</div>
</span>
</span>
<span
class="switch__track"
>
<input
aria-checked="true"
checked=""
class="switch__input"
data-testid="allowReferenceSwitch-button"
id="allowReferenceSwitch"
role="switch"
tabindex="0"
type="checkbox"
/>
<span
aria-hidden="true"
class="switch__knob"
/>
</span>
</label>
</div>
</div>
</div>
@@ -5,7 +5,7 @@ import React from 'react';
import {FormattedMessage, defineMessage} from 'react-intl';
import GroupProfile from 'components/admin_console/group_settings/group_details/group_profile';
import LineSwitch from 'components/admin_console/team_channel_settings/line_switch';
import AdminSettingsSwitch from 'components/admin_console/team_channel_settings/admin_settings_switch';
import AdminPanel from 'components/widgets/admin_console/admin_panel';
type GroupSettingsToggleProps = {
@@ -21,31 +21,29 @@ const GroupSettingsToggle = ({
onToggle,
isDisabled,
}: GroupSettingsToggleProps) => (
<LineSwitch
<AdminSettingsSwitch
id={'allowReferenceSwitch'}
disabled={isDisabled || isDefault}
toggled={allowReference}
checked={allowReference}
last={true}
onToggle={() => {
onChange={() => {
if (isDefault) {
return;
}
onToggle(!allowReference);
}}
singleLine={false}
title={
<FormattedMessage
id='admin.team_settings.team_details.groupDetailsToggle'
defaultMessage='Enable Group Mentions'
/>
}
subTitle={
secondaryLabel={
<FormattedMessage
id='admin.team_settings.team_details.groupDetailsToggleDescr'
defaultMessage='When enabled, this group can be mentioned in other channels and teams. This may result in the group member list being visible to all users.'
/>
}
/>
>
<FormattedMessage
id='admin.team_settings.team_details.groupDetailsToggle'
defaultMessage='Enable Group Mentions'
/>
</AdminSettingsSwitch>
);
type GroupProfileAndSettingsProps = {
@@ -31,7 +31,7 @@ describe('EnableSectionContent', () => {
expect(screen.getByText('Enable IP Filtering')).toBeInTheDocument();
expect(screen.getByText('Limit access to your workspace by IP address.')).toBeInTheDocument();
expect(screen.getByTestId('filterToggle-button')).toBeInTheDocument();
expect(screen.getByRole('button', {pressed: true})).toBeInTheDocument();
expect(screen.getByRole('switch', {checked: true})).toBeInTheDocument();
});
test('clicking the toggle calls setFilterToggle', async () => {
@@ -47,6 +47,19 @@ describe('EnableSectionContent', () => {
expect(setFilterToggle).toHaveBeenCalledWith(false);
});
test('clicking the toggle when off calls setFilterToggle with true', async () => {
renderWithContext(
<EnableSectionContent
{...baseProps}
filterToggle={false}
/>,
);
await userEvent.click(screen.getByTestId('filterToggle-button'));
expect(setFilterToggle).toHaveBeenCalledWith(true);
});
test('renders the component, with toggle not pressed if filterToggle is false', () => {
renderWithContext(
<EnableSectionContent
@@ -58,7 +71,7 @@ describe('EnableSectionContent', () => {
expect(screen.getByText('Enable IP Filtering')).toBeInTheDocument();
expect(screen.getByText('Limit access to your workspace by IP address.')).toBeInTheDocument();
expect(screen.getByTestId('filterToggle-button')).toBeInTheDocument();
expect(screen.getByRole('button', {pressed: false})).toBeInTheDocument();
expect(screen.getByRole('switch', {checked: false})).toBeInTheDocument();
});
});
@@ -4,8 +4,9 @@
import React from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {Switch} from '@mattermost/compass-ui/components/switch';
import ExternalLink from 'components/external_link';
import Toggle from 'components/toggle';
type Props = {
filterToggle: boolean;
@@ -40,13 +41,15 @@ const EnableSectionContent: React.FC<Props> = ({filterToggle, setFilterToggle})
</div>
</div>
<div className='SwitchSelector'>
<Toggle
size={'btn-md'}
id={'filterToggle'}
<Switch
id='filterToggle'
size='medium'
disabled={false}
onToggle={() => setFilterToggle(!filterToggle)}
toggled={filterToggle}
toggleClassName='btn-toggle-primary'
onChange={(e) => setFilterToggle(e.target.checked)}
checked={filterToggle}
data-testid='filterToggle-button'
tabIndex={0}
aria-label={formatMessage({id: 'admin.ip_filtering.enable_ip_filtering', defaultMessage: 'Enable IP Filtering'})}
/>
</div>
</div>
@@ -64,9 +64,18 @@
}
.SwitchSelector {
display: flex;
width: 32px;
height: 20px;
flex-shrink: 0;
align-items: center;
// Compass Switch root is a <label>; Bootstrap form labels
// add margin-bottom which misaligns / can clip hit targets.
> label {
width: auto;
margin-bottom: 0;
}
}
}
}
@@ -97,13 +97,13 @@ describe('IPFiltering', () => {
await waitFor(() => {
expect(screen.getByTestId('filterToggle-button')).toBeInTheDocument();
expect(screen.getByRole('button', {pressed: true})).toBeInTheDocument();
expect(screen.getByRole('switch', {checked: true})).toBeInTheDocument();
});
await userEvent.click(screen.getByTestId('filterToggle-button'));
await waitFor(() => {
expect(screen.getByRole('button', {pressed: false})).toBeInTheDocument();
expect(screen.getByRole('switch', {checked: false})).toBeInTheDocument();
});
});
@@ -189,13 +189,13 @@ describe('IPFiltering', () => {
await waitFor(() => {
expect(screen.getByTestId('filterToggle-button')).toBeInTheDocument();
expect(screen.getByRole('button', {pressed: true})).toBeInTheDocument();
expect(screen.getByRole('switch', {checked: true})).toBeInTheDocument();
});
await userEvent.click(screen.getByTestId('filterToggle-button'));
await waitFor(() => {
expect(screen.getByRole('button', {pressed: false})).toBeInTheDocument();
expect(screen.getByRole('switch', {checked: false})).toBeInTheDocument();
});
await waitFor(() => {
@@ -251,14 +251,14 @@ describe('IPFiltering', () => {
await waitFor(() => {
expect(screen.getByTestId('filterToggle-button')).toBeInTheDocument();
expect(screen.getByRole('button', {pressed: true})).toBeInTheDocument();
expect(screen.getByRole('switch', {checked: true})).toBeInTheDocument();
});
// Use fireEvent.click here because userEvent doesn't work well with fake timers
fireEvent.click(screen.getByTestId('filterToggle-button'));
await waitFor(() => {
expect(screen.getByRole('button', {pressed: false})).toBeInTheDocument();
expect(screen.getByRole('switch', {checked: false})).toBeInTheDocument();
});
await waitFor(() => {
@@ -2,9 +2,10 @@
// See LICENSE.txt for license information.
import React, {useCallback, useMemo, useState} from 'react';
import {defineMessage, defineMessages, FormattedMessage} from 'react-intl';
import {defineMessage, defineMessages, FormattedMessage, useIntl} from 'react-intl';
import {Link} from 'react-router-dom';
import {Switch} from '@mattermost/compass-ui/components/switch';
import type {AutoTranslationSettings} from '@mattermost/types/config';
import BooleanSetting from 'components/admin_console/boolean_setting';
@@ -17,7 +18,6 @@ import {
} from 'components/admin_console/system_properties/controls';
import TextSetting from 'components/admin_console/text_setting';
import useGetAgentsBridgeEnabled from 'components/common/hooks/useGetAgentsBridgeEnabled';
import Toggle from 'components/toggle';
import BetaTag from 'components/widgets/tag/beta_tag';
import * as I18n from 'i18n/i18n';
@@ -47,6 +47,7 @@ const messages = defineMessages({
export const searchableStrings: SearchableStrings = Object.values(messages);
export default function AutoTranslation(props: SystemConsoleCustomSettingsComponentProps) {
const {formatMessage} = useIntl();
const {available: isAgentsBridgeEnabled, reason: agentsBridgeUnavailableReason} = useGetAgentsBridgeEnabled();
const [autoTranslationSettings, setAutoTranslationSettings] = useState<AutoTranslationSettings>(() => {
const settings = props.value as AutoTranslationSettings;
@@ -173,14 +174,15 @@ export default function AutoTranslation(props: SystemConsoleCustomSettingsCompon
<span style={{marginRight: '12px'}}>
{autoTranslationSettings.Enable ? on : off}
</span>
<Toggle
size='btn-md'
<Switch
id='Enable'
size='medium'
disabled={props.disabled || props.setByEnv}
toggled={autoTranslationSettings.Enable}
id={'Enable'}
checked={autoTranslationSettings.Enable}
onChange={handleToggle}
data-testid='Enable-button'
tabIndex={-1}
toggleClassName='btn-toggle-primary'
onToggle={handleToggle}
aria-label={formatMessage(messages.enableAutoTranslationTitle)}
/>
</div>
</div>
@@ -123,11 +123,6 @@
border: none;
margin: 0;
gap: 8px;
.btn-toggle,
.btn-toggle-primary {
float: none;
}
}
&__enable {
@@ -138,6 +133,14 @@
gap: 12px;
line-height: 20px;
// Compass Switch root is a <label>. Bootstrap form labels add
// margin-bottom, and Switch defaults to width: 100%, which throws off
// vertical alignment next to the actions menu button.
> label {
width: auto;
margin-bottom: 0;
}
.LoadingSpinner .spinner {
color: rgba(var(--center-channel-color-rgb), 0.64);
}
@@ -284,7 +284,7 @@ describe('UserPropertyDotMenu', () => {
const editableItem = screen.getByRole('menuitemcheckbox', {name: /Editable by users/});
expect(editableItem).toHaveAttribute('aria-checked', 'false');
expect(within(editableItem).getByRole('button')).toBeEnabled();
expect(within(editableItem).getByRole('switch')).toBeEnabled();
expect(screen.queryByText('Synced attributes are managed by AD/LDAP or SAML')).not.toBeInTheDocument();
});
@@ -305,7 +305,7 @@ describe('UserPropertyDotMenu', () => {
const editableItem = screen.getByRole('menuitemcheckbox', {name: /Editable by users/});
expect(editableItem).toHaveAttribute('aria-checked', 'false');
expect(within(editableItem).getByRole('button')).toBeDisabled();
expect(within(editableItem).getByRole('switch')).toBeDisabled();
expect(screen.getByText('Synced attributes are managed by AD/LDAP or SAML')).toBeInTheDocument();
});
@@ -347,7 +347,7 @@ describe('UserPropertyDotMenu', () => {
const editableItem = screen.getByRole('menuitemcheckbox', {name: /Editable by users/});
expect(editableItem).toHaveAttribute('aria-checked', 'false');
expect(within(editableItem).getByRole('button')).toBeDisabled();
expect(within(editableItem).getByRole('switch')).toBeDisabled();
expect(screen.getByText('Synced attributes are managed by AD/LDAP or SAML')).toBeInTheDocument();
});
@@ -369,7 +369,7 @@ describe('UserPropertyDotMenu', () => {
const editableItem = screen.getByRole('menuitemcheckbox', {name: /Editable by users/});
expect(editableItem).toHaveAttribute('aria-checked', 'false');
expect(within(editableItem).getByRole('button')).toBeDisabled();
expect(within(editableItem).getByRole('switch')).toBeDisabled();
expect(screen.getByText('Synced attributes are managed by AD/LDAP or SAML')).toBeInTheDocument();
});
@@ -390,7 +390,7 @@ describe('UserPropertyDotMenu', () => {
const editableItem = screen.getByRole('menuitemcheckbox', {name: /Editable by users/});
expect(editableItem).toHaveAttribute('aria-checked', 'false');
expect(within(editableItem).getByRole('button')).toBeDisabled();
expect(within(editableItem).getByRole('switch')).toBeDisabled();
expect(screen.getByText('This attribute is managed by an integration')).toBeInTheDocument();
expect(screen.getByText('Link attribute to AD/LDAP')).toBeInTheDocument();
@@ -438,7 +438,7 @@ describe('UserPropertyDotMenu', () => {
// everything else stays editable
expect(screen.getByRole('menuitem', {name: /Visibility/})).not.toHaveAttribute('aria-disabled', 'true');
expect(within(screen.getByRole('menuitemcheckbox', {name: /Editable by users/})).getByRole('button')).toBeEnabled();
expect(within(screen.getByRole('menuitemcheckbox', {name: /Editable by users/})).getByRole('switch')).toBeEnabled();
expect(screen.getByRole('menuitem', {name: /Duplicate attribute/})).not.toHaveAttribute('aria-disabled', 'true');
expect(screen.getByRole('menuitem', {name: /Delete attribute/})).not.toHaveAttribute('aria-disabled', 'true');
});
@@ -7,13 +7,13 @@ import {FormattedMessage, useIntl} from 'react-intl';
import {useDispatch} from 'react-redux';
import {CheckIcon, ChevronRightIcon, DotsHorizontalIcon, EyeOutlineIcon, FormatListNumberedIcon, LockOutlineIcon, PencilOutlineIcon, SyncIcon, TrashCanOutlineIcon, ContentCopyIcon} from '@mattermost/compass-icons/components';
import {Switch} from '@mattermost/compass-ui/components/switch';
import type {FieldVisibility} from '@mattermost/types/properties';
import type {UserPropertyField} from '@mattermost/types/properties_user';
import {openModal} from 'actions/views/modals';
import * as Menu from 'components/menu';
import Toggle from 'components/toggle';
import {ModalIdentifiers} from 'utils/constants';
import {slugifyForCEL} from 'utils/properties';
@@ -401,14 +401,21 @@ const DotMenu = ({
/>
)}
trailingElements={(
<Toggle
size='btn-sm'
disabled={isManagedExternally}
onToggle={handleEditableByUsersToggle}
toggled={isEditableByUsers}
toggleClassName='btn-toggle-primary'
tabIndex={-1}
/>
<span
onClick={isManagedExternally ? undefined : (event: React.MouseEvent) => {
// Compass Switch fires onChange on its own click; without
// stopping propagation Menu.Item onClick also runs.
event.stopPropagation();
}}
>
<Switch
size='small'
disabled={isManagedExternally}
onChange={handleEditableByUsersToggle}
checked={isEditableByUsers}
tabIndex={-1}
/>
</span>
)}
/>
{field.create_at !== 0 && ([
@@ -0,0 +1,65 @@
.AdminSettingsSegmentedRow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
&__text {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
gap: 2px;
}
&__title {
color: var(--center-channel-color);
font-size: 14px;
font-weight: 600;
line-height: 20px;
}
&__description {
color: rgba(var(--center-channel-color-rgb), 0.72);
font-size: 12px;
line-height: 16px;
}
&__segments {
display: inline-flex;
flex-shrink: 0;
padding: 4px;
border: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
border-radius: 8px;
background: rgba(var(--center-channel-color-rgb), 0.08);
}
&__segment {
padding: 4px 12px;
border: 0;
border-radius: 4px;
background: transparent;
color: rgba(var(--center-channel-color-rgb), 0.72);
cursor: pointer;
font: inherit;
font-size: 13px;
font-weight: 600;
line-height: 20px;
&:hover:not(:disabled):not(.AdminSettingsSegmentedRow__segment--active) {
background: rgba(var(--center-channel-color-rgb), 0.06);
color: var(--center-channel-color);
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
&--active {
background: var(--center-channel-bg);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
color: var(--center-channel-color);
}
}
}
@@ -0,0 +1,92 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import classNames from 'classnames';
import React, {type JSX, type ReactNode} from 'react';
import './admin_settings_segmented_row.scss';
export type AdminSettingsSegment = {
id: string;
label: ReactNode;
selected: boolean;
onSelect: () => void;
testId?: string;
};
type Props = {
id: string;
children: ReactNode;
secondaryLabel: ReactNode;
segments: AdminSettingsSegment[];
last?: boolean;
disabled?: boolean;
footer?: ReactNode;
groupAriaLabel?: string;
};
/**
* Admin Console settings row with a temporary segmented control.
* Use for mutually exclusive named modes (e.g. Public / Private) until
* Compass ships a segmented control primitive.
*/
const AdminSettingsSegmentedRow = ({
id,
children,
secondaryLabel,
segments,
last,
disabled,
footer,
groupAriaLabel,
}: Props): JSX.Element => (
<>
<div
className='AdminSettingsSegmentedRow'
data-testid={id}
>
<div className='AdminSettingsSegmentedRow__text'>
<div className='AdminSettingsSegmentedRow__title'>
{children}
</div>
<div className='AdminSettingsSegmentedRow__description'>
{secondaryLabel}
</div>
</div>
<div
className='AdminSettingsSegmentedRow__segments'
role='group'
aria-label={groupAriaLabel}
>
{segments.map((segment) => (
<button
key={segment.id}
type='button'
id={`${id}-${segment.id}`}
data-testid={segment.testId ?? `${id}-${segment.id}`}
className={classNames('AdminSettingsSegmentedRow__segment', {
'AdminSettingsSegmentedRow__segment--active': segment.selected,
})}
aria-pressed={segment.selected}
disabled={disabled}
onClick={() => {
if (!segment.selected) {
segment.onSelect();
}
}}
>
{segment.label}
</button>
))}
</div>
</div>
{footer}
{!last && (
<div className='section-separator'>
<hr className='separator__hr'/>
</div>
)}
</>
);
export default AdminSettingsSegmentedRow;
@@ -0,0 +1,66 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {type JSX, type ReactNode} from 'react';
import {Switch} from '@mattermost/compass-ui/components/switch';
type Props = {
id: string;
children: ReactNode;
secondaryLabel: ReactNode;
checked: boolean;
onChange: () => void;
last?: boolean;
disabled?: boolean;
/** Extra content rendered below the switch row (e.g. domain input). */
footer?: ReactNode;
};
/**
* Admin Console settings row: Compass Switch with primary + secondary labels,
* optional footer, and the legacy section separator between rows.
*
* Uses `size="medium"` + `semiBold` to match Channel Settings rows. The old
* Toggle adapter defaulted to `btn-lg` → Compass `large` (16px label).
*
* Preserves both `data-testid={id}` (wrapper, for Cypress) and
* `data-testid={`${id}-button`}` (on the switch input, for unit tests).
*/
const AdminSettingsSwitch = ({
id,
children,
secondaryLabel,
checked,
onChange,
last,
disabled,
footer,
}: Props): JSX.Element => (
<>
<div data-testid={id}>
<Switch
id={id}
size='medium'
checked={checked}
disabled={disabled}
onChange={onChange}
data-testid={`${id}-button`}
tabIndex={0}
semiBold={true}
secondaryLabel={secondaryLabel}
>
{children}
</Switch>
</div>
{footer}
{!last && (
<div className='section-separator'>
<hr className='separator__hr'/>
</div>
)}
</>
);
export default AdminSettingsSwitch;
@@ -26,63 +26,60 @@ exports[`admin_console/team_channel_settings/channel/ChannelModes should match s
<div
class="group-teams-and-channels--body"
>
<div>
<div
class="AdminSettingsSegmentedRow"
data-testid="allow-all-toggle"
>
<div
class="line-switch d-flex flex-sm-column flex-md-row align-items-sm-start align-items-center"
class="AdminSettingsSegmentedRow__text"
>
<label
class="line-switch__label"
<div
class="AdminSettingsSegmentedRow__title"
>
Public channel or private channel
</label>
</div>
<div
class="line-switch__toggle"
data-testid="allow-all-toggle"
class="AdminSettingsSegmentedRow__description"
>
<button
aria-pressed="true"
class="btn btn-lg btn-toggle active"
data-testid="allow-all-toggle-button"
id="allow-all-toggle"
tabindex="0"
type="button"
>
<div
class="handle"
/>
<div
class="bg-text on"
>
Public
</div>
</button>
Select Public for a channel any user can find and join.
<br />
Select Private to require channel invitations to join.
<br />
Use this control to change this channel from public to private or from private to public.
</div>
</div>
<div
class="row"
aria-label="Public channel or private channel"
class="AdminSettingsSegmentedRow__segments"
role="group"
>
<div
class="col-sm-10"
<button
aria-pressed="true"
class="AdminSettingsSegmentedRow__segment AdminSettingsSegmentedRow__segment--active"
data-testid="allow-all-toggle-public"
id="allow-all-toggle-public"
type="button"
>
<div
class="help-text-small help-text-no-padding "
>
Select Public for a channel any user can find and join.
<br />
Select Private to require channel invitations to join.
<br />
Use this switch to change this channel from public to private or from private to public.
</div>
</div>
</div>
<div
class="section-separator"
>
<hr
class="separator__hr"
/>
Public
</button>
<button
aria-pressed="false"
class="AdminSettingsSegmentedRow__segment"
data-testid="allow-all-toggle-private"
id="allow-all-toggle-private"
type="button"
>
Private
</button>
</div>
</div>
<div
class="section-separator"
>
<hr
class="separator__hr"
/>
</div>
</div>
</div>
</div>
@@ -115,41 +112,23 @@ exports[`admin_console/team_channel_settings/channel/ChannelModes should match s
<div
class="group-teams-and-channels--body"
>
<div>
<div
class="line-switch d-flex flex-sm-column flex-md-row align-items-sm-start align-items-center"
<div
data-testid="syncGroupSwitch"
>
<label
class="switch switch--size-medium switch--semi-bold switch--with-secondary"
for="syncGroupSwitch"
>
<label
class="line-switch__label"
<span
class="switch__labels"
>
Sync Group Members
</label>
<div
class="line-switch__toggle"
data-testid="syncGroupSwitch"
>
<button
aria-pressed="false"
class="btn btn-lg btn-toggle"
data-testid="syncGroupSwitch-button"
id="syncGroupSwitch"
tabindex="0"
type="button"
<span
class="switch__label"
>
<div
class="handle"
/>
</button>
</div>
</div>
<div
class="row"
>
<div
class="col-sm-10"
>
<div
class="help-text-small help-text-no-padding "
Sync Group Members
</span>
<span
class="switch__secondary-label"
>
When enabled, adding and removing users from groups will add or remove them from this channel. The only way of inviting members to this channel is by adding the groups they belong to.
<a
@@ -160,74 +139,88 @@ exports[`admin_console/team_channel_settings/channel/ChannelModes should match s
>
Learn More
</a>
</div>
</div>
</div>
<div
class="section-separator"
>
<hr
class="separator__hr"
/>
</div>
</span>
</span>
<span
class="switch__track"
>
<input
aria-checked="false"
class="switch__input"
data-testid="syncGroupSwitch-button"
id="syncGroupSwitch"
role="switch"
tabindex="0"
type="checkbox"
/>
<span
aria-hidden="true"
class="switch__knob"
/>
</span>
</label>
</div>
<div>
<div
class="section-separator"
>
<hr
class="separator__hr"
/>
</div>
<div
class="AdminSettingsSegmentedRow"
data-testid="allow-all-toggle"
>
<div
class="line-switch d-flex flex-sm-column flex-md-row align-items-sm-start align-items-center"
class="AdminSettingsSegmentedRow__text"
>
<label
class="line-switch__label"
<div
class="AdminSettingsSegmentedRow__title"
>
Public channel or private channel
</label>
</div>
<div
class="line-switch__toggle"
data-testid="allow-all-toggle"
class="AdminSettingsSegmentedRow__description"
>
<button
aria-pressed="true"
class="btn btn-lg btn-toggle active"
data-testid="allow-all-toggle-button"
id="allow-all-toggle"
tabindex="0"
type="button"
>
<div
class="handle"
/>
<div
class="bg-text on"
>
Public
</div>
</button>
Select Public for a channel any user can find and join.
<br />
Select Private to require channel invitations to join.
<br />
Use this control to change this channel from public to private or from private to public.
</div>
</div>
<div
class="row"
aria-label="Public channel or private channel"
class="AdminSettingsSegmentedRow__segments"
role="group"
>
<div
class="col-sm-10"
<button
aria-pressed="true"
class="AdminSettingsSegmentedRow__segment AdminSettingsSegmentedRow__segment--active"
data-testid="allow-all-toggle-public"
id="allow-all-toggle-public"
type="button"
>
<div
class="help-text-small help-text-no-padding "
>
Select Public for a channel any user can find and join.
<br />
Select Private to require channel invitations to join.
<br />
Use this switch to change this channel from public to private or from private to public.
</div>
</div>
</div>
<div
class="section-separator"
>
<hr
class="separator__hr"
/>
Public
</button>
<button
aria-pressed="false"
class="AdminSettingsSegmentedRow__segment"
data-testid="allow-all-toggle-private"
id="allow-all-toggle-private"
type="button"
>
Private
</button>
</div>
</div>
<div
class="section-separator"
>
<hr
class="separator__hr"
/>
</div>
</div>
</div>
</div>
@@ -2,12 +2,13 @@
// See LICENSE.txt for license information.
import React, {type JSX} from 'react';
import {FormattedMessage, defineMessage} from 'react-intl';
import {FormattedMessage, defineMessage, useIntl} from 'react-intl';
import ExternalLink from 'components/external_link';
import AdminPanel from 'components/widgets/admin_console/admin_panel';
import LineSwitch from '../../line_switch';
import AdminSettingsSegmentedRow from '../../admin_settings_segmented_row';
import AdminSettingsSwitch from '../../admin_settings_switch';
interface Props {
isPublic: boolean;
@@ -24,24 +25,18 @@ interface Props {
const SyncGroupsToggle = (props: Props): JSX.Element => {
const {isPublic, isSynced, isDefault, onToggle, isDisabled, policyEnforced} = props;
return (
<LineSwitch
<AdminSettingsSwitch
id='syncGroupSwitch'
disabled={isDisabled || isDefault || policyEnforced}
toggled={isSynced}
checked={isSynced}
last={isSynced}
onToggle={() => {
onChange={() => {
if (isDefault) {
return;
}
onToggle(!isSynced, isPublic, policyEnforced);
}}
title={(
<FormattedMessage
id='admin.channel_settings.channel_details.syncGroupMembers'
defaultMessage='Sync Group Members'
/>
)}
subTitle={(
secondaryLabel={(
<FormattedMessage
id='admin.channel_settings.channel_details.syncGroupMembersDescr'
defaultMessage='When enabled, adding and removing users from groups will add or remove them from this channel. The only way of inviting members to this channel is by adding the groups they belong to. <link>Learn More</link>'
@@ -57,34 +52,41 @@ const SyncGroupsToggle = (props: Props): JSX.Element => {
}}
/>
)}
/>
>
<FormattedMessage
id='admin.channel_settings.channel_details.syncGroupMembers'
defaultMessage='Sync Group Members'
/>
</AdminSettingsSwitch>
);
};
const AllowAllToggle = (props: Props): JSX.Element | null => {
const {isPublic, isSynced, isDefault, onToggle, isDisabled, policyEnforced} = props;
const {formatMessage} = useIntl();
if (isSynced) {
return null;
}
// Mutual exclusive modes — temporary segmented control until Compass has one.
const disabled = isDisabled || isDefault || policyEnforced;
const setPublic = (nextPublic: boolean) => {
if (isDefault || nextPublic === isPublic) {
return;
}
onToggle(isSynced, nextPublic, policyEnforced);
};
return (
<LineSwitch
<AdminSettingsSegmentedRow
id='allow-all-toggle'
disabled={isDisabled || isDefault || policyEnforced}
toggled={isPublic}
disabled={disabled}
last={false}
onToggle={() => {
if (isDefault) {
return;
}
onToggle(isSynced, !isPublic, policyEnforced);
}}
title={(
<FormattedMessage
id='admin.channel_settings.channel_details.isPublic'
defaultMessage='Public channel or private channel'
/>
)}
subTitle={isDefault ? (
groupAriaLabel={formatMessage({
id: 'admin.channel_settings.channel_details.isPublic',
defaultMessage: 'Public channel or private channel',
})}
secondaryLabel={isDefault ? (
<FormattedMessage
id='admin.channel_settings.channel_details.isDefaultDescr'
defaultMessage='This default channel cannot be converted into a private channel.'
@@ -92,24 +94,42 @@ const AllowAllToggle = (props: Props): JSX.Element | null => {
) : (
<FormattedMessage
id='admin.channel_settings.channel_details.isPublicDescr'
defaultMessage='Select Public for a channel any user can find and join. {br}Select Private to require channel invitations to join. {br}Use this switch to change this channel from public to private or from private to public.'
defaultMessage='Select Public for a channel any user can find and join. {br}Select Private to require channel invitations to join. {br}Use this control to change this channel from public to private or from private to public.'
values={{br: (<br/>)}}
/>
)
}
onText={(
<FormattedMessage
id='channel_toggle_button.public'
defaultMessage='Public'
/>
)}
offText={(
<FormattedMessage
id='channel_toggle_button.private'
defaultMessage='Private'
/>
)}
/>
segments={[
{
id: 'public',
testId: 'allow-all-toggle-public',
selected: isPublic,
onSelect: () => setPublic(true),
label: (
<FormattedMessage
id='channel_toggle_button.public'
defaultMessage='Public'
/>
),
},
{
id: 'private',
testId: 'allow-all-toggle-private',
selected: !isPublic,
onSelect: () => setPublic(false),
label: (
<FormattedMessage
id='channel_toggle_button.private'
defaultMessage='Private'
/>
),
},
]}
>
<FormattedMessage
id='admin.channel_settings.channel_details.isPublic'
defaultMessage='Public channel or private channel'
/>
</AdminSettingsSegmentedRow>
);
};
@@ -119,23 +139,23 @@ const PolicyEnforceToggle = (props: Props): JSX.Element | null => {
return null;
}
let subTitle: JSX.Element;
let secondaryLabel: JSX.Element;
if (isDefault) {
subTitle = (
secondaryLabel = (
<FormattedMessage
id='admin.channel_settings.channel_details.default_channel_not_supported'
defaultMessage='The default channel cannot have a membership policy.'
/>
);
} else if (isPublic) {
subTitle = (
secondaryLabel = (
<FormattedMessage
id='admin.channel_settings.channel_details.attribute_based_description_public'
defaultMessage='Recommend this channel to users whose attributes match the rules and (optionally) auto-add them. Anyone can still join freely.'
/>
);
} else {
subTitle = (
secondaryLabel = (
<FormattedMessage
id='admin.channel_settings.channel_details.attribute_based_description'
defaultMessage='Restrict which users can be invited to this channel based on their user attributes and values. Only people who match the specified conditions will be allowed to be selected and added to this channel.'
@@ -144,30 +164,29 @@ const PolicyEnforceToggle = (props: Props): JSX.Element | null => {
}
return (
<LineSwitch
<AdminSettingsSwitch
id='policy-enforce-toggle'
// Keep the visual disabled state aligned with the click guard in
// onToggle (which short-circuits when isDefault is true). Without
// onChange (which short-circuits when isDefault is true). Without
// including isDefault here the toggle would look enabled but do
// nothing on the default channel.
disabled={isDisabled || isSynced || isDefault || !policyEnforcedToggleAvailable}
toggled={policyEnforced}
checked={policyEnforced}
last={true}
onToggle={() => {
onChange={() => {
if (isDefault || !policyEnforcedToggleAvailable) {
return;
}
onToggle(isSynced, isPublic, !policyEnforced);
}}
title={(
<FormattedMessage
id='admin.channel_settings.channel_details.policy_enforced_title'
defaultMessage='Enable attribute based channel access'
/>
)}
subTitle={subTitle}
/>
secondaryLabel={secondaryLabel}
>
<FormattedMessage
id='admin.channel_settings.channel_details.policy_enforced_title'
defaultMessage='Enable attribute based channel access'
/>
</AdminSettingsSwitch>
);
};
@@ -216,4 +235,3 @@ export const ChannelModes = (props: Props): JSX.Element => {
</AdminPanel>
);
};
@@ -1,51 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {type JSX} from 'react';
import Toggle from 'components/toggle';
type Props = {
id: string;
title: JSX.Element;
toggled: boolean;
subTitle: JSX.Element;
onToggle: () => void;
last?: boolean;
disabled?: boolean;
singleLine?: boolean;
children?: JSX.Element;
offText?: JSX.Element;
onText?: JSX.Element;
};
const LineSwitch = (props: Props): JSX.Element => {
const {title, subTitle, singleLine, toggled, onToggle, children, offText, onText, disabled, last, id} = props;
return (<div>
<div className='line-switch d-flex flex-sm-column flex-md-row align-items-sm-start align-items-center'>
<label className='line-switch__label'>{title}</label>
<div
data-testid={id}
className='line-switch__toggle'
>
<Toggle
id={id}
disabled={disabled}
onToggle={onToggle}
toggled={toggled}
onText={onText}
offText={offText}
/>
</div>
</div>
<div className='row'>
<div className='col-sm-10'>
<div className={`help-text-small help-text-no-padding ${singleLine ? 'help-text-single-line' : ''}`}>{subTitle}</div>
</div>
</div>
{children}
{!last && <div className='section-separator'><hr className='separator__hr'/></div>}
</div>);
};
export default LineSwitch;
@@ -61,106 +61,104 @@ exports[`admin_console/team_channel_settings/team/TeamDetails should match snaps
<div
class="group-teams-and-channels--body"
>
<div>
<div
class="line-switch d-flex flex-sm-column flex-md-row align-items-sm-start align-items-center"
<div
data-testid="allowAllToggleSwitch"
>
<label
class="switch switch--size-medium switch--semi-bold switch--with-secondary"
for="allowAllToggleSwitch"
>
<label
class="line-switch__label"
<span
class="switch__labels"
>
Anyone can join this team
</label>
<div
class="line-switch__toggle"
data-testid="allowAllToggleSwitch"
>
<button
aria-pressed="false"
class="btn btn-lg btn-toggle"
data-testid="allowAllToggleSwitch-button"
id="allowAllToggleSwitch"
tabindex="0"
type="button"
<span
class="switch__label"
>
<div
class="handle"
/>
</button>
</div>
</div>
<div
class="row"
>
<div
class="col-sm-10"
>
<div
class="help-text-small help-text-no-padding help-text-single-line"
Anyone can join this team
</span>
<span
class="switch__secondary-label"
>
This team can be discovered allowing anyone with an account to join this team.
</div>
</div>
</div>
<div
class="section-separator"
>
<hr
class="separator__hr"
/>
</div>
</div>
<div>
<div
class="line-switch d-flex flex-sm-column flex-md-row align-items-sm-start align-items-center"
>
<label
class="line-switch__label"
</span>
</span>
<span
class="switch__track"
>
Only specific email domains can join this team
</label>
<div
class="line-switch__toggle"
data-testid="allowedDomainsToggleSwitch"
>
<button
aria-pressed="false"
class="btn btn-lg btn-toggle"
data-testid="allowedDomainsToggleSwitch-button"
id="allowedDomainsToggleSwitch"
<input
aria-checked="false"
class="switch__input"
data-testid="allowAllToggleSwitch-button"
id="allowAllToggleSwitch"
role="switch"
tabindex="0"
type="button"
>
<div
class="handle"
/>
</button>
</div>
</div>
<div
class="row"
>
<div
class="col-sm-10"
>
<div
class="help-text-small help-text-no-padding help-text-single-line"
>
Users can only join the team if their email matches one of the specified domains
</div>
</div>
</div>
<div
class="help-text csvDomains"
>
Comma Separated Email Domain List
</div>
<input
class="form-control"
placeholder="mattermost.com"
type="text"
value=""
type="checkbox"
/>
<span
aria-hidden="true"
class="switch__knob"
/>
</span>
</label>
</div>
<div
class="section-separator"
>
<hr
class="separator__hr"
/>
</div>
<div
data-testid="allowedDomainsToggleSwitch"
>
<label
class="switch switch--size-medium switch--semi-bold switch--with-secondary"
for="allowedDomainsToggleSwitch"
>
<span
class="switch__labels"
>
<span
class="switch__label"
>
Only specific email domains can join this team
</span>
<span
class="switch__secondary-label"
>
Users can only join the team if their email matches one of the specified domains
</span>
</span>
<span
class="switch__track"
>
<input
aria-checked="false"
class="switch__input"
data-testid="allowedDomainsToggleSwitch-button"
id="allowedDomainsToggleSwitch"
role="switch"
tabindex="0"
type="checkbox"
/>
<span
aria-hidden="true"
class="switch__knob"
/>
</span>
</label>
</div>
<div
class="help-text csvDomains"
>
Comma Separated Email Domain List
</div>
<input
class="form-control"
placeholder="mattermost.com"
type="text"
value=""
/>
</div>
</div>
</div>
@@ -26,106 +26,105 @@ exports[`admin_console/team_channel_settings/team/TeamModes should match snapsho
<div
class="group-teams-and-channels--body"
>
<div>
<div
class="line-switch d-flex flex-sm-column flex-md-row align-items-sm-start align-items-center"
<div
data-testid="allowAllToggleSwitch"
>
<label
class="switch switch--size-medium switch--semi-bold switch--with-secondary"
for="allowAllToggleSwitch"
>
<label
class="line-switch__label"
<span
class="switch__labels"
>
Anyone can join this team
</label>
<div
class="line-switch__toggle"
data-testid="allowAllToggleSwitch"
>
<button
aria-pressed="false"
class="btn btn-lg btn-toggle"
data-testid="allowAllToggleSwitch-button"
id="allowAllToggleSwitch"
tabindex="0"
type="button"
<span
class="switch__label"
>
<div
class="handle"
/>
</button>
</div>
</div>
<div
class="row"
>
<div
class="col-sm-10"
>
<div
class="help-text-small help-text-no-padding help-text-single-line"
Anyone can join this team
</span>
<span
class="switch__secondary-label"
>
This team can be discovered allowing anyone with an account to join this team.
</div>
</div>
</div>
<div
class="section-separator"
>
<hr
class="separator__hr"
/>
</div>
</div>
<div>
<div
class="line-switch d-flex flex-sm-column flex-md-row align-items-sm-start align-items-center"
>
<label
class="line-switch__label"
</span>
</span>
<span
class="switch__track"
>
Only specific email domains can join this team
</label>
<div
class="line-switch__toggle"
data-testid="allowedDomainsToggleSwitch"
>
<button
aria-pressed="true"
class="btn btn-lg btn-toggle active"
data-testid="allowedDomainsToggleSwitch-button"
id="allowedDomainsToggleSwitch"
<input
aria-checked="false"
class="switch__input"
data-testid="allowAllToggleSwitch-button"
id="allowAllToggleSwitch"
role="switch"
tabindex="0"
type="button"
>
<div
class="handle"
/>
</button>
</div>
</div>
<div
class="row"
>
<div
class="col-sm-10"
>
<div
class="help-text-small help-text-no-padding help-text-single-line"
>
Users can only join the team if their email matches one of the specified domains
</div>
</div>
</div>
<div
class="help-text csvDomains"
>
Comma Separated Email Domain List
</div>
<input
class="form-control"
placeholder="mattermost.com"
type="text"
value=""
type="checkbox"
/>
<span
aria-hidden="true"
class="switch__knob"
/>
</span>
</label>
</div>
<div
class="section-separator"
>
<hr
class="separator__hr"
/>
</div>
<div
data-testid="allowedDomainsToggleSwitch"
>
<label
class="switch switch--size-medium switch--semi-bold switch--with-secondary"
for="allowedDomainsToggleSwitch"
>
<span
class="switch__labels"
>
<span
class="switch__label"
>
Only specific email domains can join this team
</span>
<span
class="switch__secondary-label"
>
Users can only join the team if their email matches one of the specified domains
</span>
</span>
<span
class="switch__track"
>
<input
aria-checked="true"
checked=""
class="switch__input"
data-testid="allowedDomainsToggleSwitch-button"
id="allowedDomainsToggleSwitch"
role="switch"
tabindex="0"
type="checkbox"
/>
<span
aria-hidden="true"
class="switch__knob"
/>
</span>
</label>
</div>
<div
class="help-text csvDomains"
>
Comma Separated Email Domain List
</div>
<input
class="form-control"
placeholder="mattermost.com"
type="text"
value=""
/>
</div>
</div>
</div>
@@ -542,7 +542,7 @@ describe('admin_console/team_channel_settings/team/TeamDetails', () => {
// Removing the last policy drops the enforce toggle on its own.
await waitFor(() => {
expect(screen.getByTestId('policy-enforce-toggle-button')).toHaveAttribute('aria-pressed', 'false');
expect(screen.getByTestId('policy-enforce-toggle-button')).not.toBeChecked();
});
// Save goes straight through — no spurious "Apply membership policy" modal.
@@ -646,7 +646,7 @@ describe('admin_console/team_channel_settings/team/TeamDetails', () => {
await userEvent.click(document.getElementById('confirmModalButton')!);
// Custom rule still governs, so enforcement stays on.
expect(screen.getByTestId('policy-enforce-toggle-button')).toHaveAttribute('aria-pressed', 'true');
expect(screen.getByTestId('policy-enforce-toggle-button')).toBeChecked();
// Removal is not new criteria: the unedited custom rule must not trigger the apply-count modal.
await userEvent.click(screen.getByText('Save'));
@@ -3,7 +3,7 @@
import React from 'react';
import {renderWithContext} from 'tests/react_testing_utils';
import {renderWithContext, screen} from 'tests/react_testing_utils';
import {TeamModes} from './team_modes';
@@ -20,4 +20,44 @@ describe('admin_console/team_channel_settings/team/TeamModes', () => {
);
expect(container).toMatchSnapshot();
});
test('keeps separator under Sync Group Members when synced and ABAC row follows', () => {
renderWithContext(
<TeamModes
onToggle={jest.fn()}
syncChecked={true}
allAllowedChecked={false}
allowedDomains={''}
allowedDomainsChecked={false}
isLicensedForLDAPGroups={true}
abacSupported={true}
policyEnforced={false}
policyEnforcedToggleAvailable={false}
/>,
);
expect(screen.getByTestId('policy-enforce-toggle')).toBeInTheDocument();
const syncRow = screen.getByTestId('syncGroupSwitch');
expect(syncRow.nextElementSibling).toHaveClass('section-separator');
});
test('omits separator under Sync Group Members when synced and no following row', () => {
renderWithContext(
<TeamModes
onToggle={jest.fn()}
syncChecked={true}
allAllowedChecked={false}
allowedDomains={''}
allowedDomainsChecked={false}
isLicensedForLDAPGroups={true}
abacSupported={false}
/>,
);
expect(screen.queryByTestId('policy-enforce-toggle')).not.toBeInTheDocument();
const syncRow = screen.getByTestId('syncGroupSwitch');
expect(syncRow.nextElementSibling).toBeNull();
});
});
@@ -7,7 +7,7 @@ import {FormattedMessage, defineMessage} from 'react-intl';
import ExternalLink from 'components/external_link';
import AdminPanel from 'components/widgets/admin_console/admin_panel';
import LineSwitch from '../../line_switch';
import AdminSettingsSwitch from '../../admin_settings_switch';
type Props = {
syncChecked: boolean;
@@ -18,20 +18,14 @@ type Props = {
isDisabled?: boolean;
};
const SyncGroupsToggle = ({syncChecked, allAllowedChecked, allowedDomainsChecked, allowedDomains, onToggle, isDisabled}: Props) => (
<LineSwitch
const SyncGroupsToggle = ({syncChecked, allAllowedChecked, allowedDomainsChecked, allowedDomains, onToggle, isDisabled, last}: Props & {last?: boolean}) => (
<AdminSettingsSwitch
id='syncGroupSwitch'
disabled={isDisabled}
toggled={syncChecked}
last={syncChecked}
onToggle={() => onToggle(!syncChecked, allAllowedChecked, allowedDomainsChecked, allowedDomains)}
title={(
<FormattedMessage
id='admin.team_settings.team_details.syncGroupMembers'
defaultMessage='Sync Group Members'
/>
)}
subTitle={(
checked={syncChecked}
last={last}
onChange={() => onToggle(!syncChecked, allAllowedChecked, allowedDomainsChecked, allowedDomains)}
secondaryLabel={(
<FormattedMessage
id='admin.team_settings.team_details.syncGroupMembersDescr'
defaultMessage='When enabled, adding and removing users from groups will add or remove them from this team. The only way of inviting members to this team is by adding the groups they belong to. <link>Learn More</link>'
@@ -47,69 +41,74 @@ const SyncGroupsToggle = ({syncChecked, allAllowedChecked, allowedDomainsChecked
}}
/>
)}
/>);
>
<FormattedMessage
id='admin.team_settings.team_details.syncGroupMembers'
defaultMessage='Sync Group Members'
/>
</AdminSettingsSwitch>
);
const AllowAllToggle = ({syncChecked, allAllowedChecked, allowedDomainsChecked, allowedDomains, onToggle, isDisabled}: Props) =>
(syncChecked ? null : (
<LineSwitch
<AdminSettingsSwitch
id='allowAllToggleSwitch'
disabled={isDisabled}
toggled={allAllowedChecked}
singleLine={true}
onToggle={() => onToggle(syncChecked, !allAllowedChecked, allowedDomainsChecked, allowedDomains)}
title={(
<FormattedMessage
id='admin.team_settings.team_details.anyoneCanJoin'
defaultMessage='Anyone can join this team'
/>
)}
subTitle={(
checked={allAllowedChecked}
onChange={() => onToggle(syncChecked, !allAllowedChecked, allowedDomainsChecked, allowedDomains)}
secondaryLabel={(
<FormattedMessage
id='admin.team_settings.team_details.anyoneCanJoinDescr'
defaultMessage='This team can be discovered allowing anyone with an account to join this team.'
/>
)}
/>));
>
<FormattedMessage
id='admin.team_settings.team_details.anyoneCanJoin'
defaultMessage='Anyone can join this team'
/>
</AdminSettingsSwitch>
));
const AllowedDomainsToggle = ({syncChecked, allAllowedChecked, allowedDomainsChecked, allowedDomains, onToggle, isDisabled, last = true}: Props & {last?: boolean}) =>
(syncChecked ? null : (
<LineSwitch
<AdminSettingsSwitch
id='allowedDomainsToggleSwitch'
disabled={isDisabled}
toggled={allowedDomainsChecked}
checked={allowedDomainsChecked}
last={last}
onToggle={() => onToggle(syncChecked, allAllowedChecked, !allowedDomainsChecked, allowedDomains)}
singleLine={true}
title={(
<FormattedMessage
id='admin.team_settings.team_details.specificDomains'
defaultMessage='Only specific email domains can join this team'
/>
)}
subTitle={(
onChange={() => onToggle(syncChecked, allAllowedChecked, !allowedDomainsChecked, allowedDomains)}
secondaryLabel={(
<FormattedMessage
id='admin.team_settings.team_details.specificDomainsDescr'
defaultMessage='Users can only join the team if their email matches one of the specified domains'
/>
)}
>
<>
<div className='help-text csvDomains'>
<FormattedMessage
id='admin.team_settings.team_details.csvDomains'
defaultMessage='Comma Separated Email Domain List'
footer={(
<>
<div className='help-text csvDomains'>
<FormattedMessage
id='admin.team_settings.team_details.csvDomains'
defaultMessage='Comma Separated Email Domain List'
/>
</div>
<input
type='text'
value={allowedDomains}
placeholder='mattermost.com'
className='form-control'
onChange={(e) => onToggle(syncChecked, allAllowedChecked, allowedDomainsChecked, e.currentTarget.value)}
disabled={isDisabled}
/>
</div>
<input
type='text'
value={allowedDomains}
placeholder='mattermost.com'
className='form-control'
onChange={(e) => onToggle(syncChecked, allAllowedChecked, allowedDomainsChecked, e.currentTarget.value)}
disabled={isDisabled}
/>
</>
</LineSwitch>));
</>
)}
>
<FormattedMessage
id='admin.team_settings.team_details.specificDomains'
defaultMessage='Only specific email domains can join this team'
/>
</AdminSettingsSwitch>
));
type PolicyEnforceToggleProps = {
syncChecked: boolean;
@@ -122,27 +121,21 @@ type PolicyEnforceToggleProps = {
};
const PolicyEnforceToggle = ({syncChecked, policyEnforced, policyEnforcedToggleAvailable, onToggle, isDisabled}: PolicyEnforceToggleProps) => (
<LineSwitch
<AdminSettingsSwitch
id='policy-enforce-toggle'
disabled={isDisabled || syncChecked || !policyEnforcedToggleAvailable}
toggled={policyEnforced}
checked={policyEnforced}
// The email-domains row renders after this one unless group sync hides it,
// in which case this is the last row.
// Email-domains (and anyone-can-join) hide when synced; this row stays
// visible (disabled) and is then the last row.
last={syncChecked}
onToggle={() => {
onChange={() => {
if (syncChecked || !policyEnforcedToggleAvailable) {
return;
}
onToggle?.(!policyEnforced);
}}
title={(
<FormattedMessage
id='admin.team_settings.team_details.policy_enforced_title'
defaultMessage='Manage membership with attribute based membership policies'
/>
)}
subTitle={syncChecked ? (
secondaryLabel={syncChecked ? (
<FormattedMessage
id='admin.team_settings.team_details.policy_enforced_group_synced'
defaultMessage='Group synced teams cannot use a membership policy. Disable group sync to manage access by attributes.'
@@ -153,7 +146,13 @@ const PolicyEnforceToggle = ({syncChecked, policyEnforced, policyEnforcedToggleA
defaultMessage='Restrict which users can be added to this team based on their user attributes and values. Only people who match the specified conditions will be allowed to be selected and added to this team.'
/>
)}
/>);
>
<FormattedMessage
id='admin.team_settings.team_details.policy_enforced_title'
defaultMessage='Manage membership with attribute based membership policies'
/>
</AdminSettingsSwitch>
);
type TeamModesProps = Props & {
isLicensedForLDAPGroups?: boolean;
@@ -163,16 +162,32 @@ type TeamModesProps = Props & {
onPolicyEnforcedToggle?: (policyEnforced: boolean) => void;
};
export const TeamModes = ({allAllowedChecked, syncChecked, allowedDomains, allowedDomainsChecked, onToggle, isDisabled, isLicensedForLDAPGroups, abacSupported, policyEnforced, policyEnforcedToggleAvailable, onPolicyEnforcedToggle}: TeamModesProps) => (
<AdminPanel
id='team_manage'
title={defineMessage({id: 'admin.team_settings.team_detail.manageTitle', defaultMessage: 'Team Management'})}
subtitle={defineMessage({id: 'admin.team_settings.team_detail.manageDescription', defaultMessage: 'Choose between inviting members manually or syncing members automatically from groups.'})}
>
<div className='group-teams-and-channels'>
<div className='group-teams-and-channels--body'>
{isLicensedForLDAPGroups && !policyEnforced &&
<SyncGroupsToggle
export const TeamModes = ({allAllowedChecked, syncChecked, allowedDomains, allowedDomainsChecked, onToggle, isDisabled, isLicensedForLDAPGroups, abacSupported, policyEnforced, policyEnforcedToggleAvailable, onPolicyEnforcedToggle}: TeamModesProps) => {
// Omit the Sync row separator only when nothing else in this panel is
// visible. ABAC still renders (disabled) while synced, so sync-on alone
// must not set last.
const syncIsLastVisible = Boolean(syncChecked && !abacSupported);
return (
<AdminPanel
id='team_manage'
title={defineMessage({id: 'admin.team_settings.team_detail.manageTitle', defaultMessage: 'Team Management'})}
subtitle={defineMessage({id: 'admin.team_settings.team_detail.manageDescription', defaultMessage: 'Choose between inviting members manually or syncing members automatically from groups.'})}
>
<div className='group-teams-and-channels'>
<div className='group-teams-and-channels--body'>
{isLicensedForLDAPGroups && !policyEnforced &&
<SyncGroupsToggle
allAllowedChecked={allAllowedChecked}
allowedDomainsChecked={allowedDomainsChecked}
allowedDomains={allowedDomains}
syncChecked={syncChecked}
onToggle={onToggle}
isDisabled={isDisabled}
last={syncIsLastVisible}
/>
}
<AllowAllToggle
allAllowedChecked={allAllowedChecked}
allowedDomainsChecked={allowedDomainsChecked}
allowedDomains={allowedDomains}
@@ -180,32 +195,25 @@ export const TeamModes = ({allAllowedChecked, syncChecked, allowedDomains, allow
onToggle={onToggle}
isDisabled={isDisabled}
/>
}
<AllowAllToggle
allAllowedChecked={allAllowedChecked}
allowedDomainsChecked={allowedDomainsChecked}
allowedDomains={allowedDomains}
syncChecked={syncChecked}
onToggle={onToggle}
isDisabled={isDisabled}
/>
{abacSupported &&
<PolicyEnforceToggle
{abacSupported &&
<PolicyEnforceToggle
syncChecked={syncChecked}
policyEnforced={Boolean(policyEnforced)}
policyEnforcedToggleAvailable={Boolean(policyEnforcedToggleAvailable)}
onToggle={onPolicyEnforcedToggle}
isDisabled={isDisabled}
/>
}
<AllowedDomainsToggle
allAllowedChecked={allAllowedChecked}
allowedDomainsChecked={allowedDomainsChecked}
allowedDomains={allowedDomains}
syncChecked={syncChecked}
policyEnforced={Boolean(policyEnforced)}
policyEnforcedToggleAvailable={Boolean(policyEnforcedToggleAvailable)}
onToggle={onPolicyEnforcedToggle}
onToggle={onToggle}
isDisabled={isDisabled}
/>
}
<AllowedDomainsToggle
allAllowedChecked={allAllowedChecked}
allowedDomainsChecked={allowedDomainsChecked}
allowedDomains={allowedDomains}
syncChecked={syncChecked}
onToggle={onToggle}
isDisabled={isDisabled}
/>
</div>
</div>
</div>
</AdminPanel>);
</AdminPanel>
);
};
@@ -24,29 +24,6 @@
gap: 16px;
}
.channel_banner_header, .channel_translation_header, .channel_join_leave_messages_header, .channel_shared_with_workspaces_header {
display: flex;
.channel_banner_header__text, .channel_translation_header__text, .channel_join_leave_messages_header__text, .channel_shared_with_workspaces_header__text {
display: flex;
flex: 1;
flex-direction: column;
gap: 8px;
span {
&.heading {
color: rgb(var(--center-channel-color-rgb));
font-size: 16px;
}
&.subheading {
color: rgba(var(--center-channel-color-rgb), 0.75);
font-size: 12px;
}
}
}
}
.channel_banner_section_body, .channel_shared_with_workspaces_section_body {
@include animations.fade-in;
@@ -141,7 +141,7 @@ describe('ChannelSettingsConfigurationTab', () => {
// Check that the toggle is not enabled
const toggle = screen.getByTestId('channelBannerToggle-button');
expect(toggle).toBeInTheDocument();
expect(toggle).not.toHaveClass('active');
expect(toggle).not.toBeChecked();
// Banner text and color inputs should not be visible when banner is disabled
expect(screen.queryByTestId('channel_banner_banner_text_textbox')).not.toBeInTheDocument();
@@ -155,7 +155,7 @@ describe('ChannelSettingsConfigurationTab', () => {
// Check that the toggle is enabled
const toggle = screen.getByTestId('channelBannerToggle-button');
expect(toggle).toBeInTheDocument();
expect(toggle).not.toHaveClass('active');
expect(toggle).not.toBeChecked();
// Click the toggle to enable the banner
await userEvent.click(screen.getByTestId('channelBannerToggle-button'));
@@ -175,7 +175,7 @@ describe('ChannelSettingsConfigurationTab', () => {
// Check that the toggle is enabled
const toggle = screen.getByTestId('channelBannerToggle-button');
expect(toggle).toBeInTheDocument();
expect(toggle).toHaveClass('active');
expect(toggle).toBeChecked();
// Banner text and color inputs should be visible when banner is enabled
expect(screen.getByTestId('channel_banner_banner_text_textbox')).toBeInTheDocument();
@@ -576,7 +576,7 @@ describe('ChannelSettingsConfigurationTab', () => {
const toggle = screen.getByTestId('shareChannelWithWorkspacesToggle-button');
await userEvent.click(toggle);
expect(toggle).toHaveClass('active');
expect(toggle).toBeChecked();
expect(screen.getByText('Add workspace')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', {name: 'Save'}));
@@ -589,7 +589,7 @@ describe('ChannelSettingsConfigurationTab', () => {
expect(screen.queryByRole('button', {name: 'Save'})).not.toBeInTheDocument();
}, {timeout: 2000});
expect(screen.getByTestId('shareChannelWithWorkspacesToggle-button')).not.toHaveClass('active');
expect(screen.getByTestId('shareChannelWithWorkspacesToggle-button')).not.toBeChecked();
expect(screen.queryByText('Add workspace')).not.toBeInTheDocument();
});
@@ -623,7 +623,7 @@ describe('ChannelSettingsConfigurationTab', () => {
expect(screen.queryByRole('button', {name: 'Save'})).not.toBeInTheDocument();
}, {timeout: 2000});
expect(screen.getByTestId('shareChannelWithWorkspacesToggle-button')).not.toHaveClass('active');
expect(screen.getByTestId('shareChannelWithWorkspacesToggle-button')).not.toBeChecked();
await userEvent.click(screen.getByTestId('shareChannelWithWorkspacesToggle-button'));
await userEvent.click(screen.getByRole('button', {name: /Add workspace/i}));
@@ -704,11 +704,11 @@ describe('ChannelSettingsConfigurationTab', () => {
);
await waitFor(() => {
expect(screen.getByTestId('shareChannelWithWorkspacesToggle-button')).toHaveClass('active');
expect(screen.getByTestId('shareChannelWithWorkspacesToggle-button')).toBeChecked();
});
await userEvent.click(screen.getByTestId('shareChannelWithWorkspacesToggle-button'));
expect(screen.getByTestId('shareChannelWithWorkspacesToggle-button')).not.toHaveClass('active');
expect(screen.getByTestId('shareChannelWithWorkspacesToggle-button')).not.toBeChecked();
remotes = [{...remote}];
getRemotesForChannel.mockImplementation(() => remotes);
@@ -720,7 +720,7 @@ describe('ChannelSettingsConfigurationTab', () => {
/>,
);
expect(screen.getByTestId('shareChannelWithWorkspacesToggle-button')).not.toHaveClass('active');
expect(screen.getByTestId('shareChannelWithWorkspacesToggle-button')).not.toBeChecked();
expect(screen.getByRole('button', {name: 'Save'})).toBeInTheDocument();
});
@@ -916,7 +916,7 @@ describe('ChannelSettingsConfigurationTab', () => {
);
await waitFor(() => {
expect(screen.getByTestId('shareChannelWithWorkspacesToggle-button')).toHaveClass('active');
expect(screen.getByTestId('shareChannelWithWorkspacesToggle-button')).toBeChecked();
});
await userEvent.click(screen.getByTestId('shareChannelWithWorkspacesToggle-button'));
@@ -937,7 +937,7 @@ describe('ChannelSettingsConfigurationTab', () => {
expect(screen.queryByRole('button', {name: 'Save'})).not.toBeInTheDocument();
}, {timeout: 2000});
expect(screen.getByTestId('shareChannelWithWorkspacesToggle-button')).not.toHaveClass('active');
expect(screen.getByTestId('shareChannelWithWorkspacesToggle-button')).not.toBeChecked();
expect(screen.queryByText('Add workspace')).not.toBeInTheDocument();
});
@@ -970,7 +970,7 @@ describe('ChannelSettingsConfigurationTab', () => {
);
await waitFor(() => {
expect(screen.getByTestId('shareChannelWithWorkspacesToggle-button')).toHaveClass('active');
expect(screen.getByTestId('shareChannelWithWorkspacesToggle-button')).toBeChecked();
});
await userEvent.click(screen.getByTestId('shareChannelWithWorkspacesToggle-button'));
@@ -1476,7 +1476,7 @@ describe('ChannelSettingsConfigurationTab', () => {
await waitFor(() => {
expect(screen.queryByRole('button', {name: 'Reset'})).not.toBeInTheDocument();
});
expect(toggle).toHaveClass('active');
expect(toggle).toBeChecked();
});
it('shows an error in the SaveChangesPanel when patchPropertyValues rejects', async () => {
@@ -5,6 +5,7 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import {Switch} from '@mattermost/compass-ui/components/switch';
import type {Channel, ChannelBanner} from '@mattermost/types/channels';
import type {ServerError} from '@mattermost/types/errors';
@@ -36,7 +37,6 @@ import ConfirmModal from 'components/confirm_modal';
import DropdownInput from 'components/dropdown_input';
import type {ValueType} from 'components/dropdown_input';
import SectionNotice from 'components/section_notice';
import Toggle from 'components/toggle';
import AdvancedTextbox from 'components/widgets/advanced_textbox/advanced_textbox';
import type {SaveChangesPanelState} from 'components/widgets/modals/components/save_changes_panel';
import SaveChangesPanel from 'components/widgets/modals/components/save_changes_panel';
@@ -894,41 +894,24 @@ function ChannelSettingsConfigurationTab({
{canManageClassification && (
<>
<div className='channel_banner_header'>
<div className='channel_banner_header__text'>
<label
className='Input_legend'
htmlFor='channelClassificationToggle'
>
<FormattedMessage
id='channel_settings.classification.title'
defaultMessage='Classification'
/>
</label>
<label
className='Input_subheading'
htmlFor='channelClassificationToggle'
>
<FormattedMessage
id='channel_settings.classification.description'
defaultMessage='When enabled, a classification level can be set for the channel with configurable indicators.'
/>
</label>
</div>
<div className='channel_banner_header__toggle'>
<Toggle
id='channelClassificationToggle'
ariaLabel={formatMessage({id: 'channel_settings.classification.title', defaultMessage: 'Classification'})}
size='btn-md'
disabled={false}
onToggle={handleClassificationToggle}
toggled={classificationEnabled}
tabIndex={0}
toggleClassName='btn-toggle-primary'
/>
</div>
</div>
<Switch
id='channelClassificationToggle'
size='medium'
checked={classificationEnabled}
onChange={handleClassificationToggle}
data-testid='channelClassificationToggle-button'
tabIndex={0}
semiBold={true}
secondaryLabel={formatMessage({
id: 'channel_settings.classification.description',
defaultMessage: 'When enabled, a classification level can be set for the channel with configurable indicators.',
})}
>
<FormattedMessage
id='channel_settings.classification.title'
defaultMessage='Classification'
/>
</Switch>
{classificationEnabled && (
<div className='channel_banner_section_body'>
@@ -977,35 +960,19 @@ function ChannelSettingsConfigurationTab({
{canManageBanner && (
<>
<div className='channel_banner_header'>
<div className='channel_banner_header__text'>
<label
className='Input_legend'
aria-label={bannerHeading}
>
{bannerHeading}
</label>
<label
className='Input_subheading'
aria-label={bannerHeading}
>
{bannerSubHeading}
</label>
</div>
<div className='channel_banner_header__toggle'>
<Toggle
id='channelBannerToggle'
ariaLabel={bannerHeading}
size='btn-md'
disabled={bannerLockedByClassification || bannerRequiredByAttribute}
onToggle={handleBannerToggle}
toggled={bannerSectionOn}
tabIndex={0}
toggleClassName='btn-toggle-primary'
/>
</div>
</div>
<Switch
id='channelBannerToggle'
size='medium'
checked={bannerSectionOn}
disabled={bannerLockedByClassification || bannerRequiredByAttribute}
onChange={handleBannerToggle}
data-testid='channelBannerToggle-button'
tabIndex={0}
semiBold={true}
secondaryLabel={bannerSubHeading}
>
{bannerHeading}
</Switch>
{
bannerSectionOn &&
@@ -1107,35 +1074,18 @@ function ChannelSettingsConfigurationTab({
)}
{canManageJoinLeaveMessages && (
<div className='channel_join_leave_messages_header'>
<div className='channel_join_leave_messages_header__text'>
<label
className='Input_legend'
aria-label={joinLeaveMessagesHeading}
>
{joinLeaveMessagesHeading}
</label>
<label
className='Input_subheading'
aria-label={joinLeaveMessagesSubHeading}
>
{joinLeaveMessagesSubHeading}
</label>
</div>
<div className='channel_join_leave_messages_header__toggle'>
<Toggle
id='channelJoinLeaveMessagesToggle'
ariaLabel={joinLeaveMessagesHeading}
size='btn-md'
disabled={false}
onToggle={handleJoinLeaveMessagesToggle}
toggled={!disableJoinLeaveMessages}
tabIndex={0}
toggleClassName='btn-toggle-primary'
/>
</div>
</div>
<Switch
id='channelJoinLeaveMessagesToggle'
size='medium'
checked={!disableJoinLeaveMessages}
onChange={handleJoinLeaveMessagesToggle}
data-testid='channelJoinLeaveMessagesToggle-button'
tabIndex={0}
semiBold={true}
secondaryLabel={joinLeaveMessagesSubHeading}
>
{joinLeaveMessagesHeading}
</Switch>
)}
{(canManageSharedChannels || canManageClassification || canManageBanner || canManageJoinLeaveMessages) && canManageChannelTranslation && (
@@ -1143,35 +1093,18 @@ function ChannelSettingsConfigurationTab({
)}
{canManageChannelTranslation && (
<div className='channel_translation_header'>
<div className='channel_translation_header__text'>
<label
className='Input_legend'
aria-label={autoTranslationHeading}
>
{autoTranslationHeading}
</label>
<label
className='Input_subheading'
aria-label={autoTranslationSubHeading}
>
{autoTranslationSubHeading}
</label>
</div>
<div className='channel_translation_header__toggle'>
<Toggle
id='channelTranslationToggle'
ariaLabel={autoTranslationHeading}
size='btn-md'
disabled={false}
onToggle={handleAutoTranslationToggle}
toggled={isChannelAutotranslated}
tabIndex={0}
toggleClassName='btn-toggle-primary'
/>
</div>
</div>
<Switch
id='channelTranslationToggle'
size='medium'
checked={isChannelAutotranslated}
onChange={handleAutoTranslationToggle}
data-testid='channelTranslationToggle-button'
tabIndex={0}
semiBold={true}
secondaryLabel={autoTranslationSubHeading}
>
{autoTranslationHeading}
</Switch>
)}
{showSaveChangesPanel && (
@@ -747,7 +747,7 @@ describe('ChannelSettingsInfoTab', () => {
stateWithDiscoverableFlag,
);
expect(screen.getByTestId('channel-settings-discoverable-toggle')).toBeInTheDocument();
expect(screen.getByTestId('channel-settings-discoverable-toggle')).toHaveAttribute('aria-pressed', 'false');
expect(screen.getByTestId('channel-settings-discoverable-toggle')).not.toBeChecked();
});
it('reflects the channel\'s existing discoverable value on mount', () => {
@@ -759,7 +759,7 @@ describe('ChannelSettingsInfoTab', () => {
/>,
stateWithDiscoverableFlag,
);
expect(screen.getByTestId('channel-settings-discoverable-toggle')).toHaveAttribute('aria-pressed', 'true');
expect(screen.getByTestId('channel-settings-discoverable-toggle')).toBeChecked();
});
it('disables the toggle when the user lacks manage_private_channel_discoverability', () => {
@@ -5,6 +5,7 @@ import React, {useCallback, useState, useEffect, useMemo, useRef} from 'react';
import {useIntl} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import {Switch} from '@mattermost/compass-ui/components/switch';
import type {Channel, ChannelType} from '@mattermost/types/channels';
import type {ServerError} from '@mattermost/types/errors';
@@ -21,7 +22,6 @@ import ChannelInfoAttributes from 'components/channel_attributes/channel_info_at
import type {ChannelInfoAttributesHandle} from 'components/channel_attributes/channel_info_attributes';
import ChannelNameFormField from 'components/channel_name_form_field/channel_name_form_field';
import type {TextboxElement} from 'components/textbox';
import Toggle from 'components/toggle';
import AdvancedTextbox from 'components/widgets/advanced_textbox/advanced_textbox';
import SaveChangesPanel, {type SaveChangesPanelState} from 'components/widgets/modals/components/save_changes_panel';
import PublicPrivateSelector from 'components/widgets/public-private-selector/public-private-selector';
@@ -569,35 +569,19 @@ function ChannelSettingsInfoTab({
still visible (transparency) but not editable. */}
{showDiscoverableToggle && (
<div className='ChannelSettingsModal__discoverableSection'>
<div className='channel_banner_header'>
<div className='channel_banner_header__text'>
<label
className='Input_legend'
aria-label={discoverableTitle}
>
{discoverableTitle}
</label>
<label
className='Input_subheading'
aria-label={discoverableDescription}
>
{discoverableDescription}
</label>
</div>
<div className='channel_banner_header__toggle'>
<Toggle
id='channel-settings-discoverable-toggle'
overrideTestId={true}
ariaLabel={discoverableTitle}
size='btn-md'
disabled={!canManageDiscoverability}
onToggle={handleDiscoverableToggle}
toggled={discoverable}
tabIndex={0}
toggleClassName='btn-toggle-primary'
/>
</div>
</div>
<Switch
id='channel-settings-discoverable-toggle'
size='medium'
checked={discoverable}
disabled={!canManageDiscoverability}
onChange={handleDiscoverableToggle}
data-testid='channel-settings-discoverable-toggle'
tabIndex={0}
semiBold={true}
secondaryLabel={discoverableDescription}
>
{discoverableTitle}
</Switch>
{!canManageDiscoverability && (
<p className='ChannelSettingsModal__discoverableReadOnly'>
{formatMessage({
@@ -132,24 +132,12 @@
margin-top: 16px;
}
// Discoverable Private Channels — same header/toggle layout as Channel Banner
// in the Configuration tab (no extra fields when toggled on).
// Discoverable Private Channels — Compass Switch settings-row (label + secondary).
&__discoverableSection {
display: flex;
flex-direction: column;
margin-top: 24px;
gap: 8px;
.channel_banner_header {
display: flex;
}
.channel_banner_header__text {
display: flex;
flex: 1;
flex-direction: column;
gap: 8px;
}
}
&__discoverableReadOnly {
@@ -71,7 +71,7 @@ describe('ShareChannelWithWorkspaces', () => {
expect(screen.getByText('Share with connected workspaces')).toBeInTheDocument();
expect(screen.getByText('Collaborate with trusted organizations in this channel.')).toBeInTheDocument();
const toggle = screen.getByTestId('shareChannelWithWorkspacesToggle-button');
expect(toggle).not.toHaveClass('active');
expect(toggle).not.toBeChecked();
});
});
@@ -83,7 +83,7 @@ describe('ShareChannelWithWorkspaces', () => {
});
const toggle = screen.getByTestId('shareChannelWithWorkspacesToggle-button');
expect(toggle).toHaveClass('active');
expect(toggle).toBeChecked();
expect(screen.getByText('Nebula Networks')).toBeInTheDocument();
});
@@ -4,14 +4,13 @@
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {Switch} from '@mattermost/compass-ui/components/switch';
import {WithTooltip} from '@mattermost/shared/components/tooltip';
import type {RemoteCluster} from '@mattermost/types/remote_clusters';
import type {RemoteClusterInfo} from '@mattermost/types/shared_channels';
import {Client4} from 'mattermost-redux/client';
import Toggle from 'components/toggle';
import AddWorkspaceDropdown, {type RemoteToAdd} from './add_workspace_dropdown';
import type {WorkspaceWithStatus} from './types';
import WorkspaceList from './workspace_list';
@@ -112,47 +111,33 @@ export default function ShareChannelWithWorkspaces({
return (
<div className='channel_shared_with_workspaces_container'>
<div className='channel_shared_with_workspaces_header'>
<div className='channel_shared_with_workspaces_header__text'>
<label
className='Input_legend'
aria-label={heading}
<WithTooltip
title={formatMessage({
id: 'channel_settings.share_channel_with_workspaces.disable_toggle_tooltip',
defaultMessage: 'No connected workspaces are available',
})}
hint={formatMessage({
id: 'channel_settings.share_channel_with_workspaces.disable_toggle_tooltip_hint',
defaultMessage: 'Contact your system admin to add one.',
})}
disabled={hasAvailableWorkspaces}
>
<div>
<Switch
id='shareChannelWithWorkspacesToggle'
size='medium'
checked={enabled && hasAvailableWorkspaces}
disabled={!hasAvailableWorkspaces}
onChange={handleToggle}
data-testid='shareChannelWithWorkspacesToggle-button'
tabIndex={hasAvailableWorkspaces ? 0 : -1}
semiBold={true}
secondaryLabel={subheading}
>
{heading}
</label>
<label
className='Input_subheading'
aria-label={subheading}
>
{subheading}
</label>
</Switch>
</div>
<WithTooltip
title={formatMessage({
id: 'channel_settings.share_channel_with_workspaces.disable_toggle_tooltip',
defaultMessage: 'No connected workspaces are available',
})}
hint={formatMessage({
id: 'channel_settings.share_channel_with_workspaces.disable_toggle_tooltip_hint',
defaultMessage: 'Contact your system admin to add one.',
})}
disabled={hasAvailableWorkspaces}
>
<div className='channel_shared_with_workspaces_header__toggle'>
<Toggle
id='shareChannelWithWorkspacesToggle'
ariaLabel={heading}
size='btn-md'
onToggle={handleToggle}
toggled={enabled && hasAvailableWorkspaces}
disabled={!hasAvailableWorkspaces}
tabIndex={hasAvailableWorkspaces ? 0 : -1}
toggleClassName='btn-toggle-primary'
/>
</div>
</WithTooltip>
</div>
</WithTooltip>
{!hasAvailableWorkspaces && (
<div className='channel_shared_with_workspaces_section_body'>
@@ -247,7 +247,7 @@ describe('RecapConfiguration', () => {
/>,
);
const toggle = screen.getByRole('button', {name: /run once/i});
const toggle = screen.getByRole('switch', {name: /run once/i});
expect(toggle).toBeDisabled();
});
@@ -259,7 +259,7 @@ describe('RecapConfiguration', () => {
/>,
);
const toggle = screen.getByRole('button', {name: /run once/i});
const toggle = screen.getByRole('switch', {name: /run once/i});
expect(toggle).not.toBeDisabled();
});
@@ -272,7 +272,7 @@ describe('RecapConfiguration', () => {
/>,
);
const toggle = screen.getByRole('button', {name: /run once/i});
const toggle = screen.getByRole('switch', {name: /run once/i});
expect(toggle).not.toBeDisabled();
});
});
@@ -5,10 +5,10 @@ import React, {useCallback, useState} from 'react';
import {useIntl, FormattedMessage} from 'react-intl';
import {ProductChannelsIcon, LightningBoltOutlineIcon, CheckCircleIcon} from '@mattermost/compass-icons/components';
import {Switch} from '@mattermost/compass-ui/components/switch';
import {WithTooltip} from '@mattermost/shared/components/tooltip';
import type {Channel} from '@mattermost/types/channels';
import Toggle from 'components/toggle';
const RECAP_NAME_MAX_LENGTH = 100;
type Props = {
@@ -53,13 +53,14 @@ const RecapConfiguration = ({
const runOnceToggle = (
<div className='run-once-toggle'>
<Toggle
<Switch
id='run-once-toggle'
toggled={runOnce}
onToggle={() => setRunOnce(!runOnce)}
size='btn-sm'
toggleClassName='btn-toggle-primary'
size='small'
checked={runOnce}
onChange={() => setRunOnce(!runOnce)}
disabled={runOnceDisabled}
data-testid='run-once-toggle-button'
tabIndex={0}
/>
<label
htmlFor='run-once-toggle'
@@ -1,5 +1,3 @@
@use 'components/toggle';
.InviteAs {
&__label {
color: var(--center-channel-color);
@@ -63,21 +63,9 @@
font-weight: 400;
}
// Discoverable toggle — same header/toggle layout as Channel Settings Info
// and Channel Banner in Channel Settings Configuration.
// Discoverable — Compass Switch settings-row (label + secondary).
.new-channel-modal-discoverable {
margin-top: 24px;
.channel_banner_header {
display: flex;
}
.channel_banner_header__text {
display: flex;
flex: 1;
flex-direction: column;
gap: 8px;
}
}
.new-channel-modal-classification {
@@ -6,6 +6,7 @@ import React, {useCallback, useMemo, useRef, useState} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';
import {shallowEqual, useDispatch, useSelector} from 'react-redux';
import {Switch} from '@mattermost/compass-ui/components/switch';
import {GenericModal} from '@mattermost/components';
import {WithTooltip} from '@mattermost/shared/components/tooltip';
import type {Board} from '@mattermost/types/boards';
@@ -44,7 +45,6 @@ import useClassificationMarkings from 'components/common/hooks/useClassification
import DropdownInput from 'components/dropdown_input';
import type {ValueType} from 'components/dropdown_input';
import type {TextboxElement} from 'components/textbox';
import Toggle from 'components/toggle';
import AdvancedTextbox from 'components/widgets/advanced_textbox/advanced_textbox';
import Input from 'components/widgets/inputs/input/input';
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
@@ -602,34 +602,18 @@ const NewChannelModal = () => {
className='new-channel-modal-discoverable'
data-testid='new-channel-discoverable-section'
>
<div className='channel_banner_header'>
<div className='channel_banner_header__text'>
<label
className='Input_legend'
aria-label={discoverableTitle}
>
{discoverableTitle}
</label>
<label
className='Input_subheading'
aria-label={discoverableDescription}
>
{discoverableDescription}
</label>
</div>
<div className='channel_banner_header__toggle'>
<Toggle
id='newChannelDiscoverableToggle'
overrideTestId={true}
ariaLabel={discoverableTitle}
size='btn-md'
toggled={discoverable}
onToggle={() => setDiscoverable((v) => !v)}
tabIndex={0}
toggleClassName='btn-toggle-primary'
/>
</div>
</div>
<Switch
id='newChannelDiscoverableToggle'
size='medium'
checked={discoverable}
onChange={() => setDiscoverable((v) => !v)}
data-testid='newChannelDiscoverableToggle'
tabIndex={0}
semiBold={true}
secondaryLabel={discoverableDescription}
>
{discoverableTitle}
</Switch>
</div>
)}
{showDefaultCategorySelector && (
@@ -701,13 +685,14 @@ const NewChannelModal = () => {
/>
</h4>
</div>
<Toggle
<Switch
id='channelClassificationToggle'
toggled={classificationEnabled}
onToggle={() => setClassificationEnabled(!classificationEnabled)}
toggleClassName='btn-toggle-primary'
size='btn-md'
ariaLabel={formatMessage({id: 'channel_modal.classification.toggle_label', defaultMessage: 'Channel classification'})}
size='medium'
checked={classificationEnabled}
onChange={() => setClassificationEnabled(!classificationEnabled)}
data-testid='channelClassificationToggle-button'
tabIndex={0}
aria-label={formatMessage({id: 'channel_modal.classification.toggle_label', defaultMessage: 'Channel classification'})}
/>
</div>
<p className='new-channel-modal-classification__description'>
@@ -0,0 +1,47 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {WithTestMenuContext} from 'components/menu/menu_context_test';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
import {ToggleItem} from './post_priority_picker_item';
function renderToggleItem(props: Partial<React.ComponentProps<typeof ToggleItem>> = {}) {
const onClick = jest.fn();
renderWithContext(
<WithTestMenuContext>
<ToggleItem
ariaLabel='Request acknowledgement'
description='An acknowledgement button will appear with your message'
disabled={false}
icon={<span>{'icon'}</span>}
onClick={onClick}
text='Request acknowledgement'
toggled={false}
{...props}
/>
</WithTestMenuContext>,
);
return {onClick};
}
describe('ToggleItem', () => {
it('calls onClick once when the switch is clicked', async () => {
const {onClick} = renderToggleItem();
await userEvent.click(screen.getByRole('switch', {name: 'Request acknowledgement'}));
expect(onClick).toHaveBeenCalledTimes(1);
});
it('calls onClick once when the menu row is clicked', async () => {
const {onClick} = renderToggleItem();
await userEvent.click(screen.getByRole('menuitemcheckbox', {name: 'Request acknowledgement'}));
expect(onClick).toHaveBeenCalledTimes(1);
});
});
@@ -6,9 +6,9 @@ import React from 'react';
import styled from 'styled-components';
import {CheckIcon, AlertOutlineIcon, AlertCircleOutlineIcon, MessageTextOutlineIcon, CheckCircleOutlineIcon, BellRingOutlineIcon} from '@mattermost/compass-icons/components';
import {Switch} from '@mattermost/compass-ui/components/switch';
import {MenuItem} from 'components/menu/menu_item';
import Toggle from 'components/toggle';
type ToggleProps = {
ariaLabel?: string;
@@ -108,6 +108,12 @@ function ToggleItem({
text,
toggled,
}: ToggleProps) {
// Compass Switch fires onChange on its own click; without stopping
// propagation the MenuItem onClick also runs and the toggle nets out.
const handleSwitchClick = (event: React.MouseEvent) => {
event.stopPropagation();
};
return (
<Wrapper
onClick={disabled ? undefined : onClick}
@@ -117,17 +123,18 @@ function ToggleItem({
role='menuitemcheckbox'
aria-checked={toggled}
aria-label={ariaLabel}
trailingElements={<>
<Toggle
ariaLabel={ariaLabel}
size='btn-sm'
disabled={disabled}
onToggle={onClick}
toggled={toggled}
toggleClassName='btn-toggle-primary'
tabIndex={-1}
/>
</>}
trailingElements={
<span onClick={disabled ? undefined : handleSwitchClick}>
<Switch
size='small'
disabled={disabled}
onChange={onClick}
checked={toggled}
tabIndex={-1}
aria-label={ariaLabel}
/>
</span>
}
labels={<>
<div>
{text}
@@ -6,6 +6,7 @@ import {useIntl, FormattedMessage} from 'react-intl';
import {useDispatch} from 'react-redux';
import {DotsHorizontalIcon, PencilOutlineIcon, TrashCanOutlineIcon} from '@mattermost/compass-icons/components';
import {Switch} from '@mattermost/compass-ui/components/switch';
import type {ScheduledRecap} from '@mattermost/types/recaps';
import {pauseScheduledRecap, resumeScheduledRecap, deleteScheduledRecap} from 'mattermost-redux/actions/recaps';
@@ -13,7 +14,6 @@ import type {ActionResult} from 'mattermost-redux/types/actions';
import ConfirmModal from 'components/confirm_modal';
import * as Menu from 'components/menu';
import Toggle from 'components/toggle';
import {useScheduleDisplay} from './schedule_display';
@@ -97,14 +97,15 @@ const ScheduledRecapItem = ({scheduledRecap, onEdit}: Props) => {
</div>
<div className='scheduled-recap-toggle'>
<Toggle
<Switch
id={`toggle-${scheduledRecap.id}`}
toggled={scheduledRecap.enabled}
onToggle={handleToggle}
size='small'
checked={scheduledRecap.enabled}
onChange={handleToggle}
disabled={isToggling}
size='btn-sm'
toggleClassName='btn-toggle-primary'
ariaLabel={scheduledRecap.enabled ?
data-testid={`toggle-${scheduledRecap.id}-button`}
tabIndex={0}
aria-label={scheduledRecap.enabled ?
formatMessage({id: 'recaps.scheduled.toggle.active', defaultMessage: 'Active - click to pause'}) :
formatMessage({id: 'recaps.scheduled.toggle.paused', defaultMessage: 'Paused - click to resume'})
}
-75
View File
@@ -1,75 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import classNames from 'classnames';
import React from 'react';
type Props = {
onToggle: () => void;
toggled?: boolean;
disabled?: boolean;
onText?: React.ReactNode;
offText?: React.ReactNode;
id?: string;
overrideTestId?: boolean;
size?: 'btn-lg' | 'btn-md' | 'btn-sm';
toggleClassName?: string;
ariaLabel?: string;
tabIndex?: number;
};
const Toggle: React.FC<Props> = (props: Props) => {
const {
onToggle,
toggled,
disabled,
onText,
offText,
id,
overrideTestId,
ariaLabel,
size = 'btn-lg',
toggleClassName = 'btn-toggle',
tabIndex = 0,
} = props;
let dataTestId = `${id}-button`;
if (overrideTestId) {
dataTestId = id || '';
}
const className = classNames(
'btn',
size,
toggleClassName,
{
active: toggled,
disabled,
},
);
return (
<button
aria-label={ariaLabel}
data-testid={dataTestId}
id={id}
type='button'
onClick={onToggle}
className={className}
aria-pressed={toggled ? 'true' : 'false'}
disabled={disabled}
tabIndex={tabIndex}
>
<div className='handle'/>
{text(toggled, onText, offText)}
</button>
);
};
function text(toggled?: boolean, onText?: React.ReactNode, offText?: React.ReactNode): React.ReactNode | null {
if ((toggled && !onText) || (!toggled && !offText)) {
return null;
}
return (<div className={`bg-text ${toggled ? 'on' : 'off'}`}>{toggled ? onText : offText}</div>);
}
export default Toggle;
+1 -1
View File
@@ -3148,7 +3148,7 @@
"description": "Title of the toggle on the Channel Management panel that switches the channel between public and private."
},
"admin.channel_settings.channel_details.isPublicDescr": {
"defaultMessage": "Select Public for a channel any user can find and join. {br}Select Private to require channel invitations to join. {br}Use this switch to change this channel from public to private or from private to public.",
"defaultMessage": "Select Public for a channel any user can find and join. {br}Select Private to require channel invitations to join. {br}Use this control to change this channel from public to private or from private to public.",
"description": "Description of the public/private toggle on the System Console channel configuration page; {br} placeholders insert line breaks."
},
"admin.channel_settings.channel_details.policy_enforced_title": {
@@ -1,7 +0,0 @@
.line-switch {
justify-content: space-between;
}
.line-switch__label {
margin: 0;
}
@@ -46,8 +46,6 @@
@use 'sidebar-card';
@use 'sidebar-header-dropdown-button';
@use 'single_image_view';
@use 'toggle';
@use 'team-channel-settings';
@use 'infinite-scroll';
@use 'line-switch';
@use 'apps-modal';
@@ -1,159 +0,0 @@
@use "utils/variables";
@use 'sass:color';
// Based on https://codepen.io/aanjulena/pen/ZLZjzV
// Colors
$brand-primary: #2a9fd8;
variables.$gray: #6b7381;
$gray-light: color.adjust(variables.$gray, $lightness: 15%);
$gray-lighter: color.adjust(variables.$gray, $lightness: 30%);
// Button Colors
$btn-default-color: variables.$gray;
$btn-default-bg: $gray-lighter;
// Toggle Sizes
$toggle-default-size: 1.5rem;
$toggle-default-margin: 4rem;
$toggle-default-font-size: 0.75rem;
// Mixin for Switch Colors
// Variables: $color, $bg, $active-bg
@mixin toggle-color($color: $btn-default-color, $bg: $btn-default-bg, $active-bg: $brand-primary) {
background: $bg;
color: $color;
&.active {
background-color: $active-bg;
}
}
// Mixin for Default Switch Styles
// Variables: $size, $margin, $color, $bg, $active-bg, $font-size
@mixin toggle-mixin($size: $toggle-default-size, $margin: $toggle-default-margin, $font-size: $toggle-default-font-size) {
$handlePosition: calc($size / 7);
$handleSize: calc($size - (2 * $handlePosition));
position: relative;
min-width: calc($size * 2);
height: $size;
// color: $color;
// background: $bg;
padding: 0;
border: none;
border-radius: $size;
float: right;
&:focus,
&.focus {
&,
&.active {
outline: none;
}
}
> .handle {
position: absolute;
top: $handlePosition;
left: $handlePosition;
width: $handleSize;
height: $handleSize;
border-radius: 1rem;
background: #fff;
transition: left 0.25s;
}
&.active {
transition: background-color 0.25s;
> .handle {
right: $handlePosition;
left: inherit;
transition: left 0.25s;
}
}
}
// Apply Mixin to different sizes & colors
.btn-toggle {
@include toggle-color;
&.disabled {
cursor: not-allowed;
opacity: 0.5;
}
&.btn-lg {
@include toggle-mixin($size: 2.8rem, $font-size: 1rem, $margin: 5rem);
}
&.btn-md {
@include toggle-mixin($size: 2.2rem, $font-size: 0.8rem, $margin: 3rem);
}
&.btn-sm {
@include toggle-mixin($font-size: 0.55rem, $margin: 0.5rem);
}
&.btn-xs {
@include toggle-mixin($size:1rem,$margin:0);
}
.bg-text {
color: #fff;
font-size: 1.4rem;
font-weight: 600;
&.off {
padding: 0 1.6rem 0 3.4rem;
}
&.on {
padding: 0 3.4rem 0 1.6rem;
}
}
}
.btn-toggle-primary {
@include toggle-color(
$color: rgb(var(--button-color-rgb)),
$bg: rgba(var(--center-channel-color-rgb), 0.32),
$active-bg: rgb(var(--button-bg-rgb)),
);
&.disabled {
cursor: not-allowed;
opacity: 0.5;
}
&.btn-lg {
@include toggle-mixin($size: 2.8rem, $font-size: 1rem, $margin: 5rem);
}
&.btn-md {
@include toggle-mixin($size: 2.2rem, $font-size: 0.8rem, $margin: 3rem);
}
&.btn-sm {
@include toggle-mixin($font-size: 0.55rem, $margin: 0.5rem);
}
&.btn-xs {
@include toggle-mixin($size:1rem,$margin:0);
}
.bg-text {
color: #fff;
font-size: 1.4rem;
font-weight: 600;
&.off {
padding: 0 1.6rem 0 3.4rem;
}
&.on {
padding: 0 3.4rem 0 1.6rem;
}
}
}
@@ -352,10 +352,6 @@
}
}
.line-switch__toggle {
margin: 8px 0;
}
.backstage-list__item {
.item-actions,
.actions {
@@ -0,0 +1,26 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/**
* Stable Compass Switch CSS-module class names for Jest snapshots.
* The published `@mattermost/compass-ui` Switch.module.scss.js embeds
* content-hash suffixes that can differ across installs of the same version,
* which makes DOM snapshots flake across environments.
*/
const styles = {
switch: 'switch',
switch__input: 'switch__input',
'switch__secondary-label': 'switch__secondary-label',
switch__labels: 'switch__labels',
switch__label: 'switch__label',
switch__track: 'switch__track',
switch__knob: 'switch__knob',
'switch--size-small': 'switch--size-small',
'switch--size-medium': 'switch--size-medium',
'switch--size-large': 'switch--size-large',
'switch--semi-bold': 'switch--semi-bold',
'switch--with-secondary': 'switch--with-secondary',
};
module.exports = styles;
module.exports.default = styles;