mirror of
https://github.com/mattermost/mattermost.git
synced 2026-02-18 18:18:23 -05:00
Mm 65975 - migrate team modal to generic modal (#35256)
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres with binary parameters (push) Blocked by required conditions
Server CI / Postgres (push) Blocked by required conditions
Server CI / Postgres (FIPS) (push) Blocked by required conditions
Server CI / Generate Test Coverage (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Blocked by required conditions
Web App CI / check-types (push) Blocked by required conditions
Web App CI / test (platform) (push) Blocked by required conditions
Web App CI / test (mattermost-redux) (push) Blocked by required conditions
Web App CI / test (channels shard 1/4) (push) Blocked by required conditions
Web App CI / test (channels shard 2/4) (push) Blocked by required conditions
Web App CI / test (channels shard 3/4) (push) Blocked by required conditions
Web App CI / test (channels shard 4/4) (push) Blocked by required conditions
Web App CI / upload-coverage (push) Blocked by required conditions
Web App CI / build (push) Blocked by required conditions
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres with binary parameters (push) Blocked by required conditions
Server CI / Postgres (push) Blocked by required conditions
Server CI / Postgres (FIPS) (push) Blocked by required conditions
Server CI / Generate Test Coverage (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Blocked by required conditions
Web App CI / check-types (push) Blocked by required conditions
Web App CI / test (platform) (push) Blocked by required conditions
Web App CI / test (mattermost-redux) (push) Blocked by required conditions
Web App CI / test (channels shard 1/4) (push) Blocked by required conditions
Web App CI / test (channels shard 2/4) (push) Blocked by required conditions
Web App CI / test (channels shard 3/4) (push) Blocked by required conditions
Web App CI / test (channels shard 4/4) (push) Blocked by required conditions
Web App CI / upload-coverage (push) Blocked by required conditions
Web App CI / build (push) Blocked by required conditions
* MM-65975 - migrate team modal to generic modal * add e2e tests to team settings * apply self review fixes, organize test pom and clean code * fix cypress tests
This commit is contained in:
parent
2da1e56e6c
commit
cef5134865
23 changed files with 1096 additions and 409 deletions
|
|
@ -49,7 +49,7 @@ describe('Environment', () => {
|
|||
cy.uiSave().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Close the modal
|
||||
cy.get('#teamSettingsModalLabel').find('button').should('be.visible').click();
|
||||
cy.get('button[aria-label="Close"]').should('be.visible').click();
|
||||
});
|
||||
|
||||
// Validate that the image is being displayed
|
||||
|
|
@ -93,7 +93,7 @@ describe('Environment', () => {
|
|||
cy.uiSave().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Close the modal
|
||||
cy.get('#teamSettingsModalLabel').find('button').should('be.visible').click();
|
||||
cy.get('button[aria-label="Close"]').should('be.visible').click();
|
||||
});
|
||||
|
||||
// Validate that the image is being displayed
|
||||
|
|
@ -137,7 +137,7 @@ describe('Environment', () => {
|
|||
cy.uiSave().wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
// # Close the modal
|
||||
cy.get('#teamSettingsModalLabel').find('button').should('be.visible').click();
|
||||
cy.get('button[aria-label="Close"]').should('be.visible').click();
|
||||
});
|
||||
|
||||
// Validate that the image is being displayed
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ describe('Team Settings', () => {
|
|||
cy.get('#allowedDomains').should('have.text', 'corp.mattermost.com, mattermost.com');
|
||||
|
||||
// # Close the modal
|
||||
cy.get('#teamSettingsModalLabel').find('button').should('be.visible').click();
|
||||
cy.get('button[aria-label="Close"]').should('be.visible').click();
|
||||
});
|
||||
|
||||
// # Open the 'Invite People' full screen modal
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export const allowOnlyUserFromSpecificDomain = (domain) => {
|
|||
cy.findByText('Save').should('be.visible').click();
|
||||
|
||||
// # Close the modal
|
||||
cy.get('#teamSettingsModalLabel').find('button').should('be.visible').click();
|
||||
cy.get('button[aria-label="Close"]').should('be.visible').click();
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,8 @@ function openTeamSettingsDialog() {
|
|||
cy.uiOpenTeamMenu('Team settings');
|
||||
|
||||
// * Verify the team settings dialog is open
|
||||
cy.get('#teamSettingsModalLabel').should('be.visible').and('contain', 'Team Settings');
|
||||
cy.get('#teamSettingsModal').should('be.visible');
|
||||
cy.get('.modal-title').should('be.visible').and('contain', 'Team Settings');
|
||||
|
||||
cy.get('.team-picture-section').within(() => {
|
||||
// * Verify the edit icon is visible
|
||||
|
|
|
|||
|
|
@ -9,4 +9,3 @@ export {createRandomPost} from './post';
|
|||
export {createNewTeam, createRandomTeam} from './team';
|
||||
export {createNewUserProfile, createRandomUser, getDefaultAdminUser, isOutsideRemoteUserHour} from './user';
|
||||
export {installAndEnablePlugin, isPluginActive, getPluginStatus} from './plugin';
|
||||
//getPluginStatus
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class AccessSettings {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly allowedDomainsCheckbox;
|
||||
readonly allowedDomainsInput;
|
||||
readonly allowOpenInviteCheckbox;
|
||||
readonly regenerateButton;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.allowedDomainsCheckbox = container.locator('input[name="showAllowedDomains"]');
|
||||
this.allowedDomainsInput = container.locator('#allowedDomains input');
|
||||
this.allowOpenInviteCheckbox = container.locator('input[name="allowOpenInvite"]');
|
||||
this.regenerateButton = container.locator('button[data-testid="regenerateButton"]');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async enableAllowedDomains() {
|
||||
const isChecked = await this.allowedDomainsCheckbox.isChecked();
|
||||
if (!isChecked) {
|
||||
await this.allowedDomainsCheckbox.check();
|
||||
}
|
||||
}
|
||||
|
||||
async addDomain(domain: string) {
|
||||
await expect(this.allowedDomainsInput).toBeVisible();
|
||||
await this.allowedDomainsInput.fill(domain);
|
||||
await this.allowedDomainsInput.press('Enter');
|
||||
}
|
||||
|
||||
async removeDomain(domain: string) {
|
||||
const removeButton = this.container.locator(`div[role="button"][aria-label*="Remove ${domain}"]`);
|
||||
await expect(removeButton).toBeVisible();
|
||||
await removeButton.click();
|
||||
}
|
||||
|
||||
async toggleOpenInvite() {
|
||||
await expect(this.allowOpenInviteCheckbox).toBeVisible();
|
||||
await this.allowOpenInviteCheckbox.click();
|
||||
}
|
||||
|
||||
async regenerateInviteId() {
|
||||
await expect(this.regenerateButton).toBeVisible();
|
||||
await this.regenerateButton.click();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
export default class InfoSettings {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly nameInput;
|
||||
readonly descriptionInput;
|
||||
readonly uploadInput;
|
||||
readonly removeImageButton;
|
||||
readonly teamIconImage;
|
||||
readonly teamIconInitial;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.nameInput = container.locator('input#teamName');
|
||||
this.descriptionInput = container.locator('textarea#teamDescription');
|
||||
this.uploadInput = container.locator('input[data-testid="uploadPicture"]');
|
||||
this.removeImageButton = container.locator('button[data-testid="removeImageButton"]');
|
||||
this.teamIconImage = container.locator('#teamIconImage');
|
||||
this.teamIconInitial = container.locator('#teamIconInitial');
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async updateName(name: string) {
|
||||
await expect(this.nameInput).toBeVisible();
|
||||
await this.nameInput.clear();
|
||||
await this.nameInput.fill(name);
|
||||
}
|
||||
|
||||
async updateDescription(description: string) {
|
||||
await expect(this.descriptionInput).toBeVisible();
|
||||
await this.descriptionInput.clear();
|
||||
await this.descriptionInput.fill(description);
|
||||
}
|
||||
|
||||
async uploadIcon(filePath: string) {
|
||||
await this.uploadInput.setInputFiles(filePath);
|
||||
await expect(this.teamIconImage).toBeVisible();
|
||||
}
|
||||
|
||||
async removeIcon() {
|
||||
await expect(this.removeImageButton).toBeVisible();
|
||||
await this.removeImageButton.click();
|
||||
await expect(this.teamIconInitial).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
|
@ -3,14 +3,77 @@
|
|||
|
||||
import {Locator, expect} from '@playwright/test';
|
||||
|
||||
import InfoSettings from './info_settings';
|
||||
import AccessSettings from './access_settings';
|
||||
|
||||
export default class TeamSettingsModal {
|
||||
readonly container: Locator;
|
||||
|
||||
readonly closeButton;
|
||||
|
||||
readonly infoTab;
|
||||
readonly accessTab;
|
||||
|
||||
readonly saveButton;
|
||||
readonly undoButton;
|
||||
|
||||
readonly infoSettings;
|
||||
readonly accessSettings;
|
||||
|
||||
constructor(container: Locator) {
|
||||
this.container = container;
|
||||
|
||||
this.closeButton = container.locator('.modal-header button.close').first();
|
||||
|
||||
this.infoTab = container.locator('[data-testid="info-tab-button"]');
|
||||
this.accessTab = container.locator('[data-testid="access-tab-button"]');
|
||||
|
||||
this.saveButton = container.locator('button[data-testid="SaveChangesPanel__save-btn"]');
|
||||
this.undoButton = container.locator('button[data-testid="SaveChangesPanel__cancel-btn"]');
|
||||
|
||||
this.infoSettings = new InfoSettings(container);
|
||||
this.accessSettings = new AccessSettings(container);
|
||||
}
|
||||
|
||||
async toBeVisible() {
|
||||
await expect(this.container).toBeVisible();
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.closeButton.click();
|
||||
}
|
||||
|
||||
async openInfoTab(): Promise<InfoSettings> {
|
||||
await expect(this.infoTab).toBeVisible();
|
||||
await this.infoTab.click();
|
||||
|
||||
return this.infoSettings;
|
||||
}
|
||||
|
||||
async openAccessTab(): Promise<AccessSettings> {
|
||||
await expect(this.accessTab).toBeVisible();
|
||||
await this.accessTab.click();
|
||||
|
||||
return this.accessSettings;
|
||||
}
|
||||
|
||||
async save() {
|
||||
await expect(this.saveButton).toBeVisible();
|
||||
await this.saveButton.click();
|
||||
}
|
||||
|
||||
async undo() {
|
||||
await expect(this.undoButton).toBeVisible();
|
||||
await this.undoButton.click();
|
||||
}
|
||||
|
||||
async verifySavedMessage() {
|
||||
const savedMessage = this.container.getByText('Settings saved');
|
||||
await expect(savedMessage).toBeVisible({timeout: 5000});
|
||||
}
|
||||
|
||||
async verifyUnsavedChanges() {
|
||||
const warningText = this.container.locator('.SaveChangesPanel:has-text("You have unsaved changes")');
|
||||
await expect(warningText).toBeVisible({timeout: 3000});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,14 @@
|
|||
import {expect, Page} from '@playwright/test';
|
||||
import {waitUntil} from 'async-wait-until';
|
||||
|
||||
import {ChannelsPost, ChannelSettingsModal, SettingsModal, components, InvitePeopleModal} from '@/ui/components';
|
||||
import {
|
||||
ChannelsPost,
|
||||
ChannelSettingsModal,
|
||||
SettingsModal,
|
||||
TeamSettingsModal,
|
||||
components,
|
||||
InvitePeopleModal,
|
||||
} from '@/ui/components';
|
||||
import {duration} from '@/util';
|
||||
export default class ChannelsPage {
|
||||
readonly channels = 'Channels';
|
||||
|
|
@ -152,6 +159,14 @@ export default class ChannelsPage {
|
|||
return {rootPost, sidebarRight, lastPost};
|
||||
}
|
||||
|
||||
async openTeamSettings(): Promise<TeamSettingsModal> {
|
||||
await this.page.locator('#sidebarTeamMenuButton').click();
|
||||
await this.page.getByText('Team settings').first().click();
|
||||
await this.teamSettingsModal.toBeVisible();
|
||||
|
||||
return this.teamSettingsModal;
|
||||
}
|
||||
|
||||
async openChannelSettings(): Promise<ChannelSettingsModal> {
|
||||
await this.centerView.header.openChannelMenu();
|
||||
await this.page.locator('#channelSettings[role="menuitem"]').click();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,489 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/**
|
||||
* @objective Complete E2E test suite for Team Settings Modal
|
||||
* @reference MM-65975 - Migrate Team Settings Modal to GenericModal
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
|
||||
import {ChannelsPage, expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
// Asset file path for team icon uploads
|
||||
const TEAM_ICON_ASSET = path.resolve(__dirname, '../../../../lib/src/asset/mattermost-icon_128x128.png');
|
||||
|
||||
test.describe('Team Settings Modal - Complete Test Suite', () => {
|
||||
/**
|
||||
* MM-TXXXX: Open and close Team Settings Modal
|
||||
* @objective Verify basic modal open/close functionality
|
||||
*/
|
||||
test('MM-TXXXX Open and close Team Settings Modal', async ({pw}) => {
|
||||
// # Set up admin user and login
|
||||
const {adminUser} = await pw.initSetup();
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate to a team
|
||||
await channelsPage.goto();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// # Open Team Settings Modal
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// * Verify Info tab is selected by default
|
||||
await expect(teamSettings.infoTab).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
// # Close modal
|
||||
await teamSettings.close();
|
||||
|
||||
// * Verify modal closes
|
||||
await expect(teamSettings.container).not.toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* MM-TXXXX: Edit team name and save changes
|
||||
* @objective Verify team name can be edited and saved
|
||||
*/
|
||||
test('MM-TXXXX Edit team name and save changes', async ({pw}) => {
|
||||
// # Set up admin user and login
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate to team
|
||||
await channelsPage.goto(team.name);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// # Open Team Settings Modal
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// * Verify current team name is displayed
|
||||
await expect(teamSettings.infoSettings.nameInput).toHaveValue(team.display_name);
|
||||
|
||||
// # Edit team name
|
||||
const newTeamName = `Updated Team ${await pw.random.id()}`;
|
||||
await teamSettings.infoSettings.updateName(newTeamName);
|
||||
|
||||
// # Save changes
|
||||
await teamSettings.save();
|
||||
|
||||
// * Wait for "Settings saved" message
|
||||
await teamSettings.verifySavedMessage();
|
||||
|
||||
// * Verify team name updated via API
|
||||
const updatedTeam = await adminClient.getTeam(team.id);
|
||||
expect(updatedTeam.display_name).toBe(newTeamName);
|
||||
|
||||
// # Close modal
|
||||
await teamSettings.close();
|
||||
|
||||
// * Verify modal closes without warning
|
||||
await expect(teamSettings.container).not.toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* MM-TXXXX: Edit team description and save changes
|
||||
* @objective Verify team description can be edited and saved
|
||||
*/
|
||||
test('MM-TXXXX Edit team description and save changes', async ({pw}) => {
|
||||
// # Set up admin user and login
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate to team
|
||||
await channelsPage.goto(team.name);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// # Open Team Settings Modal
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// # Edit team description
|
||||
const newDescription = `Test description ${await pw.random.id()}`;
|
||||
await teamSettings.infoSettings.updateDescription(newDescription);
|
||||
|
||||
// # Save changes
|
||||
await teamSettings.save();
|
||||
|
||||
// * Wait for "Settings saved" message
|
||||
await teamSettings.verifySavedMessage();
|
||||
|
||||
// * Verify description updated via API
|
||||
const updatedTeam = await adminClient.getTeam(team.id);
|
||||
expect(updatedTeam.description).toBe(newDescription);
|
||||
|
||||
// # Close modal
|
||||
await teamSettings.close();
|
||||
|
||||
// * Verify modal closes
|
||||
await expect(teamSettings.container).not.toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* MM-TXXXX: Warn on close with unsaved changes
|
||||
* @objective Verify unsaved changes warning behavior (warn-once pattern)
|
||||
*/
|
||||
test('MM-TXXXX Warn on close with unsaved changes', async ({pw}) => {
|
||||
// # Set up admin user and login
|
||||
const {adminUser, team} = await pw.initSetup();
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate to team
|
||||
await channelsPage.goto(team.name);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// # Open Team Settings Modal
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// # Edit team name to create unsaved changes
|
||||
const newTeamName = `Modified Team ${await pw.random.id()}`;
|
||||
await teamSettings.infoSettings.updateName(newTeamName);
|
||||
|
||||
// # Try to close modal (first attempt)
|
||||
await teamSettings.close();
|
||||
|
||||
// * Verify "You have unsaved changes" warning appears
|
||||
await teamSettings.verifyUnsavedChanges();
|
||||
|
||||
// * Verify Save button is visible
|
||||
await expect(teamSettings.saveButton).toBeVisible();
|
||||
|
||||
// * Verify modal is still open
|
||||
await expect(teamSettings.container).toBeVisible();
|
||||
|
||||
// # Try to close modal again (second attempt - warn-once behavior)
|
||||
await teamSettings.close();
|
||||
|
||||
// * Verify modal closes on second attempt
|
||||
await expect(teamSettings.container).not.toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* MM-TXXXX: Prevent tab switch with unsaved changes
|
||||
* @objective Verify tab switching blocked with unsaved changes
|
||||
*/
|
||||
test('MM-TXXXX Prevent tab switch with unsaved changes', async ({pw}) => {
|
||||
// # Set up admin user and login
|
||||
const {adminUser, team} = await pw.initSetup();
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate to team
|
||||
await channelsPage.goto(team.name);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// # Open Team Settings Modal
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// * Verify Access tab is visible (admin has INVITE_USER permission)
|
||||
await expect(teamSettings.accessTab).toBeVisible();
|
||||
|
||||
// # Edit team name in Info tab (create unsaved changes)
|
||||
const newTeamName = `Modified Team ${await pw.random.id()}`;
|
||||
await teamSettings.infoSettings.updateName(newTeamName);
|
||||
|
||||
// # Try to switch to Access tab
|
||||
await teamSettings.openAccessTab();
|
||||
|
||||
// * Verify "You have unsaved changes" error appears
|
||||
await teamSettings.verifyUnsavedChanges();
|
||||
|
||||
// * Verify still on Info tab
|
||||
await expect(teamSettings.infoTab).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
// # Click Undo button
|
||||
await teamSettings.undo();
|
||||
|
||||
// * Verify can now switch to Access tab
|
||||
await teamSettings.openAccessTab();
|
||||
await expect(teamSettings.accessTab).toHaveAttribute('aria-selected', 'true');
|
||||
});
|
||||
|
||||
/**
|
||||
* MM-TXXXX: Save changes and close modal without warning
|
||||
* @objective Verify that after saving, modal closes without warning
|
||||
*/
|
||||
test('MM-TXXXX Save changes and close modal without warning', async ({pw}) => {
|
||||
// # Set up admin user and login
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate to team
|
||||
await channelsPage.goto(team.name);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// # Open Team Settings Modal
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// # Edit team name
|
||||
const newTeamName = `Updated Team ${await pw.random.id()}`;
|
||||
await teamSettings.infoSettings.updateName(newTeamName);
|
||||
|
||||
// # Save changes
|
||||
await teamSettings.save();
|
||||
|
||||
// * Wait for "Settings saved" message
|
||||
await teamSettings.verifySavedMessage();
|
||||
|
||||
// * Verify team name updated via API
|
||||
const updatedTeam = await adminClient.getTeam(team.id);
|
||||
expect(updatedTeam.display_name).toBe(newTeamName);
|
||||
|
||||
// # Close modal immediately after save (should work without warning)
|
||||
await teamSettings.close();
|
||||
|
||||
// * Verify modal closes without warning
|
||||
await expect(teamSettings.container).not.toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* MM-TXXXX: Undo changes resets form state
|
||||
* @objective Verify Undo button restores original values
|
||||
*/
|
||||
test('MM-TXXXX Undo changes resets form state', async ({pw}) => {
|
||||
// # Set up admin user and login
|
||||
const {adminUser, team} = await pw.initSetup();
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate to team
|
||||
await channelsPage.goto(team.name);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// # Open Team Settings Modal
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// # Edit team name
|
||||
const newTeamName = `Modified Team ${await pw.random.id()}`;
|
||||
await teamSettings.infoSettings.updateName(newTeamName);
|
||||
|
||||
// * Verify input shows new value
|
||||
await expect(teamSettings.infoSettings.nameInput).toHaveValue(newTeamName);
|
||||
|
||||
// # Click Undo button
|
||||
await teamSettings.undo();
|
||||
|
||||
// * Verify input restored to original value
|
||||
await expect(teamSettings.infoSettings.nameInput).toHaveValue(team.display_name);
|
||||
|
||||
// * Verify can close modal without warning
|
||||
await teamSettings.close();
|
||||
await expect(teamSettings.container).not.toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* MM-TXXXX: Upload and Remove team icon
|
||||
* @objective Verify team icon can be uploaded and removed
|
||||
*/
|
||||
test('MM-TXXXX Upload and Remove team icon', async ({pw}) => {
|
||||
// # Set up admin user and login
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate to team
|
||||
await channelsPage.goto(team.name);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// # Open Team Settings Modal
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
const infoSettings = teamSettings.infoSettings;
|
||||
|
||||
// # Upload team icon using asset file
|
||||
await infoSettings.uploadIcon(TEAM_ICON_ASSET);
|
||||
|
||||
// * Verify upload preview shows
|
||||
await expect(infoSettings.teamIconImage).toBeVisible();
|
||||
|
||||
// * Verify remove button appears
|
||||
await expect(infoSettings.removeImageButton).toBeVisible();
|
||||
|
||||
// # Save changes
|
||||
await teamSettings.save();
|
||||
await teamSettings.verifySavedMessage();
|
||||
|
||||
// * Get team data after upload to verify icon exists via API
|
||||
const teamWithIcon = await adminClient.getTeam(team.id);
|
||||
expect(teamWithIcon.last_team_icon_update).toBeGreaterThan(0);
|
||||
|
||||
// # Close and reopen modal to verify persistence
|
||||
await teamSettings.close();
|
||||
await expect(teamSettings.container).not.toBeVisible();
|
||||
const teamSettings2 = await channelsPage.openTeamSettings();
|
||||
|
||||
// * Verify uploaded icon persists after reopening modal
|
||||
await expect(teamSettings2.infoSettings.teamIconImage).toBeVisible();
|
||||
await expect(teamSettings2.infoSettings.removeImageButton).toBeVisible();
|
||||
|
||||
// # Remove the icon
|
||||
await teamSettings2.infoSettings.removeIcon();
|
||||
|
||||
// * Verify icon was removed - check for default icon initials in modal
|
||||
await expect(teamSettings2.infoSettings.teamIconInitial).toBeVisible();
|
||||
|
||||
// * Verify icon was removed via API
|
||||
const teamAfterRemove = await adminClient.getTeam(team.id);
|
||||
expect(teamAfterRemove.last_team_icon_update || 0).toBe(0);
|
||||
|
||||
// # Close modal
|
||||
await teamSettings2.close();
|
||||
});
|
||||
|
||||
/**
|
||||
* MM-TXXXX: Access tab - add and remove allowed domain
|
||||
* @objective Verify allowed domains can be added and removed
|
||||
*/
|
||||
test('MM-TXXXX Access tab - add and remove allowed domain', async ({pw}) => {
|
||||
// # Set up admin user and login
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate to team
|
||||
await channelsPage.goto(team.name);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// # Open Team Settings Modal
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// # Switch to Access tab
|
||||
const accessSettings = await teamSettings.openAccessTab();
|
||||
|
||||
// * Verify Access tab is active
|
||||
await expect(teamSettings.accessTab).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
// # Enable allowed domains checkbox to show the input
|
||||
await accessSettings.enableAllowedDomains();
|
||||
|
||||
// # Add an allowed domain
|
||||
const testDomain = 'testdomain.com';
|
||||
await accessSettings.addDomain(testDomain);
|
||||
|
||||
// * Verify domain appears in the UI
|
||||
const domainChip = teamSettings.container.locator('#allowedDomains').getByText(testDomain);
|
||||
await expect(domainChip).toBeVisible();
|
||||
|
||||
// # Save changes
|
||||
await teamSettings.save();
|
||||
|
||||
// * Wait for "Settings saved" message
|
||||
await teamSettings.verifySavedMessage();
|
||||
|
||||
// * Verify domain was saved via API
|
||||
const updatedTeam = await adminClient.getTeam(team.id);
|
||||
expect(updatedTeam.allowed_domains).toContain(testDomain);
|
||||
|
||||
// # Remove the added domain
|
||||
await accessSettings.removeDomain(testDomain);
|
||||
|
||||
// # Save changes
|
||||
await teamSettings.save();
|
||||
await teamSettings.verifySavedMessage();
|
||||
|
||||
// * Verify domain was removed via API
|
||||
const finalTeam = await adminClient.getTeam(team.id);
|
||||
expect(finalTeam.allowed_domains).not.toContain(testDomain);
|
||||
|
||||
// # Close modal
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
/**
|
||||
* MM-TXXXX: Access tab - toggle allow open invite
|
||||
* @objective Verify "Users on this server" setting can be toggled on/off
|
||||
*/
|
||||
test('MM-TXXXX Access tab - toggle allow open invite', async ({pw}) => {
|
||||
// # Set up admin user and login
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// Get original allow_open_invite state
|
||||
const originalTeam = await adminClient.getTeam(team.id);
|
||||
const originalAllowOpenInvite = originalTeam.allow_open_invite ?? false;
|
||||
|
||||
// # Navigate to team
|
||||
await channelsPage.goto(team.name);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// # Open Team Settings Modal
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// # Switch to Access tab
|
||||
const accessSettings = await teamSettings.openAccessTab();
|
||||
|
||||
// * Verify Access tab is active
|
||||
await expect(teamSettings.accessTab).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
// # Toggle allow open invite checkbox
|
||||
await accessSettings.toggleOpenInvite();
|
||||
|
||||
// * Verify Save panel appears
|
||||
await expect(teamSettings.saveButton).toBeVisible();
|
||||
|
||||
// # Save changes
|
||||
await teamSettings.save();
|
||||
|
||||
// * Wait for "Settings saved" message
|
||||
await teamSettings.verifySavedMessage();
|
||||
|
||||
// * Verify setting toggled via API
|
||||
const updatedTeam = await adminClient.getTeam(team.id);
|
||||
expect(updatedTeam.allow_open_invite).toBe(!originalAllowOpenInvite);
|
||||
|
||||
// # Toggle back to original state
|
||||
await accessSettings.toggleOpenInvite();
|
||||
|
||||
// # Save changes
|
||||
await teamSettings.save();
|
||||
await teamSettings.verifySavedMessage();
|
||||
|
||||
// * Verify reverted to original state via API
|
||||
const finalTeam = await adminClient.getTeam(team.id);
|
||||
expect(finalTeam.allow_open_invite).toBe(originalAllowOpenInvite);
|
||||
|
||||
// # Close modal
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
/**
|
||||
* MM-TXXXX: Access tab - regenerate invite ID
|
||||
* @objective Verify team invite ID can be regenerated
|
||||
*/
|
||||
test('MM-TXXXX Access tab - regenerate invite ID', async ({pw}) => {
|
||||
// # Set up admin user and login
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// Get original invite ID
|
||||
const originalInviteId = team.invite_id;
|
||||
|
||||
// # Navigate to team
|
||||
await channelsPage.goto(team.name);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// # Open Team Settings Modal
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// # Switch to Access tab
|
||||
const accessSettings = await teamSettings.openAccessTab();
|
||||
|
||||
// * Verify Access tab is active
|
||||
await expect(teamSettings.accessTab).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
// # Click regenerate button
|
||||
await accessSettings.regenerateInviteId();
|
||||
|
||||
// * Verify invite ID changed via API
|
||||
const updatedTeam = await adminClient.getTeam(team.id);
|
||||
expect(updatedTeam.invite_id).not.toBe(originalInviteId);
|
||||
expect(updatedTeam.invite_id).toBeTruthy();
|
||||
|
||||
// # Close modal
|
||||
await teamSettings.close();
|
||||
});
|
||||
});
|
||||
|
|
@ -267,6 +267,9 @@ export class MobileSidebarRightItems extends React.PureComponent<Props> {
|
|||
id='teamSettings'
|
||||
modalId={ModalIdentifiers.TEAM_SETTINGS}
|
||||
dialogType={TeamSettingsModal}
|
||||
dialogProps={{
|
||||
isOpen: true,
|
||||
}}
|
||||
text={formatMessage({id: 'navbar_dropdown.teamSettings', defaultMessage: 'Team Settings'})}
|
||||
icon={
|
||||
<i
|
||||
|
|
|
|||
|
|
@ -203,6 +203,7 @@ function TeamSettingsMenuItem(props: Menu.FirstMenuItemProps) {
|
|||
modalId: ModalIdentifiers.TEAM_SETTINGS,
|
||||
dialogType: TeamSettingsModal,
|
||||
dialogProps: {
|
||||
isOpen: true,
|
||||
focusOriginElement: 'sidebarTeamMenuButton',
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -14,13 +14,10 @@ import TeamAccessTab from './team_access_tab';
|
|||
|
||||
export type OwnProps = {
|
||||
team: Team;
|
||||
hasChanges: boolean;
|
||||
hasChangeTabError: boolean;
|
||||
setHasChanges: (hasChanges: boolean) => void;
|
||||
setHasChangeTabError: (hasChangesError: boolean) => void;
|
||||
setJustSaved: (justSaved: boolean) => void;
|
||||
closeModal: () => void;
|
||||
collapseModal: () => void;
|
||||
areThereUnsavedChanges: boolean;
|
||||
showTabSwitchError: boolean;
|
||||
setAreThereUnsavedChanges: (unsaved: boolean) => void;
|
||||
setShowTabSwitchError: (error: boolean) => void;
|
||||
};
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
|
|
|
|||
|
|
@ -26,14 +26,11 @@ describe('components/TeamSettings', () => {
|
|||
};
|
||||
const defaultProps: ComponentProps<typeof AccessTab> = {
|
||||
team: TestHelper.getTeamMock({id: 'team_id'}),
|
||||
closeModal: jest.fn(),
|
||||
actions: baseActions,
|
||||
hasChanges: true,
|
||||
hasChangeTabError: false,
|
||||
setHasChanges: jest.fn(),
|
||||
setHasChangeTabError: jest.fn(),
|
||||
setJustSaved: jest.fn(),
|
||||
collapseModal: jest.fn(),
|
||||
areThereUnsavedChanges: true,
|
||||
showTabSwitchError: false,
|
||||
setAreThereUnsavedChanges: jest.fn(),
|
||||
setShowTabSwitchError: jest.fn(),
|
||||
};
|
||||
|
||||
test('should not render team invite section if no permissions for team inviting', () => {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import ModalSection from 'components/widgets/modals/components/modal_section';
|
||||
import SaveChangesPanel, {type SaveChangesPanelState} from 'components/widgets/modals/components/save_changes_panel';
|
||||
|
||||
import AllowedDomainsSelect from './allowed_domains_select';
|
||||
|
|
@ -25,11 +23,10 @@ const generateAllowedDomainOptions = (allowedDomains?: string) => {
|
|||
|
||||
type Props = PropsFromRedux & OwnProps;
|
||||
|
||||
const AccessTab = ({closeModal, collapseModal, hasChangeTabError, hasChanges, setHasChangeTabError, setHasChanges, setJustSaved, team, actions}: Props) => {
|
||||
const AccessTab = ({showTabSwitchError, areThereUnsavedChanges, setShowTabSwitchError, setAreThereUnsavedChanges, team, actions}: Props) => {
|
||||
const [allowedDomains, setAllowedDomains] = useState<string[]>(() => generateAllowedDomainOptions(team.allowed_domains));
|
||||
const [allowOpenInvite, setAllowOpenInvite] = useState<boolean>(team.allow_open_invite ?? false);
|
||||
const [saveChangesPanelState, setSaveChangesPanelState] = useState<SaveChangesPanelState>();
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const handleAllowedDomainsSubmit = useCallback(async (): Promise<boolean> => {
|
||||
const {error} = await actions.patchTeam({
|
||||
|
|
@ -59,17 +56,16 @@ const AccessTab = ({closeModal, collapseModal, hasChangeTabError, hasChanges, se
|
|||
}, [actions, allowOpenInvite, team]);
|
||||
|
||||
const updateOpenInvite = useCallback((value: boolean) => {
|
||||
setHasChanges(true);
|
||||
setAreThereUnsavedChanges(true);
|
||||
setSaveChangesPanelState('editing');
|
||||
setAllowOpenInvite(value);
|
||||
}, [setHasChanges]);
|
||||
}, [setAreThereUnsavedChanges]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setSaveChangesPanelState('editing');
|
||||
setHasChanges(false);
|
||||
setHasChangeTabError(false);
|
||||
setJustSaved(false); // Reset flag when panel closes
|
||||
}, [setHasChangeTabError, setHasChanges, setJustSaved]);
|
||||
setAreThereUnsavedChanges(false);
|
||||
setShowTabSwitchError(false);
|
||||
}, [setShowTabSwitchError, setAreThereUnsavedChanges]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
setAllowedDomains(generateAllowedDomainOptions(team.allowed_domains));
|
||||
|
|
@ -77,14 +73,6 @@ const AccessTab = ({closeModal, collapseModal, hasChangeTabError, hasChanges, se
|
|||
handleClose();
|
||||
}, [handleClose, team.allow_open_invite, team.allowed_domains]);
|
||||
|
||||
const collapseModalHandler = useCallback(() => {
|
||||
if (hasChanges) {
|
||||
setHasChangeTabError(true);
|
||||
return;
|
||||
}
|
||||
collapseModal();
|
||||
}, [collapseModal, hasChanges, setHasChangeTabError]);
|
||||
|
||||
const handleSaveChanges = useCallback(async () => {
|
||||
const allowedDomainSuccess = await handleAllowedDomainsSubmit();
|
||||
const openInviteSuccess = await handleOpenInviteSubmit();
|
||||
|
|
@ -93,75 +81,48 @@ const AccessTab = ({closeModal, collapseModal, hasChangeTabError, hasChanges, se
|
|||
return;
|
||||
}
|
||||
setSaveChangesPanelState('saved');
|
||||
setHasChangeTabError(false);
|
||||
setJustSaved(true); // Flag that save just completed
|
||||
}, [handleAllowedDomainsSubmit, handleOpenInviteSubmit, setHasChangeTabError, setJustSaved]);
|
||||
setShowTabSwitchError(false);
|
||||
|
||||
// allows modal to close immediately
|
||||
setAreThereUnsavedChanges(false);
|
||||
}, [handleAllowedDomainsSubmit, handleOpenInviteSubmit, setShowTabSwitchError, setAreThereUnsavedChanges]);
|
||||
|
||||
return (
|
||||
<ModalSection
|
||||
content={
|
||||
<>
|
||||
<div className='modal-header'>
|
||||
<button
|
||||
id='closeButton'
|
||||
type='button'
|
||||
className='close'
|
||||
data-dismiss='modal'
|
||||
onClick={closeModal}
|
||||
>
|
||||
<span aria-hidden='true'>{'×'}</span>
|
||||
</button>
|
||||
<h4 className='modal-title'>
|
||||
<div className='modal-back'>
|
||||
<i
|
||||
className='fa fa-angle-left'
|
||||
aria-label={formatMessage({
|
||||
id: 'generic_icons.collapse',
|
||||
defaultMessage: 'Collapse Icon',
|
||||
})}
|
||||
onClick={collapseModalHandler}
|
||||
/>
|
||||
</div>
|
||||
<span>{formatMessage({id: 'team_settings_modal.title', defaultMessage: 'Team Settings'})}</span>
|
||||
</h4>
|
||||
</div>
|
||||
<div
|
||||
className='modal-access-tab-content user-settings'
|
||||
id='accessSettings'
|
||||
aria-labelledby='accessButton'
|
||||
role='tabpanel'
|
||||
>
|
||||
{!team.group_constrained && (
|
||||
<AllowedDomainsSelect
|
||||
allowedDomains={allowedDomains}
|
||||
setAllowedDomains={setAllowedDomains}
|
||||
setHasChanges={setHasChanges}
|
||||
setSaveChangesPanelState={setSaveChangesPanelState}
|
||||
/>
|
||||
)}
|
||||
<div className='divider-light'/>
|
||||
<OpenInvite
|
||||
isGroupConstrained={team.group_constrained}
|
||||
allowOpenInvite={allowOpenInvite}
|
||||
setAllowOpenInvite={updateOpenInvite}
|
||||
/>
|
||||
<div className='divider-light'/>
|
||||
{!team.group_constrained && (
|
||||
<InviteSectionInput regenerateTeamInviteId={actions.regenerateTeamInviteId}/>
|
||||
)}
|
||||
{hasChanges && (
|
||||
<SaveChangesPanel
|
||||
handleCancel={handleCancel}
|
||||
handleSubmit={handleSaveChanges}
|
||||
handleClose={handleClose}
|
||||
tabChangeError={hasChangeTabError}
|
||||
state={saveChangesPanelState}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className='modal-access-tab-content user-settings'
|
||||
id='accessSettings'
|
||||
aria-labelledby='accessButton'
|
||||
role='tabpanel'
|
||||
>
|
||||
{!team.group_constrained && (
|
||||
<AllowedDomainsSelect
|
||||
allowedDomains={allowedDomains}
|
||||
setAllowedDomains={setAllowedDomains}
|
||||
setHasChanges={setAreThereUnsavedChanges}
|
||||
setSaveChangesPanelState={setSaveChangesPanelState}
|
||||
/>
|
||||
)}
|
||||
<div className='divider-light'/>
|
||||
<OpenInvite
|
||||
isGroupConstrained={team.group_constrained}
|
||||
allowOpenInvite={allowOpenInvite}
|
||||
setAllowOpenInvite={updateOpenInvite}
|
||||
/>
|
||||
<div className='divider-light'/>
|
||||
{!team.group_constrained && (
|
||||
<InviteSectionInput regenerateTeamInviteId={actions.regenerateTeamInviteId}/>
|
||||
)}
|
||||
{(areThereUnsavedChanges || saveChangesPanelState === 'saved') && (
|
||||
<SaveChangesPanel
|
||||
handleCancel={handleCancel}
|
||||
handleSubmit={handleSaveChanges}
|
||||
handleClose={handleClose}
|
||||
tabChangeError={showTabSwitchError}
|
||||
state={saveChangesPanelState}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessTab;
|
||||
|
|
|
|||
|
|
@ -17,13 +17,10 @@ import TeamInfoTab from './team_info_tab';
|
|||
|
||||
export type OwnProps = {
|
||||
team: Team;
|
||||
hasChanges: boolean;
|
||||
hasChangeTabError: boolean;
|
||||
setHasChanges: (hasChanges: boolean) => void;
|
||||
setHasChangeTabError: (hasChangesError: boolean) => void;
|
||||
setJustSaved: (justSaved: boolean) => void;
|
||||
closeModal: () => void;
|
||||
collapseModal: () => void;
|
||||
areThereUnsavedChanges: boolean;
|
||||
showTabSwitchError: boolean;
|
||||
setAreThereUnsavedChanges: (unsaved: boolean) => void;
|
||||
setShowTabSwitchError: (error: boolean) => void;
|
||||
};
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
|
|
|
|||
|
|
@ -26,13 +26,10 @@ describe('components/TeamSettings', () => {
|
|||
team: TestHelper.getTeamMock({id: 'team_id', name: 'team_name', display_name: 'team_display_name', description: 'team_description'}),
|
||||
maxFileSize: 50,
|
||||
actions: baseActions,
|
||||
hasChanges: true,
|
||||
hasChangeTabError: false,
|
||||
setHasChanges: jest.fn(),
|
||||
setHasChangeTabError: jest.fn(),
|
||||
setJustSaved: jest.fn(),
|
||||
closeModal: jest.fn(),
|
||||
collapseModal: jest.fn(),
|
||||
areThereUnsavedChanges: true,
|
||||
showTabSwitchError: false,
|
||||
setAreThereUnsavedChanges: jest.fn(),
|
||||
setShowTabSwitchError: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
|
|||
|
|
@ -3,12 +3,11 @@
|
|||
|
||||
import React, {useCallback, useState} from 'react';
|
||||
import type {ChangeEvent} from 'react';
|
||||
import {defineMessages, useIntl} from 'react-intl';
|
||||
import {defineMessages} from 'react-intl';
|
||||
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
|
||||
import type {BaseSettingItemProps} from 'components/widgets/modals/components/base_setting_item';
|
||||
import ModalSection from 'components/widgets/modals/components/modal_section';
|
||||
import SaveChangesPanel, {type SaveChangesPanelState} from 'components/widgets/modals/components/save_changes_panel';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
|
|
@ -45,9 +44,10 @@ const translations = defineMessages({
|
|||
defaultMessage: 'An error occurred while selecting the image.',
|
||||
},
|
||||
});
|
||||
|
||||
type Props = PropsFromRedux & OwnProps;
|
||||
|
||||
const InfoTab = ({team, hasChanges, maxFileSize, closeModal, collapseModal, hasChangeTabError, setHasChangeTabError, setHasChanges, setJustSaved, actions}: Props) => {
|
||||
const InfoTab = ({team, areThereUnsavedChanges, maxFileSize, showTabSwitchError, setShowTabSwitchError, setAreThereUnsavedChanges, actions}: Props) => {
|
||||
const [name, setName] = useState<Team['display_name']>(team.display_name);
|
||||
const [description, setDescription] = useState<Team['description']>(team.description);
|
||||
const [teamIconFile, setTeamIconFile] = useState<File | undefined>();
|
||||
|
|
@ -55,7 +55,6 @@ const InfoTab = ({team, hasChanges, maxFileSize, closeModal, collapseModal, hasC
|
|||
const [imageClientError, setImageClientError] = useState<BaseSettingItemProps['error'] | undefined>();
|
||||
const [nameClientError, setNameClientError] = useState<BaseSettingItemProps['error'] | undefined>();
|
||||
const [saveChangesPanelState, setSaveChangesPanelState] = useState<SaveChangesPanelState>();
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const handleNameDescriptionSubmit = useCallback(async (): Promise<boolean> => {
|
||||
if (name.trim() === team.display_name && description === team.description) {
|
||||
|
|
@ -99,16 +98,16 @@ const InfoTab = ({team, hasChanges, maxFileSize, closeModal, collapseModal, hasC
|
|||
return;
|
||||
}
|
||||
setSaveChangesPanelState('saved');
|
||||
setHasChangeTabError(false);
|
||||
setJustSaved(true); // Flag that save just completed
|
||||
}, [handleNameDescriptionSubmit, handleTeamIconSubmit, setHasChangeTabError, setJustSaved]);
|
||||
setShowTabSwitchError(false);
|
||||
|
||||
setAreThereUnsavedChanges(false);
|
||||
}, [handleNameDescriptionSubmit, handleTeamIconSubmit, setShowTabSwitchError, setAreThereUnsavedChanges]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setSaveChangesPanelState('editing');
|
||||
setHasChanges(false);
|
||||
setHasChangeTabError(false);
|
||||
setJustSaved(false); // Reset flag when panel closes
|
||||
}, [setHasChangeTabError, setHasChanges, setJustSaved]);
|
||||
setAreThereUnsavedChanges(false);
|
||||
setShowTabSwitchError(false);
|
||||
}, [setShowTabSwitchError, setAreThereUnsavedChanges]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
setName(team.display_name ?? team.name);
|
||||
|
|
@ -129,10 +128,10 @@ const InfoTab = ({team, hasChanges, maxFileSize, closeModal, collapseModal, hasC
|
|||
setLoading(false);
|
||||
if (error) {
|
||||
setSaveChangesPanelState('error');
|
||||
setHasChanges(true);
|
||||
setHasChangeTabError(true);
|
||||
setAreThereUnsavedChanges(true);
|
||||
setShowTabSwitchError(true);
|
||||
}
|
||||
}, [actions, handleClose, setHasChangeTabError, setHasChanges, team.id]);
|
||||
}, [actions, handleClose, setShowTabSwitchError, setAreThereUnsavedChanges, team.id]);
|
||||
|
||||
const updateTeamIcon = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (e && e.target && e.target.files && e.target.files[0]) {
|
||||
|
|
@ -146,99 +145,64 @@ const InfoTab = ({team, hasChanges, maxFileSize, closeModal, collapseModal, hasC
|
|||
setTeamIconFile(file);
|
||||
setImageClientError(undefined);
|
||||
setSaveChangesPanelState('editing');
|
||||
setHasChanges(true);
|
||||
setAreThereUnsavedChanges(true);
|
||||
}
|
||||
} else {
|
||||
setTeamIconFile(undefined);
|
||||
setImageClientError(translations.TeamIconError);
|
||||
}
|
||||
}, [maxFileSize, setHasChanges]);
|
||||
}, [maxFileSize, setAreThereUnsavedChanges]);
|
||||
|
||||
const handleNameChanges = useCallback((name: string) => {
|
||||
setHasChanges(true);
|
||||
setAreThereUnsavedChanges(true);
|
||||
setSaveChangesPanelState('editing');
|
||||
setName(name);
|
||||
}, [setHasChanges]);
|
||||
}, [setAreThereUnsavedChanges]);
|
||||
|
||||
const handleDescriptionChanges = useCallback((description: string) => {
|
||||
setHasChanges(true);
|
||||
setAreThereUnsavedChanges(true);
|
||||
setSaveChangesPanelState('editing');
|
||||
setDescription(description);
|
||||
}, [setHasChanges]);
|
||||
}, [setAreThereUnsavedChanges]);
|
||||
|
||||
const handleCollapseModal = useCallback(() => {
|
||||
if (hasChanges) {
|
||||
setHasChangeTabError(true);
|
||||
return;
|
||||
}
|
||||
collapseModal();
|
||||
}, [collapseModal, hasChanges, setHasChangeTabError]);
|
||||
|
||||
const modalSectionContent = (
|
||||
<>
|
||||
<div className='modal-header'>
|
||||
<button
|
||||
id='closeButton'
|
||||
type='button'
|
||||
className='close'
|
||||
data-dismiss='modal'
|
||||
onClick={closeModal}
|
||||
>
|
||||
<span aria-hidden='true'>{'×'}</span>
|
||||
</button>
|
||||
<h4 className='modal-title'>
|
||||
<div className='modal-back'>
|
||||
<i
|
||||
className='fa fa-angle-left'
|
||||
aria-label={formatMessage({
|
||||
id: 'generic_icons.collapse',
|
||||
defaultMessage: 'Collapse Icon',
|
||||
})}
|
||||
onClick={handleCollapseModal}
|
||||
/>
|
||||
</div>
|
||||
<span>{formatMessage({id: 'team_settings_modal.title', defaultMessage: 'Team Settings'})}</span>
|
||||
</h4>
|
||||
</div>
|
||||
<div
|
||||
className='modal-info-tab-content user-settings'
|
||||
id='infoSettings'
|
||||
aria-labelledby='infoButton'
|
||||
role='tabpanel'
|
||||
>
|
||||
<div className='name-description-container' >
|
||||
<TeamNameSection
|
||||
name={name}
|
||||
clientError={nameClientError}
|
||||
handleNameChanges={handleNameChanges}
|
||||
/>
|
||||
<TeamDescriptionSection
|
||||
description={description}
|
||||
handleDescriptionChanges={handleDescriptionChanges}
|
||||
/>
|
||||
</div>
|
||||
<TeamPictureSection
|
||||
team={team}
|
||||
file={teamIconFile}
|
||||
disabled={loading}
|
||||
onFileChange={updateTeamIcon}
|
||||
onRemove={handleTeamIconRemove}
|
||||
teamName={team.display_name ?? team.name}
|
||||
clientError={imageClientError}
|
||||
return (
|
||||
<div
|
||||
className='modal-info-tab-content user-settings'
|
||||
id='infoSettings'
|
||||
aria-labelledby='infoButton'
|
||||
role='tabpanel'
|
||||
>
|
||||
<div className='name-description-container'>
|
||||
<TeamNameSection
|
||||
name={name}
|
||||
clientError={nameClientError}
|
||||
handleNameChanges={handleNameChanges}
|
||||
/>
|
||||
<TeamDescriptionSection
|
||||
description={description}
|
||||
handleDescriptionChanges={handleDescriptionChanges}
|
||||
/>
|
||||
{hasChanges && (
|
||||
<SaveChangesPanel
|
||||
handleCancel={handleCancel}
|
||||
handleSubmit={handleSaveChanges}
|
||||
handleClose={handleClose}
|
||||
tabChangeError={hasChangeTabError}
|
||||
state={saveChangesPanelState}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
<TeamPictureSection
|
||||
team={team}
|
||||
file={teamIconFile}
|
||||
disabled={loading}
|
||||
onFileChange={updateTeamIcon}
|
||||
onRemove={handleTeamIconRemove}
|
||||
teamName={team.display_name ?? team.name}
|
||||
clientError={imageClientError}
|
||||
/>
|
||||
{(areThereUnsavedChanges || saveChangesPanelState === 'saved') && (
|
||||
<SaveChangesPanel
|
||||
handleCancel={handleCancel}
|
||||
handleSubmit={handleSaveChanges}
|
||||
handleClose={handleClose}
|
||||
tabChangeError={showTabSwitchError}
|
||||
state={saveChangesPanelState}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return <ModalSection content={modalSectionContent}/>;
|
||||
};
|
||||
|
||||
export default InfoTab;
|
||||
|
|
|
|||
|
|
@ -10,26 +10,20 @@ import InfoTab from './team_info_tab';
|
|||
|
||||
type Props = {
|
||||
activeTab: string;
|
||||
hasChanges: boolean;
|
||||
hasChangeTabError: boolean;
|
||||
setHasChanges: (hasChanges: boolean) => void;
|
||||
setHasChangeTabError: (hasChangesError: boolean) => void;
|
||||
setJustSaved: (justSaved: boolean) => void;
|
||||
closeModal: () => void;
|
||||
collapseModal: () => void;
|
||||
areThereUnsavedChanges: boolean;
|
||||
showTabSwitchError: boolean;
|
||||
setAreThereUnsavedChanges: (unsaved: boolean) => void;
|
||||
setShowTabSwitchError: (error: boolean) => void;
|
||||
team?: Team;
|
||||
};
|
||||
|
||||
const TeamSettings = ({
|
||||
activeTab = '',
|
||||
closeModal,
|
||||
collapseModal,
|
||||
team,
|
||||
hasChanges,
|
||||
hasChangeTabError,
|
||||
setHasChanges,
|
||||
setHasChangeTabError,
|
||||
setJustSaved,
|
||||
areThereUnsavedChanges,
|
||||
showTabSwitchError,
|
||||
setAreThereUnsavedChanges,
|
||||
setShowTabSwitchError,
|
||||
}: Props) => {
|
||||
if (!team) {
|
||||
return null;
|
||||
|
|
@ -41,13 +35,10 @@ const TeamSettings = ({
|
|||
result = (
|
||||
<InfoTab
|
||||
team={team}
|
||||
hasChanges={hasChanges}
|
||||
setHasChanges={setHasChanges}
|
||||
hasChangeTabError={hasChangeTabError}
|
||||
setHasChangeTabError={setHasChangeTabError}
|
||||
setJustSaved={setJustSaved}
|
||||
closeModal={closeModal}
|
||||
collapseModal={collapseModal}
|
||||
areThereUnsavedChanges={areThereUnsavedChanges}
|
||||
setAreThereUnsavedChanges={setAreThereUnsavedChanges}
|
||||
showTabSwitchError={showTabSwitchError}
|
||||
setShowTabSwitchError={setShowTabSwitchError}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
|
@ -55,13 +46,10 @@ const TeamSettings = ({
|
|||
result = (
|
||||
<AccessTab
|
||||
team={team}
|
||||
hasChanges={hasChanges}
|
||||
setHasChanges={setHasChanges}
|
||||
hasChangeTabError={hasChangeTabError}
|
||||
setHasChangeTabError={setHasChangeTabError}
|
||||
setJustSaved={setJustSaved}
|
||||
closeModal={closeModal}
|
||||
collapseModal={collapseModal}
|
||||
areThereUnsavedChanges={areThereUnsavedChanges}
|
||||
setAreThereUnsavedChanges={setAreThereUnsavedChanges}
|
||||
showTabSwitchError={showTabSwitchError}
|
||||
setShowTabSwitchError={setShowTabSwitchError}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -1,28 +1,4 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
import {haveITeamPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import {isModalOpen} from 'selectors/views/modals';
|
||||
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import TeamSettingsModal from './team_settings_modal';
|
||||
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const teamId = getCurrentTeamId(state);
|
||||
const canInviteUsers = haveITeamPermission(state, teamId, Permissions.INVITE_USER);
|
||||
const modalId = ModalIdentifiers.TEAM_SETTINGS;
|
||||
return {
|
||||
show: isModalOpen(state, modalId),
|
||||
canInviteUsers,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(TeamSettingsModal);
|
||||
export {default} from './team_settings_modal';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
.TeamSettingsModal {
|
||||
width: 920px !important;
|
||||
|
||||
// Override GenericModal wrapper styles
|
||||
.GenericModal__wrapper {
|
||||
display: flex;
|
||||
overflow: visible;
|
||||
max-width: 1200px;
|
||||
max-height: 90vh;
|
||||
flex-direction: column;
|
||||
border: var(--border-default);
|
||||
border-radius: var(--radius-l);
|
||||
box-shadow: var(--elevation-6);
|
||||
|
||||
.modal-body {
|
||||
display: flex;
|
||||
width: auto;
|
||||
min-height: 150px;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
gap: 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__bodyWrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 920px;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
// Settings table layout (inherits most from global styles)
|
||||
.settings-table {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.settings-content {
|
||||
display: flex;
|
||||
overflow: visible !important;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
padding: 0 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body .form-control {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
// SaveChangesPanel width override
|
||||
.SaveChangesPanel {
|
||||
width: calc(70%);
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
width: calc(75%);
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive behavior
|
||||
@media screen and (max-width: 768px) {
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
|
||||
.modal-content {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
max-height: unset;
|
||||
flex-direction: column;
|
||||
border-radius: unset;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height: 900px) and (min-width: 768px) {
|
||||
.modal-content {
|
||||
max-height: 90vh;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height: 600px) {
|
||||
.modal-content,
|
||||
.GenericModal__wrapper {
|
||||
max-height: 85vh !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,14 +3,63 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
|
||||
import TeamSettingsModal from 'components/team_settings_modal/team_settings_modal';
|
||||
|
||||
import {renderWithContext, screen, userEvent, waitFor} from 'tests/react_testing_utils';
|
||||
|
||||
// Mock Redux actions
|
||||
jest.mock('mattermost-redux/actions/teams', () => ({
|
||||
patchTeam: jest.fn(() => async () => ({data: {}, error: null})),
|
||||
getTeam: jest.fn(() => async () => ({data: {}, error: null})),
|
||||
removeTeamIcon: jest.fn(() => async () => ({data: {}, error: null})),
|
||||
setTeamIcon: jest.fn(() => async () => ({data: {}, error: null})),
|
||||
}));
|
||||
|
||||
describe('components/team_settings_modal', () => {
|
||||
const baseProps = {
|
||||
isOpen: true,
|
||||
onExited: jest.fn(),
|
||||
canInviteUsers: true,
|
||||
};
|
||||
|
||||
const baseState = {
|
||||
entities: {
|
||||
teams: {
|
||||
currentTeamId: 'team-id',
|
||||
teams: {
|
||||
'team-id': {
|
||||
id: 'team-id',
|
||||
display_name: 'Team Name',
|
||||
description: 'Team Description',
|
||||
name: 'team-name',
|
||||
},
|
||||
},
|
||||
myMembers: {
|
||||
'team-id': {
|
||||
team_id: 'team-id',
|
||||
user_id: 'user-id',
|
||||
roles: 'team_user',
|
||||
},
|
||||
},
|
||||
},
|
||||
roles: {
|
||||
roles: {
|
||||
team_user: {
|
||||
permissions: [Permissions.INVITE_USER],
|
||||
},
|
||||
},
|
||||
},
|
||||
users: {
|
||||
currentUserId: 'user-id',
|
||||
profiles: {
|
||||
'user-id': {
|
||||
id: 'user-id',
|
||||
roles: 'team_user',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
test('should hide the modal when the close button is clicked', async () => {
|
||||
|
|
@ -18,19 +67,24 @@ describe('components/team_settings_modal', () => {
|
|||
<TeamSettingsModal
|
||||
{...baseProps}
|
||||
/>,
|
||||
baseState,
|
||||
);
|
||||
const modal = screen.getByRole('dialog', {name: 'Close Team Settings'});
|
||||
expect(modal.className).toBe('fade in modal');
|
||||
await userEvent.click(screen.getByText('Close'));
|
||||
expect(modal.className).toBe('fade modal');
|
||||
const modal = screen.getByRole('dialog', {name: 'Team Settings'});
|
||||
expect(modal).toBeInTheDocument();
|
||||
const closeButton = screen.getByLabelText('Close');
|
||||
await userEvent.click(closeButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(baseProps.onExited).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
test('should display access tab when can invite users', async () => {
|
||||
const props = {...baseProps, canInviteUsers: true};
|
||||
renderWithContext(
|
||||
<TeamSettingsModal
|
||||
{...props}
|
||||
{...baseProps}
|
||||
/>,
|
||||
baseState,
|
||||
);
|
||||
const infoButton = screen.getByRole('tab', {name: 'info'});
|
||||
expect(infoButton).toBeDefined();
|
||||
|
|
@ -39,11 +93,25 @@ describe('components/team_settings_modal', () => {
|
|||
});
|
||||
|
||||
test('should not display access tab when can not invite users', async () => {
|
||||
const props = {...baseProps, canInviteUsers: false};
|
||||
const stateWithoutPermission = {
|
||||
...baseState,
|
||||
entities: {
|
||||
...baseState.entities,
|
||||
roles: {
|
||||
roles: {
|
||||
team_user: {
|
||||
permissions: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderWithContext(
|
||||
<TeamSettingsModal
|
||||
{...props}
|
||||
{...baseProps}
|
||||
/>,
|
||||
stateWithoutPermission,
|
||||
);
|
||||
const tabs = screen.getAllByRole('tab');
|
||||
expect(tabs.length).toEqual(1);
|
||||
|
|
@ -56,23 +124,10 @@ describe('components/team_settings_modal', () => {
|
|||
<TeamSettingsModal
|
||||
{...baseProps}
|
||||
/>,
|
||||
{
|
||||
entities: {
|
||||
teams: {
|
||||
currentTeamId: 'team-id',
|
||||
teams: {
|
||||
'team-id': {
|
||||
id: 'team-id',
|
||||
display_name: 'Team Name',
|
||||
description: 'Team Description',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
baseState,
|
||||
);
|
||||
|
||||
const modal = screen.getByRole('dialog', {name: 'Close Team Settings'});
|
||||
const modal = screen.getByRole('dialog', {name: 'Team Settings'});
|
||||
expect(modal).toBeInTheDocument();
|
||||
|
||||
// Create unsaved changes by modifying team name
|
||||
|
|
@ -100,20 +155,7 @@ describe('components/team_settings_modal', () => {
|
|||
<TeamSettingsModal
|
||||
{...baseProps}
|
||||
/>,
|
||||
{
|
||||
entities: {
|
||||
teams: {
|
||||
currentTeamId: 'team-id',
|
||||
teams: {
|
||||
'team-id': {
|
||||
id: 'team-id',
|
||||
display_name: 'Team Name',
|
||||
description: 'Team Description',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
baseState,
|
||||
);
|
||||
|
||||
// Create unsaved changes
|
||||
|
|
@ -143,23 +185,10 @@ describe('components/team_settings_modal', () => {
|
|||
<TeamSettingsModal
|
||||
{...baseProps}
|
||||
/>,
|
||||
{
|
||||
entities: {
|
||||
teams: {
|
||||
currentTeamId: 'team-id',
|
||||
teams: {
|
||||
'team-id': {
|
||||
id: 'team-id',
|
||||
display_name: 'Team Name',
|
||||
description: 'Team Description',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
baseState,
|
||||
);
|
||||
|
||||
const modal = screen.getByRole('dialog', {name: 'Close Team Settings'});
|
||||
const modal = screen.getByRole('dialog', {name: 'Team Settings'});
|
||||
expect(modal).toBeInTheDocument();
|
||||
|
||||
// Close modal with no unsaved changes
|
||||
|
|
@ -177,20 +206,7 @@ describe('components/team_settings_modal', () => {
|
|||
<TeamSettingsModal
|
||||
{...baseProps}
|
||||
/>,
|
||||
{
|
||||
entities: {
|
||||
teams: {
|
||||
currentTeamId: 'team-id',
|
||||
teams: {
|
||||
'team-id': {
|
||||
id: 'team-id',
|
||||
display_name: 'Team Name',
|
||||
description: 'Team Description',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
baseState,
|
||||
);
|
||||
|
||||
// Create unsaved changes
|
||||
|
|
@ -198,24 +214,22 @@ describe('components/team_settings_modal', () => {
|
|||
await userEvent.clear(nameInput);
|
||||
await userEvent.type(nameInput, 'Modified Team Name');
|
||||
|
||||
const closeButton = screen.getByLabelText('Close');
|
||||
|
||||
// Trigger warning by attempting to close
|
||||
await userEvent.click(closeButton);
|
||||
|
||||
expect(screen.getByText('You have unsaved changes')).toBeInTheDocument();
|
||||
|
||||
// Save changes to reset warning state
|
||||
// Save changes immediately (without triggering warning first)
|
||||
const saveButton = screen.getByText('Save');
|
||||
await userEvent.click(saveButton);
|
||||
|
||||
// Close modal after saving
|
||||
// Wait for save to complete and "Settings saved" message
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Settings saved')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// After saving, close modal - should work immediately (single click)
|
||||
const closeButton = screen.getByLabelText('Close');
|
||||
await userEvent.click(closeButton);
|
||||
|
||||
// Verify modal closes successfully
|
||||
// Verify modal closes successfully without warning
|
||||
await waitFor(() => {
|
||||
expect(baseProps.onExited).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,108 +1,138 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useState, useRef, useCallback} from 'react';
|
||||
import {Modal, type ModalBody} from 'react-bootstrap';
|
||||
import ReactDOM from 'react-dom';
|
||||
import React, {useState, useRef, useCallback, useEffect} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {GenericModal} from '@mattermost/components';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
import {haveITeamPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
|
||||
import TeamSettings from 'components/team_settings';
|
||||
|
||||
import {focusElement} from 'utils/a11y_utils';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
import './team_settings_modal.scss';
|
||||
|
||||
const SettingsSidebar = React.lazy(() => import('components/settings_sidebar'));
|
||||
|
||||
const SHOW_PANEL_ERROR_STATE_TAB_SWITCH_TIMEOUT = 3000;
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onExited: () => void;
|
||||
canInviteUsers: boolean;
|
||||
focusOriginElement?: string;
|
||||
}
|
||||
|
||||
const TeamSettingsModal = ({onExited, canInviteUsers, focusOriginElement}: Props) => {
|
||||
const TeamSettingsModal = ({isOpen, onExited, focusOriginElement}: Props) => {
|
||||
const [activeTab, setActiveTab] = useState('info');
|
||||
const [show, setShow] = useState<boolean>(true);
|
||||
const [hasChanges, setHasChanges] = useState<boolean>(false);
|
||||
const [hasChangeTabError, setHasChangeTabError] = useState<boolean>(false);
|
||||
const [hasBeenWarned, setHasBeenWarned] = useState<boolean>(false);
|
||||
const [justSaved, setJustSaved] = useState<boolean>(false);
|
||||
const modalBodyRef = useRef<ModalBody>(null);
|
||||
const [show, setShow] = useState(isOpen);
|
||||
const [areThereUnsavedChanges, setAreThereUnsavedChanges] = useState(false);
|
||||
const [showTabSwitchError, setShowTabSwitchError] = useState(false);
|
||||
const [hasBeenWarned, setHasBeenWarned] = useState(false);
|
||||
const modalBodyRef = useRef<HTMLDivElement>(null);
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const teamId = useSelector(getCurrentTeamId);
|
||||
const canInviteUsers = useSelector((state: GlobalState) =>
|
||||
haveITeamPermission(state, teamId, Permissions.INVITE_USER),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setShow(isOpen);
|
||||
}, [isOpen]);
|
||||
|
||||
const updateTab = useCallback((tab: string) => {
|
||||
if (hasChanges) {
|
||||
setHasChangeTabError(true);
|
||||
if (areThereUnsavedChanges) {
|
||||
setShowTabSwitchError(true);
|
||||
setTimeout(() => {
|
||||
setShowTabSwitchError(false);
|
||||
}, SHOW_PANEL_ERROR_STATE_TAB_SWITCH_TIMEOUT);
|
||||
return;
|
||||
}
|
||||
setActiveTab(tab);
|
||||
setHasChanges(false);
|
||||
setHasChangeTabError(false);
|
||||
setHasBeenWarned(false);
|
||||
}, [hasChanges]);
|
||||
|
||||
if (modalBodyRef.current) {
|
||||
modalBodyRef.current.scrollTop = 0;
|
||||
}
|
||||
}, [areThereUnsavedChanges]);
|
||||
|
||||
const handleHide = useCallback(() => {
|
||||
// Prevent modal closing if there are unsaved changes (warn once, then allow)
|
||||
// Don't warn if showing "Settings saved"
|
||||
if (hasChanges && !hasBeenWarned && !justSaved) {
|
||||
if (areThereUnsavedChanges && !hasBeenWarned) {
|
||||
setHasBeenWarned(true);
|
||||
setHasChangeTabError(true);
|
||||
setShowTabSwitchError(true);
|
||||
setTimeout(() => {
|
||||
setHasChangeTabError(false);
|
||||
setShowTabSwitchError(false);
|
||||
}, SHOW_PANEL_ERROR_STATE_TAB_SWITCH_TIMEOUT);
|
||||
} else {
|
||||
setShow(false);
|
||||
handleHideConfirm();
|
||||
}
|
||||
}, [hasChanges, hasBeenWarned, justSaved]);
|
||||
}, [areThereUnsavedChanges, hasBeenWarned]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
const handleHideConfirm = useCallback(() => {
|
||||
setShow(false);
|
||||
}, []);
|
||||
|
||||
const handleExited = useCallback(() => {
|
||||
// Reset all state
|
||||
setActiveTab('info');
|
||||
setAreThereUnsavedChanges(false);
|
||||
setShowTabSwitchError(false);
|
||||
setHasBeenWarned(false);
|
||||
|
||||
// Restore focus
|
||||
if (focusOriginElement) {
|
||||
focusElement(focusOriginElement, true);
|
||||
}
|
||||
setActiveTab('info');
|
||||
setHasChanges(false);
|
||||
setHasChangeTabError(false);
|
||||
setHasBeenWarned(false);
|
||||
setJustSaved(false);
|
||||
|
||||
// Notify parent
|
||||
onExited();
|
||||
}, [onExited, focusOriginElement]);
|
||||
|
||||
const handleCollapse = useCallback(() => {
|
||||
const el = ReactDOM.findDOMNode(modalBodyRef.current) as HTMLDivElement;
|
||||
el?.closest('.modal-dialog')!.classList.remove('display--content');
|
||||
setActiveTab('');
|
||||
}, []);
|
||||
|
||||
const tabs = [
|
||||
{name: 'info', uiName: formatMessage({id: 'team_settings_modal.infoTab', defaultMessage: 'Info'}), icon: 'icon icon-information-outline', iconTitle: formatMessage({id: 'generic_icons.info', defaultMessage: 'Info Icon'})},
|
||||
{
|
||||
name: 'info',
|
||||
uiName: formatMessage({id: 'team_settings_modal.infoTab', defaultMessage: 'Info'}),
|
||||
icon: 'icon icon-information-outline',
|
||||
iconTitle: formatMessage({id: 'generic_icons.info', defaultMessage: 'Info Icon'}),
|
||||
},
|
||||
{
|
||||
name: 'access',
|
||||
uiName: formatMessage({id: 'team_settings_modal.accessTab', defaultMessage: 'Access'}),
|
||||
icon: 'icon icon-account-multiple-outline',
|
||||
iconTitle: formatMessage({id: 'generic_icons.member', defaultMessage: 'Member Icon'}),
|
||||
display: canInviteUsers,
|
||||
},
|
||||
];
|
||||
if (canInviteUsers) {
|
||||
tabs.push({name: 'access', uiName: formatMessage({id: 'team_settings_modal.accessTab', defaultMessage: 'Access'}), icon: 'icon icon-account-multiple-outline', iconTitle: formatMessage({id: 'generic_icons.member', defaultMessage: 'Member Icon'})});
|
||||
}
|
||||
|
||||
const modalTitle = formatMessage({id: 'team_settings_modal.title', defaultMessage: 'Team Settings'});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
dialogClassName='a11y__modal settings-modal'
|
||||
<GenericModal
|
||||
id='teamSettingsModal'
|
||||
ariaLabel={modalTitle}
|
||||
className='TeamSettingsModal settings-modal'
|
||||
show={show}
|
||||
onHide={handleHide}
|
||||
onExited={handleClose}
|
||||
role='none'
|
||||
aria-labelledby='teamSettingsModalLabel'
|
||||
id='teamSettingsModal'
|
||||
preventClose={areThereUnsavedChanges && !hasBeenWarned}
|
||||
onExited={handleExited}
|
||||
compassDesign={true}
|
||||
modalHeaderText={modalTitle}
|
||||
bodyPadding={false}
|
||||
modalLocation={'top'}
|
||||
enforceFocus={false}
|
||||
>
|
||||
<Modal.Header
|
||||
id='teamSettingsModalLabel'
|
||||
closeButton={true}
|
||||
>
|
||||
<Modal.Title
|
||||
componentClass='h2'
|
||||
className='modal-header__title'
|
||||
<div className='TeamSettingsModal__bodyWrapper'>
|
||||
<div
|
||||
ref={modalBodyRef}
|
||||
className='settings-table'
|
||||
>
|
||||
{formatMessage({id: 'team_settings_modal.title', defaultMessage: 'Team Settings'})}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body ref={modalBodyRef}>
|
||||
<div className='settings-table'>
|
||||
<div className='settings-links'>
|
||||
<React.Suspense fallback={null}>
|
||||
<SettingsSidebar
|
||||
|
|
@ -115,18 +145,15 @@ const TeamSettingsModal = ({onExited, canInviteUsers, focusOriginElement}: Props
|
|||
<div className='settings-content minimize-settings'>
|
||||
<TeamSettings
|
||||
activeTab={activeTab}
|
||||
hasChanges={hasChanges}
|
||||
setHasChanges={setHasChanges}
|
||||
hasChangeTabError={hasChangeTabError}
|
||||
setHasChangeTabError={setHasChangeTabError}
|
||||
setJustSaved={setJustSaved}
|
||||
closeModal={handleHide}
|
||||
collapseModal={handleCollapse}
|
||||
areThereUnsavedChanges={areThereUnsavedChanges}
|
||||
setAreThereUnsavedChanges={setAreThereUnsavedChanges}
|
||||
showTabSwitchError={showTabSwitchError}
|
||||
setShowTabSwitchError={setShowTabSwitchError}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</div>
|
||||
</GenericModal>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue