mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
MM-41586: Team setting modal UI update (#25729)
* add new sections * remove section related code * add some todos * convert team setting to functional component * remove unused props from main_menu * remove unused collapseModal * create 2 sections files * clean info section * cleanup access tab * further clean team info setting component * fix input fields * fix description input field * reorganize files * add team icon when there is no team image * improve layout * fix autofocus * delete empty file * add note related to learna bout teams link * add edit icon * add upload functionality * finish image upload * implement logic for handle save * add remove icon button * fix remove button color * fix styling on image remove and upload * fix image remove feature * show remove image button dynamically * fix height * update haveImageChanges on handleTeamIconRemove cl * fix spacing inside input * fix cursor point * access tab basis * add some todo * add baseline for client error * handle desc and image client errors * move folders * rename section to tab * move the name section to new file * dedicated description component * dedicated image section * convert to functional component * remove unnecessary fetchTeam * remove havechanges state * remove not needed folder * rename from section to tab * convert access tab to FC * fix invite section input * finalize team invite code section * add checkbox * add select_text_input * finish allowed domains * fix save changes panel style * convert open_invite * add logic for show save changes panel globally * handle server errors * combine client errors * fix save changes issue * clean colors used in css * fix style * fix type issues * fix another type * fix allowed domains * fix type error * add save changes panel to access tab * add success state to save changes panel * remove unused prop * cleanup css * fix save changes modal position * fix title font size * remove not used prop * fix mobile view width * fix mobile view * add group constraint text * handle invite code error * update snapshots * fix input height * fix tests * write tests for open_invite * write tests for team_info_tab * write tests for team_access_tab * Refactor setTeamIcon test in teams.test.ts * Refactor team access and team info tabs for save changes panel * Add useEffect hook to set inviteId in AccessTab component * fix lint * fix lint * fix i18 * remove old todo * fix text * fix css * fix css * fix padding * fix mobile view * update snapshot * performance improvements * fix type * improve translation passing to components * fix lint * rename saving to editing * fix empty allowed domains * complete renaming of saving * seperate AllowedDomainsSelect * seperate InviteSectionInput * fix i18n * capitalize translation id * final fix for i18n * remove empty file * fix lint and test * fix rgb values * remove action related types from index file * add last_team_icon_update to Team type * fix unnecessary null check operator * fix more types * add new features for text selector * update text for select text input * fix style issues on save changes * fix lint check * add animation for save changes panel * remove unused type * fix test * fix theming issues * fix MM-T385 * fix MM-T388 * fix MM-T387 and MM-T2341 * fix MM-T391 * Fix MM-T2318, MM-T2317, MM-T2312, MM-T2322, MM-T2335 * fix top padding --------- Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
parent
c064c3a979
commit
bff19228e1
@ -59,13 +59,16 @@ describe('Team Settings', () => {
|
||||
|
||||
// * Check that the 'Team Settings' modal was opened
|
||||
cy.get('#teamSettingsModal').should('exist').within(() => {
|
||||
cy.get('#open_inviteDesc').should('have.text', 'No');
|
||||
// # Go to Access section
|
||||
cy.get('#accessButton').click();
|
||||
|
||||
// # Click on the 'Allow only users with a specific email domain to join this team' edit button
|
||||
cy.get('#allowed_domainsEdit').should('be.visible').click();
|
||||
cy.get('.access-allowed-domains-section').should('exist').within(() => {
|
||||
// # Click on the 'Allow only users with a specific email domain to join this team' checkbox
|
||||
cy.get('.mm-modal-generic-section-item__input-checkbox').should('not.be.checked').click();
|
||||
});
|
||||
|
||||
// * Verify that the '#allowedDomains' input field is empty
|
||||
cy.get('#allowedDomains').should('be.empty');
|
||||
cy.get('#allowedDomains').should('have.text', 'corp.mattermost.com, mattermost.com');
|
||||
|
||||
// # Close the modal
|
||||
cy.get('#teamSettingsModalLabel').find('button').should('be.visible').click();
|
||||
|
@ -38,12 +38,16 @@ describe('Team Settings', () => {
|
||||
|
||||
// * Check that the 'Team Settings' modal was opened
|
||||
cy.get('#teamSettingsModal').should('exist').within(() => {
|
||||
// # Click on the 'Allow only users with a specific email domain to join this team' edit button
|
||||
cy.get('#allowed_domainsEdit').should('be.visible').click();
|
||||
// # Go to Access section
|
||||
cy.get('#accessButton').click();
|
||||
|
||||
cy.get('.access-allowed-domains-section').should('exist').within(() => {
|
||||
// # Click on the 'Allow only users with a specific email domain to join this team' checkbox
|
||||
cy.get('.mm-modal-generic-section-item__input-checkbox').should('not.be.checked').click();
|
||||
});
|
||||
|
||||
// # Set 'sample.mattermost.com' as the only allowed email domain and save
|
||||
cy.wait(TIMEOUTS.HALF_SEC);
|
||||
cy.focused().type(emailDomain);
|
||||
cy.get('#allowedDomains').click().type(emailDomain).type(' ');
|
||||
cy.findByText('Save').should('be.visible').click();
|
||||
|
||||
// # Close the modal
|
||||
|
@ -11,7 +11,6 @@
|
||||
// Group: @channels @team_settings
|
||||
|
||||
import {getRandomId, stubClipboard} from '../../../utils';
|
||||
import * as TIMEOUTS from '../../../fixtures/timeouts';
|
||||
|
||||
describe('Team Settings', () => {
|
||||
const randomId = getRandomId();
|
||||
@ -47,15 +46,22 @@ describe('Team Settings', () => {
|
||||
|
||||
// * Check that the 'Team Settings' modal was opened
|
||||
cy.get('#teamSettingsModal').should('exist').within(() => {
|
||||
// # Click on the 'Allow only users with a specific email domain to join this team' edit button
|
||||
cy.get('#allowed_domainsEdit').should('be.visible').click();
|
||||
// # Go to Access section
|
||||
cy.get('#accessButton').click();
|
||||
|
||||
// # Set 'sample.mattermost.com' as the only allowed email domain, save then close
|
||||
cy.wait(TIMEOUTS.HALF_SEC);
|
||||
cy.focused().type(emailDomain);
|
||||
cy.uiSaveAndClose();
|
||||
// # Click on the 'Allow only users with a specific email domain to join this team' edit button
|
||||
cy.get('.access-allowed-domains-section').should('exist').within(() => {
|
||||
// # Click on the 'Allow only users with a specific email domain to join this team' checkbox
|
||||
cy.get('.mm-modal-generic-section-item__input-checkbox').should('not.be.checked').click();
|
||||
});
|
||||
|
||||
// # Set 'sample.mattermost.com' as the only allowed email domain and save
|
||||
cy.get('#allowedDomains').click().type(emailDomain).type(' ');
|
||||
cy.findByText('Save').should('be.visible').click();
|
||||
});
|
||||
|
||||
cy.uiClose();
|
||||
|
||||
// # Open team menu and click 'Invite People'
|
||||
cy.uiOpenTeamMenu('Invite People');
|
||||
|
||||
@ -92,26 +98,26 @@ describe('Team Settings', () => {
|
||||
|
||||
// * Check that the 'Team Settings' modal was opened
|
||||
cy.get('#teamSettingsModal').should('exist').within(() => {
|
||||
// # Click on the 'Allow any user with an account on this server to join this team' edit button
|
||||
cy.get('#open_inviteEdit').should('be.visible').click();
|
||||
// # Go to Access section
|
||||
cy.get('#accessButton').click();
|
||||
|
||||
// # Enable any user with an account on the server to join the team
|
||||
cy.get('#teamOpenInvite').should('be.visible').check();
|
||||
|
||||
// # Save and verify it took effect
|
||||
cy.uiSave();
|
||||
cy.get('#open_inviteDesc').should('be.visible').and('have.text', 'Yes');
|
||||
cy.get('.access-invite-domains-section').should('exist').within(() => {
|
||||
// # Enable any user with an account on the server to join the team
|
||||
cy.get('.mm-modal-generic-section-item__input-checkbox').should('not.be.checked').click();
|
||||
});
|
||||
|
||||
// # Click on the 'Allow only users with a specific email domain to join this team' edit button
|
||||
cy.get('#allowed_domainsEdit').should('be.visible').click();
|
||||
cy.get('.access-allowed-domains-section').should('exist').within(() => {
|
||||
// # Click on the 'Allow only users with a specific email domain to join this team' checkbox
|
||||
cy.get('.mm-modal-generic-section-item__input-checkbox').should('not.be.checked').click();
|
||||
});
|
||||
|
||||
// # Set 'sample.mattermost.com' as the only allowed email domain and save
|
||||
cy.wait(TIMEOUTS.HALF_SEC);
|
||||
cy.findByRole('textbox', {name: 'Allowed Domains'}).should('be.visible').and('be.focused').type(emailDomain);
|
||||
cy.get('#allowedDomains').click().type(emailDomain).type(' ');
|
||||
cy.findByText('Save').should('be.visible').click();
|
||||
|
||||
// # Save and verify it took effect
|
||||
cy.uiSave();
|
||||
cy.get('#allowed_domainsDesc').should('be.visible').and('have.text', emailDomain);
|
||||
|
||||
// # Close the modal
|
||||
cy.uiClose();
|
||||
|
@ -28,56 +28,34 @@ describe('Teams Settings', () => {
|
||||
// # Open team settings dialog
|
||||
openTeamSettingsDialog();
|
||||
|
||||
// * Verify the settings picture button is visible to click
|
||||
cy.findByTestId('inputSettingPictureButton').should('be.visible').click();
|
||||
|
||||
// * Before uploading the picture the save button must be disabled
|
||||
cy.uiSaveButton().should('be.disabled');
|
||||
|
||||
// # Upload a file on center view
|
||||
cy.findByTestId('uploadPicture').attachFile('mattermost-icon.png');
|
||||
|
||||
// * Save then close
|
||||
cy.uiSaveAndClose();
|
||||
// * Save
|
||||
cy.uiSave();
|
||||
|
||||
// * Verify team icon
|
||||
cy.get(`#${testTeam.name}TeamButton`).within(() => {
|
||||
cy.findByTestId('teamIconImage').should('be.visible');
|
||||
cy.findByTestId('teamIconInitial').should('not.exist');
|
||||
});
|
||||
|
||||
// # Open the team settings dialog
|
||||
openTeamSettingsDialog();
|
||||
|
||||
// # Click on 'X' icon to remove the image
|
||||
cy.findByTestId('removeSettingPicture').should('be.visible').click();
|
||||
|
||||
// # Click on the cancel button
|
||||
cy.findByTestId('cancelSettingPicture').should('be.visible').click();
|
||||
cy.get('#teamIconImage').should('be.visible');
|
||||
cy.get('#teamIconInitial').should('not.exist');
|
||||
|
||||
// # Close the team settings dialog
|
||||
cy.uiClose();
|
||||
|
||||
// * Verify the team icon image is visible and initial team holder is not visible
|
||||
cy.get(`#${testTeam.name}TeamButton`).within(() => {
|
||||
cy.findByTestId('teamIconImage').should('be.visible');
|
||||
cy.findByTestId('teamIconInitial').should('not.exist');
|
||||
});
|
||||
|
||||
// # Open team settings dialog
|
||||
// # Open the team settings dialog
|
||||
openTeamSettingsDialog();
|
||||
|
||||
// # Click on 'X' icon to remove the image
|
||||
cy.findByTestId('removeSettingPicture').should('be.visible').click();
|
||||
// # Click on 'Remove Image' button to remove the image
|
||||
cy.findByTestId('removeImageButton').should('be.visible').click();
|
||||
|
||||
// # Save and close the modal
|
||||
cy.uiSaveAndClose();
|
||||
// # Close the modal
|
||||
cy.uiClose();
|
||||
|
||||
// # Open the team settings dialog
|
||||
openTeamSettingsDialog();
|
||||
|
||||
// * After removing the team icon initial team holder is visible but not team icon holder
|
||||
cy.get(`#${testTeam.name}TeamButton`).within(() => {
|
||||
cy.findByTestId('teamIconImage').should('not.exist');
|
||||
cy.findByTestId('teamIconInitial').should('be.visible');
|
||||
});
|
||||
cy.get('#teamIconImage').should('not.exist');
|
||||
cy.get('#teamIconInitial').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
@ -88,9 +66,11 @@ function openTeamSettingsDialog() {
|
||||
// * Verify the team settings dialog is open
|
||||
cy.get('#teamSettingsModalLabel').should('be.visible').and('contain', 'Team Settings');
|
||||
|
||||
// * Verify the edit icon is visible
|
||||
cy.get('#team_iconEdit').should('be.visible');
|
||||
cy.get('.team-picture-section').within(() => {
|
||||
// * Verify the edit icon is visible
|
||||
cy.get('.icon-pencil-outline').should('be.visible');
|
||||
|
||||
// # Click on edit button
|
||||
cy.get('#team_iconEdit').click();
|
||||
// # Click on edit button
|
||||
cy.get('.icon-pencil-outline').click();
|
||||
});
|
||||
}
|
||||
|
@ -186,6 +186,9 @@ describe('Teams Suite', () => {
|
||||
// # Open team menu and click "Team Settings"
|
||||
cy.uiOpenTeamMenu('Team Settings');
|
||||
|
||||
// # Go to Access section
|
||||
cy.get('#accessButton').click();
|
||||
|
||||
// # Open edit settings for invite code
|
||||
cy.findByText('Invite Code').should('be.visible').click();
|
||||
|
||||
@ -213,11 +216,8 @@ describe('Teams Suite', () => {
|
||||
// # Change team name in the input
|
||||
cy.get('#teamName').should('be.visible').clear().type(teamName);
|
||||
|
||||
// Save new team name
|
||||
cy.findByText(/save/i).click();
|
||||
|
||||
// # Close the team settings
|
||||
cy.get('body').typeWithForce('{esc}');
|
||||
// Save new team name annd close<
|
||||
cy.uiSaveAndClose();
|
||||
|
||||
// Team display name shows as "Testing Team" at top of team menu
|
||||
cy.uiGetLHSHeader().findByText(teamName);
|
||||
@ -238,9 +238,6 @@ describe('Teams Suite', () => {
|
||||
// # Open team menu and click "Team Settings"
|
||||
cy.uiOpenTeamMenu('Team Settings');
|
||||
|
||||
// # Click on the team description menu item
|
||||
cy.findByText('Team Description').should('be.visible').click();
|
||||
|
||||
// # Change team description in the input
|
||||
cy.get('#teamDescription').should('be.visible').clear().type(teamDescription);
|
||||
cy.get('#teamDescription').should('have.value', teamDescription);
|
||||
@ -252,7 +249,7 @@ describe('Teams Suite', () => {
|
||||
cy.uiOpenTeamMenu('Team Settings');
|
||||
|
||||
// * Verify team description is updated
|
||||
cy.get('#descriptionDesc').should('have.text', teamDescription);
|
||||
cy.get('#teamDescription').should('have.text', teamDescription);
|
||||
});
|
||||
|
||||
it('MM-T2318 Allow anyone to join this team', () => {
|
||||
@ -262,17 +259,16 @@ describe('Teams Suite', () => {
|
||||
// # Open team menu and click "Team Settings"
|
||||
cy.uiOpenTeamMenu('Team Settings');
|
||||
|
||||
// # Click on the team description menu item
|
||||
cy.findByText('Allow any user with an account on this server to join this team').should('be.visible').click();
|
||||
// # Go to Access section
|
||||
cy.get('#accessButton').click();
|
||||
|
||||
// # Change team description in the input
|
||||
cy.get('#teamOpenInvite').click();
|
||||
cy.get('.access-invite-domains-section').should('exist').within(() => {
|
||||
// # Click on the 'Allow any user with an account on this server to join this team' checkbox
|
||||
cy.get('.mm-modal-generic-section-item__input-checkbox').should('not.be.checked').click();
|
||||
});
|
||||
|
||||
// Save new team description
|
||||
cy.findByText(/save/i).click();
|
||||
|
||||
// # Close the team settings
|
||||
cy.get('body').typeWithForce('{esc}');
|
||||
// # Save and close
|
||||
cy.uiSaveAndClose();
|
||||
|
||||
// # Login as new user
|
||||
cy.apiLogin(newUser);
|
||||
@ -302,14 +298,16 @@ describe('Teams Suite', () => {
|
||||
// # Open team menu and click "Team Settings"
|
||||
cy.uiOpenTeamMenu('Team Settings');
|
||||
|
||||
// # Click on the team description menu item
|
||||
cy.findByText('Allow any user with an account on this server to join this team').should('be.visible').click();
|
||||
// # Go to Access section
|
||||
cy.get('#accessButton').click();
|
||||
|
||||
// # Change team description in the input
|
||||
cy.get('#teamOpenInviteNo').click();
|
||||
cy.get('.access-invite-domains-section').should('exist').within(() => {
|
||||
// # Click on the 'Allow any user with an account on this server to join this team' checkbox
|
||||
cy.get('.mm-modal-generic-section-item__input-checkbox').should('not.be.checked');
|
||||
});
|
||||
|
||||
// Save new team description and close team settings
|
||||
cy.uiSaveAndClose();
|
||||
// # Save and close
|
||||
cy.uiClose();
|
||||
|
||||
// # Login as new user
|
||||
cy.apiLogin(testUser);
|
||||
|
@ -96,7 +96,7 @@ const ChannelNameFormField = (props: Props): JSX.Element => {
|
||||
}
|
||||
}, [props.onURLChange, displayName.current, url, displayNameModified]);
|
||||
|
||||
const handleOnURLChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleOnURLChange = useCallback((e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
e.preventDefault();
|
||||
const {target: {value: url}} = e;
|
||||
|
||||
|
@ -76,7 +76,7 @@ Object {
|
||||
class="mm-modal-generic-section"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__info-ctr"
|
||||
class="mm-modal-generic-section__title-description-ctr"
|
||||
>
|
||||
<h4
|
||||
class="mm-modal-generic-section__title"
|
||||
@ -92,6 +92,7 @@ Object {
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox-ctr"
|
||||
@ -112,6 +113,7 @@ Object {
|
||||
</div>
|
||||
<p
|
||||
class="mm-modal-generic-section-item__description"
|
||||
data-testid="mm-modal-generic-section-item__description"
|
||||
>
|
||||
Turns off notifications for this channel. You’ll still see badges if you’re mentioned.
|
||||
</p>
|
||||
@ -121,6 +123,7 @@ Object {
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox-ctr"
|
||||
@ -141,6 +144,7 @@ Object {
|
||||
</div>
|
||||
<p
|
||||
class="mm-modal-generic-section-item__description"
|
||||
data-testid="mm-modal-generic-section-item__description"
|
||||
>
|
||||
When enabled, @channel, @here and @all will not trigger mentions or mention notifications in this channel
|
||||
</p>
|
||||
@ -154,7 +158,7 @@ Object {
|
||||
class="mm-modal-generic-section"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__info-ctr"
|
||||
class="mm-modal-generic-section__title-description-ctr"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__row"
|
||||
@ -179,11 +183,13 @@ Object {
|
||||
>
|
||||
<h4
|
||||
class="mm-modal-generic-section-item__title"
|
||||
data-testid="mm-modal-generic-section-item__title"
|
||||
>
|
||||
Notify me about…
|
||||
</h4>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-radio"
|
||||
@ -238,7 +244,7 @@ Object {
|
||||
class="mm-modal-generic-section"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__info-ctr"
|
||||
class="mm-modal-generic-section__title-description-ctr"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__row"
|
||||
@ -280,6 +286,7 @@ Object {
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox-ctr"
|
||||
@ -304,11 +311,13 @@ Object {
|
||||
>
|
||||
<h4
|
||||
class="mm-modal-generic-section-item__title"
|
||||
data-testid="mm-modal-generic-section-item__title"
|
||||
>
|
||||
Notify me about…
|
||||
</h4>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-radio"
|
||||
@ -511,7 +520,7 @@ Object {
|
||||
class="mm-modal-generic-section"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__info-ctr"
|
||||
class="mm-modal-generic-section__title-description-ctr"
|
||||
>
|
||||
<h4
|
||||
class="mm-modal-generic-section__title"
|
||||
@ -527,6 +536,7 @@ Object {
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox-ctr"
|
||||
@ -547,6 +557,7 @@ Object {
|
||||
</div>
|
||||
<p
|
||||
class="mm-modal-generic-section-item__description"
|
||||
data-testid="mm-modal-generic-section-item__description"
|
||||
>
|
||||
Turns off notifications for this channel. You’ll still see badges if you’re mentioned.
|
||||
</p>
|
||||
@ -556,6 +567,7 @@ Object {
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox-ctr"
|
||||
@ -576,6 +588,7 @@ Object {
|
||||
</div>
|
||||
<p
|
||||
class="mm-modal-generic-section-item__description"
|
||||
data-testid="mm-modal-generic-section-item__description"
|
||||
>
|
||||
When enabled, @channel, @here and @all will not trigger mentions or mention notifications in this channel
|
||||
</p>
|
||||
@ -589,7 +602,7 @@ Object {
|
||||
class="mm-modal-generic-section"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__info-ctr"
|
||||
class="mm-modal-generic-section__title-description-ctr"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__row"
|
||||
@ -631,11 +644,13 @@ Object {
|
||||
>
|
||||
<h4
|
||||
class="mm-modal-generic-section-item__title"
|
||||
data-testid="mm-modal-generic-section-item__title"
|
||||
>
|
||||
Notify me about…
|
||||
</h4>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-radio"
|
||||
@ -690,7 +705,7 @@ Object {
|
||||
class="mm-modal-generic-section"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__info-ctr"
|
||||
class="mm-modal-generic-section__title-description-ctr"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__row"
|
||||
@ -732,6 +747,7 @@ Object {
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox-ctr"
|
||||
@ -756,11 +772,13 @@ Object {
|
||||
>
|
||||
<h4
|
||||
class="mm-modal-generic-section-item__title"
|
||||
data-testid="mm-modal-generic-section-item__title"
|
||||
>
|
||||
Notify me about…
|
||||
</h4>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-radio"
|
||||
@ -963,7 +981,7 @@ Object {
|
||||
class="mm-modal-generic-section"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__info-ctr"
|
||||
class="mm-modal-generic-section__title-description-ctr"
|
||||
>
|
||||
<h4
|
||||
class="mm-modal-generic-section__title"
|
||||
@ -979,6 +997,7 @@ Object {
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox-ctr"
|
||||
@ -999,6 +1018,7 @@ Object {
|
||||
</div>
|
||||
<p
|
||||
class="mm-modal-generic-section-item__description"
|
||||
data-testid="mm-modal-generic-section-item__description"
|
||||
>
|
||||
Turns off notifications for this channel. You’ll still see badges if you’re mentioned.
|
||||
</p>
|
||||
@ -1008,6 +1028,7 @@ Object {
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox-ctr"
|
||||
@ -1028,6 +1049,7 @@ Object {
|
||||
</div>
|
||||
<p
|
||||
class="mm-modal-generic-section-item__description"
|
||||
data-testid="mm-modal-generic-section-item__description"
|
||||
>
|
||||
When enabled, @channel, @here and @all will not trigger mentions or mention notifications in this channel
|
||||
</p>
|
||||
@ -1041,7 +1063,7 @@ Object {
|
||||
class="mm-modal-generic-section"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__info-ctr"
|
||||
class="mm-modal-generic-section__title-description-ctr"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__row"
|
||||
@ -1066,11 +1088,13 @@ Object {
|
||||
>
|
||||
<h4
|
||||
class="mm-modal-generic-section-item__title"
|
||||
data-testid="mm-modal-generic-section-item__title"
|
||||
>
|
||||
Notify me about…
|
||||
</h4>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-radio"
|
||||
@ -1125,7 +1149,7 @@ Object {
|
||||
class="mm-modal-generic-section"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__info-ctr"
|
||||
class="mm-modal-generic-section__title-description-ctr"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__row"
|
||||
@ -1167,6 +1191,7 @@ Object {
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox-ctr"
|
||||
@ -1191,11 +1216,13 @@ Object {
|
||||
>
|
||||
<h4
|
||||
class="mm-modal-generic-section-item__title"
|
||||
data-testid="mm-modal-generic-section-item__title"
|
||||
>
|
||||
Notify me about…
|
||||
</h4>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-radio"
|
||||
@ -1398,7 +1425,7 @@ Object {
|
||||
class="mm-modal-generic-section"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__info-ctr"
|
||||
class="mm-modal-generic-section__title-description-ctr"
|
||||
>
|
||||
<h4
|
||||
class="mm-modal-generic-section__title"
|
||||
@ -1414,6 +1441,7 @@ Object {
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox-ctr"
|
||||
@ -1434,6 +1462,7 @@ Object {
|
||||
</div>
|
||||
<p
|
||||
class="mm-modal-generic-section-item__description"
|
||||
data-testid="mm-modal-generic-section-item__description"
|
||||
>
|
||||
Turns off notifications for this channel. You’ll still see badges if you’re mentioned.
|
||||
</p>
|
||||
@ -1443,6 +1472,7 @@ Object {
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox-ctr"
|
||||
@ -1463,6 +1493,7 @@ Object {
|
||||
</div>
|
||||
<p
|
||||
class="mm-modal-generic-section-item__description"
|
||||
data-testid="mm-modal-generic-section-item__description"
|
||||
>
|
||||
When enabled, @channel, @here and @all will not trigger mentions or mention notifications in this channel
|
||||
</p>
|
||||
@ -1659,7 +1690,7 @@ Object {
|
||||
class="mm-modal-generic-section"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__info-ctr"
|
||||
class="mm-modal-generic-section__title-description-ctr"
|
||||
>
|
||||
<h4
|
||||
class="mm-modal-generic-section__title"
|
||||
@ -1675,6 +1706,7 @@ Object {
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox-ctr"
|
||||
@ -1695,6 +1727,7 @@ Object {
|
||||
</div>
|
||||
<p
|
||||
class="mm-modal-generic-section-item__description"
|
||||
data-testid="mm-modal-generic-section-item__description"
|
||||
>
|
||||
Turns off notifications for this channel. You’ll still see badges if you’re mentioned.
|
||||
</p>
|
||||
@ -1704,6 +1737,7 @@ Object {
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox-ctr"
|
||||
@ -1724,6 +1758,7 @@ Object {
|
||||
</div>
|
||||
<p
|
||||
class="mm-modal-generic-section-item__description"
|
||||
data-testid="mm-modal-generic-section-item__description"
|
||||
>
|
||||
When enabled, @channel, @here and @all will not trigger mentions or mention notifications in this channel
|
||||
</p>
|
||||
@ -1737,7 +1772,7 @@ Object {
|
||||
class="mm-modal-generic-section"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__info-ctr"
|
||||
class="mm-modal-generic-section__title-description-ctr"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__row"
|
||||
@ -1762,11 +1797,13 @@ Object {
|
||||
>
|
||||
<h4
|
||||
class="mm-modal-generic-section-item__title"
|
||||
data-testid="mm-modal-generic-section-item__title"
|
||||
>
|
||||
Notify me about…
|
||||
</h4>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-radio"
|
||||
@ -1821,7 +1858,7 @@ Object {
|
||||
class="mm-modal-generic-section"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__info-ctr"
|
||||
class="mm-modal-generic-section__title-description-ctr"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__row"
|
||||
@ -1863,6 +1900,7 @@ Object {
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox-ctr"
|
||||
@ -2040,7 +2078,7 @@ Object {
|
||||
class="mm-modal-generic-section"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__info-ctr"
|
||||
class="mm-modal-generic-section__title-description-ctr"
|
||||
>
|
||||
<h4
|
||||
class="mm-modal-generic-section__title"
|
||||
@ -2056,6 +2094,7 @@ Object {
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox-ctr"
|
||||
@ -2076,6 +2115,7 @@ Object {
|
||||
</div>
|
||||
<p
|
||||
class="mm-modal-generic-section-item__description"
|
||||
data-testid="mm-modal-generic-section-item__description"
|
||||
>
|
||||
Turns off notifications for this channel. You’ll still see badges if you’re mentioned.
|
||||
</p>
|
||||
@ -2085,6 +2125,7 @@ Object {
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox-ctr"
|
||||
@ -2105,6 +2146,7 @@ Object {
|
||||
</div>
|
||||
<p
|
||||
class="mm-modal-generic-section-item__description"
|
||||
data-testid="mm-modal-generic-section-item__description"
|
||||
>
|
||||
When enabled, @channel, @here and @all will not trigger mentions or mention notifications in this channel
|
||||
</p>
|
||||
@ -2118,7 +2160,7 @@ Object {
|
||||
class="mm-modal-generic-section"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__info-ctr"
|
||||
class="mm-modal-generic-section__title-description-ctr"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__row"
|
||||
@ -2143,11 +2185,13 @@ Object {
|
||||
>
|
||||
<h4
|
||||
class="mm-modal-generic-section-item__title"
|
||||
data-testid="mm-modal-generic-section-item__title"
|
||||
>
|
||||
Notify me about…
|
||||
</h4>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-radio"
|
||||
@ -2202,7 +2246,7 @@ Object {
|
||||
class="mm-modal-generic-section"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__info-ctr"
|
||||
class="mm-modal-generic-section__title-description-ctr"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__row"
|
||||
@ -2244,6 +2288,7 @@ Object {
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox-ctr"
|
||||
@ -2268,11 +2313,13 @@ Object {
|
||||
>
|
||||
<h4
|
||||
class="mm-modal-generic-section-item__title"
|
||||
data-testid="mm-modal-generic-section-item__title"
|
||||
>
|
||||
Notify me about…
|
||||
</h4>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-radio"
|
||||
@ -2326,7 +2373,7 @@ Object {
|
||||
class="mm-modal-generic-section"
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section__info-ctr"
|
||||
class="mm-modal-generic-section__title-description-ctr"
|
||||
>
|
||||
<h4
|
||||
class="mm-modal-generic-section__title"
|
||||
@ -2347,6 +2394,7 @@ Object {
|
||||
>
|
||||
<div
|
||||
class="mm-modal-generic-section-item__content"
|
||||
data-testid="mm-modal-generic-section-item__content"
|
||||
>
|
||||
<fieldset
|
||||
class="mm-modal-generic-section-item__fieldset-checkbox-ctr"
|
||||
|
@ -0,0 +1,9 @@
|
||||
.select-text-description {
|
||||
margin-top: 8px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
font-family: Open Sans;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import type {CSSProperties, KeyboardEventHandler} from 'react';
|
||||
import CreatableSelect from 'react-select/creatable';
|
||||
|
||||
import './select_text_input.scss';
|
||||
|
||||
const components = {
|
||||
DropdownIndicator: null,
|
||||
};
|
||||
|
||||
export interface SelectTextInputOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
placeholder: string;
|
||||
value: string[];
|
||||
handleNewSelection: (selection: string) => void;
|
||||
onChange: (option?: SelectTextInputOption[] | null) => void;
|
||||
id?: string;
|
||||
isClearable?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const styles = {
|
||||
control: (baseStyles: CSSProperties) => ({
|
||||
...baseStyles,
|
||||
background: 'var(--center-channel-color-rgb)',
|
||||
}),
|
||||
input: (baseStyles: CSSProperties) => ({
|
||||
...baseStyles,
|
||||
color: 'rgba(var(--center-channel-color-rgb), 0.64)',
|
||||
}),
|
||||
multiValue: (baseStyles: CSSProperties) => ({
|
||||
...baseStyles,
|
||||
borderRadius: '10px',
|
||||
background: 'rgba(var(--center-channel-color-rgb), 0.08)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}),
|
||||
multiValueLabel: (baseStyles: CSSProperties) => ({
|
||||
...baseStyles,
|
||||
padding: '4px 6px 4px 10px',
|
||||
color: 'var(--center-channel-color)',
|
||||
fontFamily: 'Open Sans',
|
||||
fontSize: '10px',
|
||||
fontWeight: 600,
|
||||
lineHeight: '12px',
|
||||
letterSpacing: '0.2px',
|
||||
}),
|
||||
multiValueRemove: (baseStyles: CSSProperties) => ({
|
||||
...baseStyles,
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(var(--center-channel-color-rgb), 0.32)',
|
||||
fontFamily: 'compass-icons',
|
||||
fontSize: '12px',
|
||||
fontWeight: 400,
|
||||
color: 'white',
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
padding: 0,
|
||||
marginRight: '4px',
|
||||
':hover': {
|
||||
background: 'rgba(var(--center-channel-color-rgb), 0.32)',
|
||||
color: 'white',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const SelectTextInput = ({placeholder, value, handleNewSelection, onChange, id, isClearable, description}: Props) => {
|
||||
const [inputValue, setInputValue] = React.useState('');
|
||||
|
||||
const handleTextEnter = useCallback(() => {
|
||||
// do not add the value if already exists
|
||||
if (value?.includes(inputValue.trim()) || inputValue.length === 0) {
|
||||
return;
|
||||
}
|
||||
handleNewSelection(inputValue);
|
||||
setInputValue('');
|
||||
}, [handleNewSelection, inputValue, value]);
|
||||
|
||||
const handleKeyDown: KeyboardEventHandler = useCallback((event) => {
|
||||
if (!inputValue) {
|
||||
return;
|
||||
}
|
||||
switch (event.key) {
|
||||
case ' ':
|
||||
case ',':
|
||||
case 'Enter':
|
||||
handleTextEnter();
|
||||
event.preventDefault();
|
||||
}
|
||||
}, [inputValue, handleTextEnter]);
|
||||
|
||||
const selectValues = useMemo(() => {
|
||||
return value.map((singleValue) => ({label: singleValue, value: singleValue}));
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreatableSelect
|
||||
id={id}
|
||||
className='select-text-input'
|
||||
styles={styles}
|
||||
components={components}
|
||||
isClearable={isClearable}
|
||||
onChange={useCallback((value) => onChange(value as SelectTextInputOption[]), [onChange])}
|
||||
inputValue={inputValue}
|
||||
isMulti={true}
|
||||
menuIsOpen={false}
|
||||
onInputChange={setInputValue}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
value={selectValues}
|
||||
onBlur={handleTextEnter}
|
||||
/>
|
||||
{description ? <p className='select-text-description'>{description}</p> : undefined}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectTextInput;
|
@ -16,9 +16,8 @@ import {haveICurrentTeamPermission, haveISystemPermission} from 'mattermost-redu
|
||||
import {
|
||||
getJoinableTeamIds,
|
||||
getCurrentTeam,
|
||||
getCurrentRelativeTeamUrl,
|
||||
} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentUser, isFirstAdmin} from 'mattermost-redux/selectors/entities/users';
|
||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
import {showMentions, showFlaggedPosts, closeRightHandSide, closeMenu as closeRhsMenu} from 'actions/views/rhs';
|
||||
@ -83,10 +82,8 @@ function mapStateToProps(state: GlobalState) {
|
||||
isMentionSearch: rhsState === RHSStates.MENTION,
|
||||
teamIsGroupConstrained: Boolean(currentTeam.group_constrained),
|
||||
isLicensedForLDAPGroups: state.entities.general.license.LDAPGroups === 'true',
|
||||
teamUrl: getCurrentRelativeTeamUrl(state),
|
||||
guestAccessEnabled: config.EnableGuestAccounts === 'true',
|
||||
canInviteTeamMember,
|
||||
isFirstAdmin: isFirstAdmin(state),
|
||||
isCloud,
|
||||
isStarterFree,
|
||||
isFreeTrial,
|
||||
|
@ -52,12 +52,7 @@ describe('components/Menu', () => {
|
||||
moreTeamsToJoin: false,
|
||||
pluginMenuItems: [],
|
||||
isMentionSearch: false,
|
||||
isFirstAdmin: false,
|
||||
intl: createIntl({locale: 'en', defaultLocale: 'en', timeZone: 'Etc/UTC', textComponent: 'span'}),
|
||||
teamUrl: '/team',
|
||||
location: {
|
||||
pathname: '/team',
|
||||
},
|
||||
guestAccessEnabled: true,
|
||||
canInviteTeamMember: true,
|
||||
actions: {
|
||||
|
@ -61,15 +61,10 @@ export type Props = {
|
||||
teamIsGroupConstrained: boolean;
|
||||
isLicensedForLDAPGroups?: boolean;
|
||||
intl: IntlShape;
|
||||
teamUrl: string;
|
||||
isFirstAdmin: boolean;
|
||||
isCloud: boolean;
|
||||
isStarterFree: boolean;
|
||||
isFreeTrial: boolean;
|
||||
usageDeltaTeams: number;
|
||||
location: {
|
||||
pathname: string;
|
||||
};
|
||||
guestAccessEnabled: boolean;
|
||||
canInviteTeamMember: boolean;
|
||||
actions: {
|
||||
|
@ -35,7 +35,7 @@ const AddressForm = (props: AddressFormProps) => {
|
||||
const handleInputChange = (key: keyof Address) => (
|
||||
event:
|
||||
| React.ChangeEvent<HTMLInputElement>
|
||||
| React.ChangeEvent<HTMLSelectElement>,
|
||||
| React.ChangeEvent<HTMLTextAreaElement>,
|
||||
) => {
|
||||
const target = event.target;
|
||||
const value = target.value;
|
||||
|
@ -65,7 +65,7 @@ const PaymentForm: React.FC<Props> = (props: Props) => {
|
||||
company_name: props.customer?.name || '',
|
||||
});
|
||||
|
||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement> | React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement> | React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const target = event.target;
|
||||
const name = target.name;
|
||||
const value = target.value;
|
||||
|
@ -679,7 +679,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleOnBlur = (e: FocusEvent<HTMLInputElement>, inputId: string) => {
|
||||
const handleOnBlur = (e: FocusEvent<HTMLInputElement | HTMLTextAreaElement>, inputId: string) => {
|
||||
const text = e.target.value;
|
||||
if (!text) {
|
||||
return;
|
||||
|
@ -1,129 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/TeamSettings/OpenInvite should match snapshot on active with groupConstrained 1`] = `
|
||||
<SettingItemMax
|
||||
containerStyle=""
|
||||
infoPosition="bottom"
|
||||
inputs={
|
||||
Array [
|
||||
<div>
|
||||
<div>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="No, members of this team are added and removed by linked groups. <link>Learn More</link>"
|
||||
id="team_settings.openInviteDescription.groupConstrained"
|
||||
values={
|
||||
Object {
|
||||
"link": [Function],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
}
|
||||
saving={false}
|
||||
section=""
|
||||
serverError=""
|
||||
submit={[Function]}
|
||||
title="Allow any user with an account on this server to join this team"
|
||||
updateSection={[Function]}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`components/TeamSettings/OpenInvite should match snapshot on active without groupConstrained 1`] = `
|
||||
<SettingItemMax
|
||||
containerStyle=""
|
||||
infoPosition="bottom"
|
||||
inputs={
|
||||
Array [
|
||||
<fieldset>
|
||||
<legend
|
||||
className="form-legend hidden-label"
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Invite Code"
|
||||
id="team_settings.openInviteDescription.ariaLabel"
|
||||
/>
|
||||
</legend>
|
||||
<div
|
||||
className="radio"
|
||||
>
|
||||
<label>
|
||||
<input
|
||||
defaultChecked={false}
|
||||
id="teamOpenInvite"
|
||||
name="userOpenInviteOptions"
|
||||
onChange={[Function]}
|
||||
type="radio"
|
||||
/>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Yes"
|
||||
id="general_tab.yes"
|
||||
/>
|
||||
</label>
|
||||
<br />
|
||||
</div>
|
||||
<div
|
||||
className="radio"
|
||||
>
|
||||
<label>
|
||||
<input
|
||||
defaultChecked={true}
|
||||
id="teamOpenInviteNo"
|
||||
name="userOpenInviteOptions"
|
||||
onChange={[Function]}
|
||||
type="radio"
|
||||
/>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="No"
|
||||
id="general_tab.no"
|
||||
/>
|
||||
</label>
|
||||
<br />
|
||||
</div>
|
||||
<div
|
||||
className="mt-5"
|
||||
>
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="When allowed, a link to this team will be included on the landing page allowing anyone with an account to join this team. Changing from \\"Yes\\" to \\"No\\" will regenerate the invitation code, create a new invitation link and invalidate the previous link."
|
||||
id="general_tab.openInviteDesc"
|
||||
/>
|
||||
</div>
|
||||
</fieldset>,
|
||||
]
|
||||
}
|
||||
saving={false}
|
||||
section=""
|
||||
serverError=""
|
||||
submit={[Function]}
|
||||
title="Allow any user with an account on this server to join this team"
|
||||
updateSection={[Function]}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`components/TeamSettings/OpenInvite should match snapshot on non active allowing open invite 1`] = `
|
||||
<SettingItemMin
|
||||
describe="Yes"
|
||||
section="open_invite"
|
||||
title="Allow any user with an account on this server to join this team"
|
||||
updateSection={[Function]}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`components/TeamSettings/OpenInvite should match snapshot on non active with groupConstrained 1`] = `
|
||||
<SettingItemMin
|
||||
describe="No, members of this team are added and removed by linked groups."
|
||||
section="open_invite"
|
||||
title="Allow any user with an account on this server to join this team"
|
||||
updateSection={[Function]}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`components/TeamSettings/OpenInvite should match snapshot on non active without groupConstrained 1`] = `
|
||||
<SettingItemMin
|
||||
describe="No"
|
||||
section="open_invite"
|
||||
title="Allow any user with an account on this server to join this team"
|
||||
updateSection={[Function]}
|
||||
/>
|
||||
`;
|
@ -1,361 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/TeamSettings hide invite code if no permissions for team inviting 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="modal-header"
|
||||
>
|
||||
<button
|
||||
aria-label="Close"
|
||||
className="close"
|
||||
data-dismiss="modal"
|
||||
id="closeButton"
|
||||
onClick={[MockFunction]}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
×
|
||||
</span>
|
||||
</button>
|
||||
<h4
|
||||
className="modal-title"
|
||||
>
|
||||
<div
|
||||
className="modal-back"
|
||||
>
|
||||
<span
|
||||
onClick={[MockFunction]}
|
||||
>
|
||||
<BackIcon />
|
||||
</span>
|
||||
</div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="General Settings"
|
||||
id="general_tab.title"
|
||||
/>
|
||||
</h4>
|
||||
</div>
|
||||
<div
|
||||
className="user-settings"
|
||||
>
|
||||
<div
|
||||
className="GeneralTab__header"
|
||||
>
|
||||
<h3>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="General Settings"
|
||||
id="general_tab.title"
|
||||
/>
|
||||
</h3>
|
||||
<LearnAboutTeamsLink />
|
||||
</div>
|
||||
<div
|
||||
className="divider-dark first"
|
||||
/>
|
||||
<SettingItemMin
|
||||
describe="name"
|
||||
section="name"
|
||||
title="Team Name"
|
||||
updateSection={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="divider-light"
|
||||
/>
|
||||
<SettingItemMin
|
||||
describe=""
|
||||
section="description"
|
||||
title="Team Description"
|
||||
updateSection={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="divider-light"
|
||||
/>
|
||||
<SettingPicture
|
||||
clientError=""
|
||||
file={null}
|
||||
helpText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Upload a team icon in BMP, JPG or PNG format.\\\\nSquare images with a solid background color are recommended."
|
||||
id="setting_picture.help.team"
|
||||
/>
|
||||
}
|
||||
imageContext="team"
|
||||
loadingPicture={false}
|
||||
onFileChange={[Function]}
|
||||
onRemove={[Function]}
|
||||
onSubmit={[Function]}
|
||||
serverError=""
|
||||
src={null}
|
||||
submitActive={false}
|
||||
title="Team Icon"
|
||||
updateSection={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="divider-light"
|
||||
/>
|
||||
<SettingItemMin
|
||||
describe=""
|
||||
section="allowed_domains"
|
||||
title="allowedDomains"
|
||||
updateSection={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="divider-light"
|
||||
/>
|
||||
<OpenInvite
|
||||
allowOpenInvite={false}
|
||||
isActive={false}
|
||||
isGroupConstrained={false}
|
||||
onToggle={[Function]}
|
||||
patchTeam={[MockFunction]}
|
||||
teamId="team_id"
|
||||
/>
|
||||
<div
|
||||
className="divider-light"
|
||||
/>
|
||||
<SettingItemMin
|
||||
describe="Click 'Edit' to regenerate Invite Code."
|
||||
section="invite_id"
|
||||
title="Invite Code"
|
||||
updateSection={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="divider-dark"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/TeamSettings hide invite code if no permissions for team inviting 2`] = `
|
||||
<div>
|
||||
<div
|
||||
className="modal-header"
|
||||
>
|
||||
<button
|
||||
aria-label="Close"
|
||||
className="close"
|
||||
data-dismiss="modal"
|
||||
id="closeButton"
|
||||
onClick={[MockFunction]}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
×
|
||||
</span>
|
||||
</button>
|
||||
<h4
|
||||
className="modal-title"
|
||||
>
|
||||
<div
|
||||
className="modal-back"
|
||||
>
|
||||
<span
|
||||
onClick={[MockFunction]}
|
||||
>
|
||||
<BackIcon />
|
||||
</span>
|
||||
</div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="General Settings"
|
||||
id="general_tab.title"
|
||||
/>
|
||||
</h4>
|
||||
</div>
|
||||
<div
|
||||
className="user-settings"
|
||||
>
|
||||
<div
|
||||
className="GeneralTab__header"
|
||||
>
|
||||
<h3>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="General Settings"
|
||||
id="general_tab.title"
|
||||
/>
|
||||
</h3>
|
||||
<LearnAboutTeamsLink />
|
||||
</div>
|
||||
<div
|
||||
className="divider-dark first"
|
||||
/>
|
||||
<SettingItemMin
|
||||
describe="name"
|
||||
section="name"
|
||||
title="Team Name"
|
||||
updateSection={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="divider-light"
|
||||
/>
|
||||
<SettingItemMin
|
||||
describe=""
|
||||
section="description"
|
||||
title="Team Description"
|
||||
updateSection={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="divider-light"
|
||||
/>
|
||||
<SettingPicture
|
||||
clientError=""
|
||||
file={null}
|
||||
helpText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Upload a team icon in BMP, JPG or PNG format.\\\\nSquare images with a solid background color are recommended."
|
||||
id="setting_picture.help.team"
|
||||
/>
|
||||
}
|
||||
imageContext="team"
|
||||
loadingPicture={false}
|
||||
onFileChange={[Function]}
|
||||
onRemove={[Function]}
|
||||
onSubmit={[Function]}
|
||||
serverError=""
|
||||
src={null}
|
||||
submitActive={false}
|
||||
title="Team Icon"
|
||||
updateSection={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="divider-light"
|
||||
/>
|
||||
<SettingItemMin
|
||||
describe=""
|
||||
section="allowed_domains"
|
||||
title="allowedDomains"
|
||||
updateSection={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="divider-light"
|
||||
/>
|
||||
<OpenInvite
|
||||
allowOpenInvite={false}
|
||||
isActive={false}
|
||||
isGroupConstrained={false}
|
||||
onToggle={[Function]}
|
||||
patchTeam={[MockFunction]}
|
||||
teamId="team_id"
|
||||
/>
|
||||
<div
|
||||
className="divider-light"
|
||||
/>
|
||||
<div
|
||||
className="divider-dark"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`components/TeamSettings should match snapshot when team is group constrained 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="modal-header"
|
||||
>
|
||||
<button
|
||||
aria-label="Close"
|
||||
className="close"
|
||||
data-dismiss="modal"
|
||||
id="closeButton"
|
||||
onClick={[MockFunction]}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
×
|
||||
</span>
|
||||
</button>
|
||||
<h4
|
||||
className="modal-title"
|
||||
>
|
||||
<div
|
||||
className="modal-back"
|
||||
>
|
||||
<span
|
||||
onClick={[MockFunction]}
|
||||
>
|
||||
<BackIcon />
|
||||
</span>
|
||||
</div>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="General Settings"
|
||||
id="general_tab.title"
|
||||
/>
|
||||
</h4>
|
||||
</div>
|
||||
<div
|
||||
className="user-settings"
|
||||
>
|
||||
<div
|
||||
className="GeneralTab__header"
|
||||
>
|
||||
<h3>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="General Settings"
|
||||
id="general_tab.title"
|
||||
/>
|
||||
</h3>
|
||||
<LearnAboutTeamsLink />
|
||||
</div>
|
||||
<div
|
||||
className="divider-dark first"
|
||||
/>
|
||||
<SettingItemMin
|
||||
describe="TestTeam"
|
||||
section="name"
|
||||
title="Team Name"
|
||||
updateSection={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="divider-light"
|
||||
/>
|
||||
<SettingItemMin
|
||||
describe="The Test Team"
|
||||
section="description"
|
||||
title="Team Description"
|
||||
updateSection={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="divider-light"
|
||||
/>
|
||||
<SettingPicture
|
||||
clientError=""
|
||||
file={null}
|
||||
helpText={
|
||||
<Memo(MemoizedFormattedMessage)
|
||||
defaultMessage="Upload a team icon in BMP, JPG or PNG format.\\\\nSquare images with a solid background color are recommended."
|
||||
id="setting_picture.help.team"
|
||||
/>
|
||||
}
|
||||
imageContext="team"
|
||||
loadingPicture={false}
|
||||
onFileChange={[Function]}
|
||||
onRemove={[Function]}
|
||||
onSubmit={[Function]}
|
||||
serverError=""
|
||||
src={null}
|
||||
submitActive={false}
|
||||
title="Team Icon"
|
||||
updateSection={[Function]}
|
||||
/>
|
||||
<div
|
||||
className="divider-light"
|
||||
/>
|
||||
<OpenInvite
|
||||
allowOpenInvite={false}
|
||||
isActive={false}
|
||||
isGroupConstrained={true}
|
||||
onToggle={[Function]}
|
||||
patchTeam={[MockFunction]}
|
||||
teamId="team_id"
|
||||
/>
|
||||
<div
|
||||
className="divider-dark"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -1,60 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
|
||||
import OpenInvite from './open_invite';
|
||||
|
||||
describe('components/TeamSettings/OpenInvite', () => {
|
||||
const patchTeam = jest.fn().mockReturnValue({data: true});
|
||||
const onToggle = jest.fn().mockReturnValue({data: true});
|
||||
const defaultProps = {
|
||||
teamId: 'team_id',
|
||||
isActive: false,
|
||||
isGroupConstrained: false,
|
||||
allowOpenInvite: false,
|
||||
patchTeam,
|
||||
onToggle,
|
||||
};
|
||||
|
||||
test('should match snapshot on non active without groupConstrained', () => {
|
||||
const props = {...defaultProps};
|
||||
|
||||
const wrapper = shallow(<OpenInvite {...props}/>);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot on non active allowing open invite', () => {
|
||||
const props = {...defaultProps, allowOpenInvite: true};
|
||||
|
||||
const wrapper = shallow(<OpenInvite {...props}/>);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot on non active with groupConstrained', () => {
|
||||
const props = {...defaultProps, isGroupConstrained: true};
|
||||
|
||||
const wrapper = shallow(<OpenInvite {...props}/>);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot on active without groupConstrained', () => {
|
||||
const props = {...defaultProps, isActive: true};
|
||||
|
||||
const wrapper = shallow(<OpenInvite {...props}/>);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot on active with groupConstrained', () => {
|
||||
const props = {...defaultProps, isActive: true, isGroupConstrained: true};
|
||||
|
||||
const wrapper = shallow(<OpenInvite {...props}/>);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
@ -1,160 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useState} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
|
||||
import ExternalLink from 'components/external_link';
|
||||
import SettingItemMax from 'components/setting_item_max';
|
||||
import SettingItemMin from 'components/setting_item_min';
|
||||
|
||||
type Props = {
|
||||
teamId: string;
|
||||
isActive: boolean;
|
||||
isGroupConstrained?: boolean;
|
||||
allowOpenInvite?: boolean;
|
||||
onToggle: (active: boolean) => void;
|
||||
patchTeam: (patch: Partial<Team> & {id: string}) => Promise<ActionResult>;
|
||||
};
|
||||
|
||||
const OpenInvite = (props: Props) => {
|
||||
const {teamId, isActive, isGroupConstrained, onToggle, patchTeam} = props;
|
||||
const intl = useIntl();
|
||||
const [serverError, setServerError] = useState('');
|
||||
const [allowOpenInvite, setAllowOpenInvite] = useState(props.allowOpenInvite);
|
||||
|
||||
const submit = useCallback(() => {
|
||||
setServerError('');
|
||||
const data = {
|
||||
id: teamId,
|
||||
allow_open_invite: allowOpenInvite,
|
||||
};
|
||||
|
||||
patchTeam(data).then(({error}) => {
|
||||
if (error) {
|
||||
setServerError(error.message);
|
||||
} else {
|
||||
onToggle(false);
|
||||
}
|
||||
});
|
||||
}, [onToggle, patchTeam, teamId, allowOpenInvite]);
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
if (isActive) {
|
||||
onToggle(false);
|
||||
} else {
|
||||
onToggle(true);
|
||||
setAllowOpenInvite(props.allowOpenInvite);
|
||||
}
|
||||
}, [isActive, onToggle]);
|
||||
|
||||
if (!isActive) {
|
||||
let describe = '';
|
||||
if (props.allowOpenInvite) {
|
||||
describe = intl.formatMessage({id: 'general_tab.yes', defaultMessage: 'Yes'});
|
||||
} else if (isGroupConstrained) {
|
||||
describe = intl.formatMessage({id: 'team_settings.openInviteSetting.groupConstrained', defaultMessage: 'No, members of this team are added and removed by linked groups.'});
|
||||
} else {
|
||||
describe = intl.formatMessage({id: 'general_tab.no', defaultMessage: 'No'});
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingItemMin
|
||||
title={intl.formatMessage({id: 'general_tab.openInviteTitle', defaultMessage: 'Allow any user with an account on this server to join this team'})}
|
||||
describe={describe}
|
||||
updateSection={handleToggle}
|
||||
section={'open_invite'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let inputs;
|
||||
|
||||
if (isGroupConstrained) {
|
||||
inputs = [
|
||||
<div key='userOpenInviteOptions'>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id='team_settings.openInviteDescription.groupConstrained'
|
||||
defaultMessage='No, members of this team are added and removed by linked groups. <link>Learn More</link>'
|
||||
values={{
|
||||
link: (msg: React.ReactNode) => (
|
||||
<ExternalLink
|
||||
href='https://mattermost.com/pl/default-ldap-group-constrained-team-channel.html'
|
||||
location='open_invite'
|
||||
>
|
||||
{msg}
|
||||
</ExternalLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
];
|
||||
} else {
|
||||
inputs = [
|
||||
<fieldset key='userOpenInviteOptions'>
|
||||
<legend className='form-legend hidden-label'>
|
||||
<FormattedMessage
|
||||
id='team_settings.openInviteDescription.ariaLabel'
|
||||
defaultMessage='Invite Code'
|
||||
/>
|
||||
</legend>
|
||||
<div className='radio'>
|
||||
<label>
|
||||
<input
|
||||
id='teamOpenInvite'
|
||||
name='userOpenInviteOptions'
|
||||
type='radio'
|
||||
defaultChecked={allowOpenInvite}
|
||||
onChange={() => setAllowOpenInvite(true)}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='general_tab.yes'
|
||||
defaultMessage='Yes'
|
||||
/>
|
||||
</label>
|
||||
<br/>
|
||||
</div>
|
||||
<div className='radio'>
|
||||
<label>
|
||||
<input
|
||||
id='teamOpenInviteNo'
|
||||
name='userOpenInviteOptions'
|
||||
type='radio'
|
||||
defaultChecked={!allowOpenInvite}
|
||||
onChange={() => setAllowOpenInvite(false)}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='general_tab.no'
|
||||
defaultMessage='No'
|
||||
/>
|
||||
</label>
|
||||
<br/>
|
||||
</div>
|
||||
<div className='mt-5'>
|
||||
<FormattedMessage
|
||||
id='general_tab.openInviteDesc'
|
||||
defaultMessage='When allowed, a link to this team will be included on the landing page allowing anyone with an account to join this team. Changing from "Yes" to "No" will regenerate the invitation code, create a new invitation link and invalidate the previous link.'
|
||||
/>
|
||||
</div>
|
||||
</fieldset>,
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingItemMax
|
||||
title={intl.formatMessage({id: 'general_tab.openInviteTitle', defaultMessage: 'Allow any user with an account on this server to join this team'})}
|
||||
inputs={inputs}
|
||||
submit={submit}
|
||||
serverError={serverError}
|
||||
updateSection={handleToggle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default OpenInvite;
|
@ -1,17 +0,0 @@
|
||||
.GeneralTab__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 20px 0;
|
||||
|
||||
>h3 {
|
||||
margin: 0;
|
||||
|
||||
span {
|
||||
display: inline-flex;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 28px;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,213 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
import type {ChangeEvent, ComponentProps} from 'react';
|
||||
|
||||
import {GeneralTab} from 'components/team_general_tab/team_general_tab';
|
||||
|
||||
import {type MockIntl} from 'tests/helpers/intl-test-helper';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
describe('components/TeamSettings', () => {
|
||||
const getTeam = jest.fn().mockResolvedValue({data: true});
|
||||
const patchTeam = jest.fn().mockReturnValue({data: true});
|
||||
const regenerateTeamInviteId = jest.fn().mockReturnValue({data: true});
|
||||
const removeTeamIcon = jest.fn().mockReturnValue({data: true});
|
||||
const setTeamIcon = jest.fn().mockReturnValue({data: true});
|
||||
const baseActions = {
|
||||
getTeam,
|
||||
patchTeam,
|
||||
regenerateTeamInviteId,
|
||||
removeTeamIcon,
|
||||
setTeamIcon,
|
||||
};
|
||||
const defaultProps: ComponentProps<typeof GeneralTab> = {
|
||||
team: TestHelper.getTeamMock({id: 'team_id'}),
|
||||
maxFileSize: 50,
|
||||
activeSection: 'team_icon',
|
||||
intl: {
|
||||
formatMessage: jest.fn(),
|
||||
} as MockIntl,
|
||||
updateSection: jest.fn(),
|
||||
closeModal: jest.fn(),
|
||||
collapseModal: jest.fn(),
|
||||
actions: baseActions,
|
||||
canInviteTeamMembers: true,
|
||||
isMobileView: false,
|
||||
};
|
||||
|
||||
test('should handle bad updateTeamIcon function call', () => {
|
||||
const wrapper = shallow<GeneralTab>(<GeneralTab {...defaultProps}/>);
|
||||
|
||||
wrapper.instance().updateTeamIcon(null as unknown as ChangeEvent<HTMLInputElement>);
|
||||
|
||||
expect(wrapper.state('clientError')).toEqual('An error occurred while selecting the image.');
|
||||
});
|
||||
|
||||
test('should handle invalid file selection', () => {
|
||||
const wrapper = shallow<GeneralTab>(<GeneralTab {...defaultProps}/>);
|
||||
|
||||
wrapper.instance().updateTeamIcon({
|
||||
target: {
|
||||
files: [{
|
||||
type: 'text/plain',
|
||||
}],
|
||||
},
|
||||
} as unknown as ChangeEvent<HTMLInputElement>);
|
||||
|
||||
expect(wrapper.state('clientError')).toEqual('Only BMP, JPG or PNG images may be used for team icons');
|
||||
});
|
||||
|
||||
test('should handle too large files', () => {
|
||||
const wrapper = shallow<GeneralTab>(<GeneralTab {...defaultProps}/>);
|
||||
|
||||
wrapper.instance().updateTeamIcon({
|
||||
target: {
|
||||
files: [{
|
||||
type: 'image/jpeg',
|
||||
size: defaultProps.maxFileSize + 1,
|
||||
}],
|
||||
},
|
||||
} as unknown as ChangeEvent<HTMLInputElement>);
|
||||
|
||||
expect(wrapper.state('clientError')).toEqual('Unable to upload team icon. File is too large.');
|
||||
});
|
||||
|
||||
test('should call actions.setTeamIcon on handleTeamIconSubmit', () => {
|
||||
const actions = {...baseActions};
|
||||
const props = {...defaultProps, actions};
|
||||
const wrapper = shallow<GeneralTab>(<GeneralTab {...props}/>);
|
||||
|
||||
let teamIconFile = null;
|
||||
wrapper.setState({teamIconFile, submitActive: true});
|
||||
wrapper.instance().handleTeamIconSubmit();
|
||||
expect(actions.setTeamIcon).not.toHaveBeenCalled();
|
||||
|
||||
teamIconFile = {file: 'team_icon_file'} as unknown as File;
|
||||
wrapper.setState({teamIconFile, submitActive: false});
|
||||
wrapper.instance().handleTeamIconSubmit();
|
||||
expect(actions.setTeamIcon).not.toHaveBeenCalled();
|
||||
|
||||
wrapper.setState({teamIconFile, submitActive: true});
|
||||
wrapper.instance().handleTeamIconSubmit();
|
||||
|
||||
expect(actions.setTeamIcon).toHaveBeenCalledTimes(1);
|
||||
expect(actions.setTeamIcon).toHaveBeenCalledWith(props.team?.id, teamIconFile);
|
||||
});
|
||||
|
||||
test('should call actions.removeTeamIcon on handleTeamIconRemove', () => {
|
||||
const actions = {...baseActions};
|
||||
const props = {...defaultProps, actions};
|
||||
const wrapper = shallow<GeneralTab>(<GeneralTab {...props}/>);
|
||||
|
||||
wrapper.instance().handleTeamIconRemove();
|
||||
|
||||
expect(actions.removeTeamIcon).toHaveBeenCalledTimes(1);
|
||||
expect(actions.removeTeamIcon).toHaveBeenCalledWith(props.team?.id);
|
||||
});
|
||||
|
||||
test('hide invite code if no permissions for team inviting', () => {
|
||||
const props = {...defaultProps, canInviteTeamMembers: false};
|
||||
|
||||
const wrapper1 = shallow(<GeneralTab {...defaultProps}/>);
|
||||
const wrapper2 = shallow(<GeneralTab {...props}/>);
|
||||
|
||||
expect(wrapper1).toMatchSnapshot();
|
||||
expect(wrapper2).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should call actions.patchTeam on handleAllowedDomainsSubmit', () => {
|
||||
const actions = {...baseActions};
|
||||
const props = {...defaultProps, actions};
|
||||
const wrapper = shallow<GeneralTab>(<GeneralTab {...props}/>);
|
||||
|
||||
wrapper.instance().handleAllowedDomainsSubmit();
|
||||
|
||||
expect(actions.patchTeam).toHaveBeenCalledTimes(1);
|
||||
expect(actions.patchTeam).toHaveBeenCalledWith({
|
||||
allowed_domains: '',
|
||||
id: props.team?.id,
|
||||
});
|
||||
});
|
||||
|
||||
test('should call actions.patchTeam on handleNameSubmit', () => {
|
||||
const actions = {...baseActions};
|
||||
const props = {...defaultProps, actions};
|
||||
if (props.team) {
|
||||
props.team.display_name = 'TestTeam';
|
||||
}
|
||||
|
||||
const wrapper = shallow<GeneralTab>(<GeneralTab {...props}/>);
|
||||
|
||||
wrapper.instance().handleNameSubmit();
|
||||
|
||||
expect(actions.patchTeam).toHaveBeenCalledTimes(1);
|
||||
expect(actions.patchTeam).toHaveBeenCalledWith({
|
||||
display_name: props.team?.display_name,
|
||||
id: props.team?.id,
|
||||
});
|
||||
});
|
||||
|
||||
test('should call actions.patchTeam on handleInviteIdSubmit', () => {
|
||||
const actions = {...baseActions};
|
||||
const props = {...defaultProps, actions};
|
||||
if (props.team) {
|
||||
props.team.invite_id = '12345';
|
||||
}
|
||||
|
||||
const wrapper = shallow<GeneralTab>(<GeneralTab {...props}/>);
|
||||
|
||||
wrapper.instance().handleInviteIdSubmit();
|
||||
|
||||
expect(actions.regenerateTeamInviteId).toHaveBeenCalledTimes(1);
|
||||
expect(actions.regenerateTeamInviteId).toHaveBeenCalledWith(props.team?.id);
|
||||
});
|
||||
|
||||
test('should call actions.patchTeam on handleDescriptionSubmit', () => {
|
||||
const actions = {...baseActions};
|
||||
const props = {...defaultProps, actions};
|
||||
|
||||
const wrapper = shallow<GeneralTab>(<GeneralTab {...props}/>);
|
||||
|
||||
const newDescription = 'The Test Team';
|
||||
wrapper.setState({description: newDescription});
|
||||
wrapper.instance().handleDescriptionSubmit();
|
||||
if (props.team) {
|
||||
props.team.description = newDescription;
|
||||
}
|
||||
|
||||
expect(actions.patchTeam).toHaveBeenCalledTimes(1);
|
||||
expect(actions.patchTeam).toHaveBeenCalledWith({
|
||||
description: newDescription,
|
||||
id: props.team?.id,
|
||||
});
|
||||
});
|
||||
|
||||
test('should match snapshot when team is group constrained', () => {
|
||||
const props = {...defaultProps};
|
||||
if (props.team) {
|
||||
props.team.group_constrained = true;
|
||||
}
|
||||
|
||||
const wrapper = shallow(<GeneralTab {...props}/>);
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should call actions.getTeam on handleUpdateSection if invite_id is empty', () => {
|
||||
const actions = {...baseActions};
|
||||
const props = {...defaultProps, actions};
|
||||
if (props.team) {
|
||||
props.team.invite_id = '';
|
||||
}
|
||||
|
||||
const wrapper = shallow<GeneralTab>(<GeneralTab {...props}/>);
|
||||
|
||||
wrapper.instance().handleUpdateSection('invite_id');
|
||||
|
||||
expect(actions.getTeam).toHaveBeenCalledTimes(1);
|
||||
expect(actions.getTeam).toHaveBeenCalledWith(props.team?.id);
|
||||
});
|
||||
});
|
@ -1,702 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import type {ChangeEvent, MouseEvent, ReactNode} from 'react';
|
||||
import {FormattedMessage, FormattedDate, injectIntl, type WrappedComponentProps} from 'react-intl';
|
||||
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
|
||||
import LearnAboutTeamsLink from 'components/main_menu/learn_about_teams_link';
|
||||
import SettingItemMax from 'components/setting_item_max';
|
||||
import SettingItemMin from 'components/setting_item_min';
|
||||
import SettingPicture from 'components/setting_picture';
|
||||
import BackIcon from 'components/widgets/icons/fa_back_icon';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import {imageURLForTeam, localizeMessage, moveCursorToEnd} from 'utils/utils';
|
||||
|
||||
import OpenInvite from './open_invite';
|
||||
|
||||
import type {PropsFromRedux, OwnProps} from '.';
|
||||
import './team_general_tab.scss';
|
||||
|
||||
const ACCEPTED_TEAM_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/bmp'];
|
||||
|
||||
type Props = PropsFromRedux & OwnProps & WrappedComponentProps;
|
||||
|
||||
type State = {
|
||||
name?: Team['display_name'];
|
||||
invite_id?: Team['invite_id'];
|
||||
description?: Team['description'];
|
||||
allowed_domains?: Team['allowed_domains'];
|
||||
serverError: ReactNode;
|
||||
clientError: ReactNode;
|
||||
teamIconFile: File | null;
|
||||
loadingIcon: boolean;
|
||||
submitActive: boolean;
|
||||
isInitialState: boolean;
|
||||
shouldFetchTeam?: boolean;
|
||||
}
|
||||
|
||||
export class GeneralTab extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = this.setupInitialState(props);
|
||||
}
|
||||
|
||||
updateSection = (section: string) => {
|
||||
this.setState(this.setupInitialState(this.props));
|
||||
this.props.updateSection(section);
|
||||
};
|
||||
|
||||
setupInitialState(props: Props) {
|
||||
const team = props.team;
|
||||
|
||||
return {
|
||||
name: team?.display_name,
|
||||
invite_id: team?.invite_id,
|
||||
description: team?.description,
|
||||
allowed_domains: team?.allowed_domains,
|
||||
serverError: '',
|
||||
clientError: '',
|
||||
teamIconFile: null,
|
||||
loadingIcon: false,
|
||||
submitActive: false,
|
||||
isInitialState: true,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps: Props, prevState: State) {
|
||||
const {team} = nextProps;
|
||||
if (!prevState.isInitialState) {
|
||||
return {
|
||||
name: team?.display_name,
|
||||
description: team?.description,
|
||||
allowed_domains: team?.allowed_domains,
|
||||
invite_id: team?.invite_id,
|
||||
isInitialState: false,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
if (!prevState.shouldFetchTeam && this.state.shouldFetchTeam) {
|
||||
this.fetchTeam();
|
||||
}
|
||||
}
|
||||
|
||||
fetchTeam() {
|
||||
if (this.state.serverError) {
|
||||
return;
|
||||
}
|
||||
if (this.props.team) {
|
||||
this.props.actions.getTeam(this.props.team.id).then(({error}) => {
|
||||
const state = {
|
||||
shouldFetchTeam: false,
|
||||
serverError: '',
|
||||
};
|
||||
if (error) {
|
||||
state.serverError = error.message;
|
||||
}
|
||||
this.setState(state);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleAllowedDomainsSubmit = async () => {
|
||||
const state = {serverError: '', clientError: ''};
|
||||
|
||||
const data = {
|
||||
id: this.props.team?.id,
|
||||
allowed_domains: this.state.allowed_domains,
|
||||
};
|
||||
const {error} = await this.props.actions.patchTeam(data);
|
||||
|
||||
if (error) {
|
||||
state.serverError = error.message;
|
||||
this.setState(state);
|
||||
} else {
|
||||
this.updateSection('');
|
||||
}
|
||||
};
|
||||
|
||||
handleNameSubmit = async () => {
|
||||
const state: Pick<State, 'serverError' | 'clientError'> = {serverError: '', clientError: ''};
|
||||
let valid = true;
|
||||
|
||||
const name = this.state.name?.trim();
|
||||
|
||||
if (!name) {
|
||||
state.clientError = localizeMessage('general_tab.required', 'This field is required');
|
||||
valid = false;
|
||||
} else if (name.length < Constants.MIN_TEAMNAME_LENGTH) {
|
||||
state.clientError = (
|
||||
<FormattedMessage
|
||||
id='general_tab.teamNameRestrictions'
|
||||
defaultMessage='Team Name must be {min} or more characters up to a maximum of {max}. You can add a longer team description.'
|
||||
values={{
|
||||
min: Constants.MIN_TEAMNAME_LENGTH,
|
||||
max: Constants.MAX_TEAMNAME_LENGTH,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
valid = false;
|
||||
} else {
|
||||
state.clientError = '';
|
||||
}
|
||||
|
||||
this.setState(state);
|
||||
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
id: this.props.team?.id,
|
||||
display_name: this.state.name,
|
||||
};
|
||||
const {error} = await this.props.actions.patchTeam(data);
|
||||
|
||||
if (error) {
|
||||
state.serverError = error.message;
|
||||
this.setState(state);
|
||||
} else {
|
||||
this.updateSection('');
|
||||
}
|
||||
};
|
||||
|
||||
handleInviteIdSubmit = async () => {
|
||||
const state = {serverError: '', clientError: ''};
|
||||
this.setState(state);
|
||||
|
||||
const {error} = await this.props.actions.regenerateTeamInviteId(this.props.team?.id || '');
|
||||
|
||||
if (error) {
|
||||
state.serverError = error.message;
|
||||
this.setState(state);
|
||||
} else {
|
||||
this.updateSection('');
|
||||
}
|
||||
};
|
||||
|
||||
handleClose = () => this.updateSection('');
|
||||
|
||||
handleDescriptionSubmit = async () => {
|
||||
const state = {serverError: '', clientError: ''};
|
||||
let valid = true;
|
||||
|
||||
const description = this.state.description?.trim();
|
||||
if (description === this.props.team?.description) {
|
||||
state.clientError = localizeMessage('general_tab.chooseDescription', 'Please choose a new description for your team');
|
||||
valid = false;
|
||||
} else {
|
||||
state.clientError = '';
|
||||
}
|
||||
|
||||
this.setState(state);
|
||||
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
id: this.props.team?.id,
|
||||
description: this.state.description,
|
||||
};
|
||||
const {error} = await this.props.actions.patchTeam(data);
|
||||
|
||||
if (error) {
|
||||
state.serverError = error.message;
|
||||
this.setState(state);
|
||||
} else {
|
||||
this.updateSection('');
|
||||
}
|
||||
};
|
||||
|
||||
handleTeamIconSubmit = async () => {
|
||||
if (!this.state.teamIconFile || !this.state.submitActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loadingIcon: true,
|
||||
clientError: '',
|
||||
serverError: '',
|
||||
});
|
||||
|
||||
const {error} = await this.props.actions.setTeamIcon(this.props.team?.id || '', this.state.teamIconFile);
|
||||
|
||||
if (error) {
|
||||
this.setState({
|
||||
loadingIcon: false,
|
||||
serverError: error.message,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
loadingIcon: false,
|
||||
submitActive: false,
|
||||
});
|
||||
this.updateSection('');
|
||||
}
|
||||
};
|
||||
|
||||
handleTeamIconRemove = async () => {
|
||||
this.setState({
|
||||
loadingIcon: true,
|
||||
clientError: '',
|
||||
serverError: '',
|
||||
});
|
||||
|
||||
const {error} = await this.props.actions.removeTeamIcon(this.props.team?.id || '');
|
||||
|
||||
if (error) {
|
||||
this.setState({
|
||||
loadingIcon: false,
|
||||
serverError: error.message,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
loadingIcon: false,
|
||||
submitActive: false,
|
||||
});
|
||||
this.updateSection('');
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
document.getElementById('team_settings')?.addEventListener('hidden.bs.modal', this.handleClose);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.getElementById('team_settings')?.removeEventListener('hidden.bs.modal', this.handleClose);
|
||||
}
|
||||
|
||||
onOpenInviteToggle = (active: boolean) => this.handleUpdateSection(active ? 'open_invite' : '');
|
||||
|
||||
handleUpdateSection = (section: string) => {
|
||||
if (section === 'invite_id' && this.props.activeSection !== section && !this.props.team?.invite_id) {
|
||||
this.setState({shouldFetchTeam: true}, () => {
|
||||
this.updateSection(section);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateSection(section);
|
||||
};
|
||||
|
||||
updateName = (e: ChangeEvent<HTMLInputElement>) => this.setState({name: e.target.value});
|
||||
|
||||
updateDescription = (e: ChangeEvent<HTMLInputElement>) => this.setState({description: e.target.value});
|
||||
|
||||
updateTeamIcon = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (e && e.target && e.target.files && e.target.files[0]) {
|
||||
const file = e.target.files[0];
|
||||
|
||||
if (!ACCEPTED_TEAM_IMAGE_TYPES.includes(file.type)) {
|
||||
this.setState({
|
||||
clientError: localizeMessage('general_tab.teamIconInvalidFileType', 'Only BMP, JPG or PNG images may be used for team icons'),
|
||||
});
|
||||
} else if (file.size > this.props.maxFileSize) {
|
||||
this.setState({
|
||||
clientError: localizeMessage('general_tab.teamIconTooLarge', 'Unable to upload team icon. File is too large.'),
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
teamIconFile: e.target.files[0],
|
||||
clientError: '',
|
||||
submitActive: true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.setState({
|
||||
teamIconFile: null,
|
||||
clientError: localizeMessage('general_tab.teamIconError', 'An error occurred while selecting the image.'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
updateAllowedDomains = (e: ChangeEvent<HTMLInputElement>) => this.setState({allowed_domains: e.target.value});
|
||||
|
||||
render() {
|
||||
const team = this.props.team;
|
||||
|
||||
const clientError = this.state.clientError;
|
||||
const serverError = this.state.serverError ?? null;
|
||||
|
||||
let inviteSection;
|
||||
|
||||
if (this.props.activeSection === 'invite_id' && this.props.canInviteTeamMembers) {
|
||||
const inputs = [];
|
||||
|
||||
inputs.push(
|
||||
<div key='teamInviteSetting'>
|
||||
<div className='row'>
|
||||
<label className='col-sm-5 control-label visible-xs-block'/>
|
||||
<div className='col-sm-12'>
|
||||
<input
|
||||
id='teamInviteId'
|
||||
autoFocus={true}
|
||||
className='form-control'
|
||||
type='text'
|
||||
value={this.state.invite_id}
|
||||
maxLength={32}
|
||||
onFocus={moveCursorToEnd}
|
||||
readOnly={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='setting-list__hint'>
|
||||
<FormattedMessage
|
||||
id='general_tab.codeLongDesc'
|
||||
defaultMessage='The Invite Code is part of the unique team invitation link which is sent to members you’re inviting to this team. Regenerating the code creates a new invitation link and invalidates the previous link.'
|
||||
values={{
|
||||
getTeamInviteLink: (
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id='general_tab.getTeamInviteLink'
|
||||
defaultMessage='Get Team Invite Link'
|
||||
/>
|
||||
</strong>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
|
||||
inviteSection = (
|
||||
<SettingItemMax
|
||||
title={localizeMessage('general_tab.codeTitle', 'Invite Code')}
|
||||
inputs={inputs}
|
||||
submit={this.handleInviteIdSubmit}
|
||||
serverError={serverError}
|
||||
clientError={clientError}
|
||||
updateSection={this.handleUpdateSection}
|
||||
saveButtonText={localizeMessage('general_tab.regenerate', 'Regenerate')}
|
||||
/>
|
||||
);
|
||||
} else if (this.props.canInviteTeamMembers) {
|
||||
inviteSection = (
|
||||
<SettingItemMin
|
||||
title={localizeMessage('general_tab.codeTitle', 'Invite Code')}
|
||||
describe={localizeMessage('general_tab.codeDesc', "Click 'Edit' to regenerate Invite Code.")}
|
||||
updateSection={this.handleUpdateSection}
|
||||
section={'invite_id'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let nameSection;
|
||||
|
||||
if (this.props.activeSection === 'name') {
|
||||
const inputs = [];
|
||||
|
||||
const teamNameLabel = this.props.isMobileView ? '' : (
|
||||
<FormattedMessage
|
||||
id='general_tab.teamName'
|
||||
defaultMessage='Team Name'
|
||||
/>
|
||||
);
|
||||
|
||||
inputs.push(
|
||||
<div
|
||||
key='teamNameSetting'
|
||||
className='form-group'
|
||||
>
|
||||
<label className='col-sm-5 control-label'>{teamNameLabel}</label>
|
||||
<div className='col-sm-7'>
|
||||
<input
|
||||
id='teamName'
|
||||
autoFocus={true}
|
||||
className='form-control'
|
||||
type='text'
|
||||
maxLength={Constants.MAX_TEAMNAME_LENGTH}
|
||||
onChange={this.updateName}
|
||||
value={this.state.name}
|
||||
onFocus={moveCursorToEnd}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
|
||||
const nameExtraInfo = <span>{localizeMessage('general_tab.teamNameInfo', 'Set the name of the team as it appears on your sign-in screen and at the top of the left-hand sidebar.')}</span>;
|
||||
|
||||
nameSection = (
|
||||
<SettingItemMax
|
||||
title={localizeMessage('general_tab.teamName', 'Team Name')}
|
||||
inputs={inputs}
|
||||
submit={this.handleNameSubmit}
|
||||
serverError={serverError}
|
||||
clientError={clientError}
|
||||
updateSection={this.handleUpdateSection}
|
||||
extraInfo={nameExtraInfo}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
const describe = this.state.name;
|
||||
|
||||
nameSection = (
|
||||
<SettingItemMin
|
||||
title={localizeMessage('general_tab.teamName', 'Team Name')}
|
||||
describe={describe}
|
||||
updateSection={this.handleUpdateSection}
|
||||
section={'name'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let descriptionSection;
|
||||
|
||||
if (this.props.activeSection === 'description') {
|
||||
const inputs = [];
|
||||
|
||||
const teamDescriptionLabel = this.props.isMobileView ? '' : (
|
||||
<FormattedMessage
|
||||
id='general_tab.teamDescription'
|
||||
defaultMessage='Team Description'
|
||||
/>
|
||||
);
|
||||
|
||||
inputs.push(
|
||||
<div
|
||||
key='teamDescriptionSetting'
|
||||
className='form-group'
|
||||
>
|
||||
<label className='col-sm-5 control-label'>{teamDescriptionLabel}</label>
|
||||
<div className='col-sm-7'>
|
||||
<input
|
||||
id='teamDescription'
|
||||
autoFocus={true}
|
||||
className='form-control'
|
||||
type='text'
|
||||
maxLength={Constants.MAX_TEAMDESCRIPTION_LENGTH}
|
||||
onChange={this.updateDescription}
|
||||
value={this.state.description}
|
||||
onFocus={moveCursorToEnd}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
|
||||
const descriptionExtraInfo = <span>{localizeMessage('general_tab.teamDescriptionInfo', 'Team description provides additional information to help users select the right team. Maximum of 50 characters.')}</span>;
|
||||
|
||||
descriptionSection = (
|
||||
<SettingItemMax
|
||||
title={localizeMessage('general_tab.teamDescription', 'Team Description')}
|
||||
inputs={inputs}
|
||||
submit={this.handleDescriptionSubmit}
|
||||
serverError={serverError}
|
||||
clientError={clientError}
|
||||
updateSection={this.handleUpdateSection}
|
||||
extraInfo={descriptionExtraInfo}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
const describeMsg = this.state.description ?? (
|
||||
<FormattedMessage
|
||||
id='general_tab.emptyDescription'
|
||||
defaultMessage="Click 'Edit' to add a team description."
|
||||
/>
|
||||
);
|
||||
|
||||
descriptionSection = (
|
||||
<SettingItemMin
|
||||
title={localizeMessage('general_tab.teamDescription', 'Team Description')}
|
||||
describe={describeMsg}
|
||||
updateSection={this.handleUpdateSection}
|
||||
section={'description'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let teamIconSection;
|
||||
if (this.props.activeSection === 'team_icon') {
|
||||
const helpText = (
|
||||
<FormattedMessage
|
||||
id='setting_picture.help.team'
|
||||
defaultMessage='Upload a team icon in BMP, JPG or PNG format.\nSquare images with a solid background color are recommended.'
|
||||
/>
|
||||
);
|
||||
teamIconSection = (
|
||||
<SettingPicture
|
||||
imageContext='team'
|
||||
title={localizeMessage('general_tab.teamIcon', 'Team Icon')}
|
||||
src={imageURLForTeam(team || {} as Team)}
|
||||
file={this.state.teamIconFile}
|
||||
serverError={this.state.serverError}
|
||||
clientError={this.state.clientError}
|
||||
loadingPicture={this.state.loadingIcon}
|
||||
submitActive={this.state.submitActive}
|
||||
updateSection={(e: MouseEvent<HTMLButtonElement>) => {
|
||||
this.updateSection('');
|
||||
e.preventDefault();
|
||||
}}
|
||||
onFileChange={this.updateTeamIcon}
|
||||
onSubmit={this.handleTeamIconSubmit}
|
||||
onRemove={this.handleTeamIconRemove}
|
||||
helpText={helpText}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
let minMessage;
|
||||
|
||||
if (team?.last_team_icon_update) {
|
||||
minMessage = (
|
||||
<FormattedMessage
|
||||
id='general_tab.teamIconLastUpdated'
|
||||
defaultMessage='Image last updated {date}'
|
||||
values={{
|
||||
date: (
|
||||
<FormattedDate
|
||||
value={new Date(team.last_team_icon_update)}
|
||||
day='2-digit'
|
||||
month='short'
|
||||
year='numeric'
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
minMessage = this.props.isMobileView ? localizeMessage('general_tab.teamIconEditHintMobile', 'Click to upload an image') : localizeMessage('general_tab.teamIconEditHint', 'Click \'Edit\' to upload an image.');
|
||||
}
|
||||
|
||||
teamIconSection = (
|
||||
<SettingItemMin
|
||||
title={localizeMessage('general_tab.teamIcon', 'Team Icon')}
|
||||
describe={minMessage}
|
||||
section={'team_icon'}
|
||||
updateSection={this.handleUpdateSection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let allowedDomainsSection;
|
||||
|
||||
if (this.props.activeSection === 'allowed_domains') {
|
||||
const inputs = [];
|
||||
|
||||
inputs.push(
|
||||
<div
|
||||
key='allowedDomainsSetting'
|
||||
className='form-group'
|
||||
>
|
||||
<div className='col-sm-12'>
|
||||
<input
|
||||
id='allowedDomains'
|
||||
autoFocus={true}
|
||||
className='form-control'
|
||||
type='text'
|
||||
onChange={this.updateAllowedDomains}
|
||||
value={this.state.allowed_domains}
|
||||
onFocus={moveCursorToEnd}
|
||||
placeholder={this.props.intl.formatMessage({id: 'general_tab.AllowedDomainsExample', defaultMessage: 'corp.mattermost.com, mattermost.com'})}
|
||||
aria-label={localizeMessage('general_tab.allowedDomains.ariaLabel', 'Allowed Domains')}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
);
|
||||
|
||||
const allowedDomainsInfo = <span>{localizeMessage('general_tab.AllowedDomainsInfo', 'Users can only join the team if their email matches a specific domain (e.g. "mattermost.com") or list of comma-separated domains (e.g. "corp.mattermost.com, mattermost.com").')}</span>;
|
||||
|
||||
allowedDomainsSection = (
|
||||
<SettingItemMax
|
||||
title={localizeMessage('general_tab.allowedDomains', 'Allow only users with a specific email domain to join this team')}
|
||||
inputs={inputs}
|
||||
submit={this.handleAllowedDomainsSubmit}
|
||||
serverError={serverError}
|
||||
clientError={clientError}
|
||||
updateSection={this.handleUpdateSection}
|
||||
extraInfo={allowedDomainsInfo}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
const describeMsg = this.state.allowed_domains ?? (
|
||||
<FormattedMessage
|
||||
id='general_tab.allowedDomainsEdit'
|
||||
defaultMessage="Click 'Edit' to add an email domain whitelist."
|
||||
/>
|
||||
);
|
||||
allowedDomainsSection = (
|
||||
<SettingItemMin
|
||||
title={localizeMessage('general_tab.allowedDomains', 'allowedDomains')}
|
||||
describe={describeMsg}
|
||||
updateSection={this.handleUpdateSection}
|
||||
section={'allowed_domains'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='modal-header'>
|
||||
<button
|
||||
id='closeButton'
|
||||
type='button'
|
||||
className='close'
|
||||
data-dismiss='modal'
|
||||
aria-label='Close'
|
||||
onClick={this.props.closeModal}
|
||||
>
|
||||
<span aria-hidden='true'>{'×'}</span>
|
||||
</button>
|
||||
<h4 className='modal-title'>
|
||||
<div className='modal-back'>
|
||||
<span onClick={this.props.collapseModal}>
|
||||
<BackIcon/>
|
||||
</span>
|
||||
</div>
|
||||
<FormattedMessage
|
||||
id='general_tab.title'
|
||||
defaultMessage='General Settings'
|
||||
/>
|
||||
</h4>
|
||||
</div>
|
||||
<div className='user-settings'>
|
||||
<div className='GeneralTab__header'>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='general_tab.title'
|
||||
defaultMessage='General Settings'
|
||||
/>
|
||||
</h3>
|
||||
<LearnAboutTeamsLink/>
|
||||
</div>
|
||||
<div className='divider-dark first'/>
|
||||
{nameSection}
|
||||
<div className='divider-light'/>
|
||||
{descriptionSection}
|
||||
<div className='divider-light'/>
|
||||
{teamIconSection}
|
||||
{!team?.group_constrained &&
|
||||
<>
|
||||
<div className='divider-light'/>
|
||||
{allowedDomainsSection}
|
||||
</>
|
||||
}
|
||||
<div className='divider-light'/>
|
||||
<OpenInvite
|
||||
teamId={this.props.team?.id}
|
||||
isActive={this.props.activeSection === 'open_invite'}
|
||||
isGroupConstrained={this.props.team?.group_constrained}
|
||||
allowOpenInvite={this.props.team?.allow_open_invite}
|
||||
onToggle={this.onOpenInviteToggle}
|
||||
patchTeam={this.props.actions.patchTeam}
|
||||
/>
|
||||
{!team?.group_constrained &&
|
||||
<>
|
||||
<div className='divider-light'/>
|
||||
{inviteSection}
|
||||
</>
|
||||
}
|
||||
<div className='divider-dark'/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(GeneralTab);
|
@ -0,0 +1,83 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useState} from 'react';
|
||||
import {defineMessages, useIntl} from 'react-intl';
|
||||
|
||||
import SelectTextInput, {type SelectTextInputOption} from 'components/common/select_text_input/select_text_input';
|
||||
import CheckboxSettingItem from 'components/widgets/modals/components/checkbox_setting_item';
|
||||
import {type SaveChangesPanelState} from 'components/widgets/modals/components/save_changes_panel';
|
||||
|
||||
const translations = defineMessages({
|
||||
AllowedDomainsTitle: {
|
||||
id: 'general_tab.AllowedDomainsTitle',
|
||||
defaultMessage: 'Users with a specific email domain',
|
||||
},
|
||||
AllowedDomainsInfo: {
|
||||
id: 'general_tab.AllowedDomainsInfo',
|
||||
defaultMessage: 'When enabled, users can only join the team if their email matches a specific domain (e.g. "mattermost.org")',
|
||||
},
|
||||
AllowedDomains: {
|
||||
id: 'general_tab.allowedDomains',
|
||||
defaultMessage: 'Allow only users with a specific email domain to join this team',
|
||||
},
|
||||
});
|
||||
|
||||
type Props = {
|
||||
allowedDomains: string[];
|
||||
setAllowedDomains: (domains: string[]) => void;
|
||||
setHasChanges: (hasChanges: boolean) => void;
|
||||
setSaveChangesPanelState: (state: SaveChangesPanelState) => void;
|
||||
}
|
||||
|
||||
const AllowedDomainsSelect = ({allowedDomains, setAllowedDomains, setHasChanges, setSaveChangesPanelState}: Props) => {
|
||||
const [showAllowedDomains, setShowAllowedDomains] = useState<boolean>(allowedDomains.length > 0);
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const handleEnableAllowedDomains = useCallback((enabled: boolean) => {
|
||||
setShowAllowedDomains(enabled);
|
||||
if (!enabled) {
|
||||
setAllowedDomains([]);
|
||||
}
|
||||
}, [setAllowedDomains]);
|
||||
|
||||
const updateAllowedDomains = useCallback((domain: string) => {
|
||||
setHasChanges(true);
|
||||
setSaveChangesPanelState('editing');
|
||||
setAllowedDomains([...allowedDomains, domain]);
|
||||
}, [allowedDomains, setAllowedDomains, setHasChanges, setSaveChangesPanelState]);
|
||||
|
||||
const handleOnChangeDomains = useCallback((allowedDomainsOptions?: SelectTextInputOption[] | null) => {
|
||||
setHasChanges(true);
|
||||
setSaveChangesPanelState('editing');
|
||||
setAllowedDomains(allowedDomainsOptions?.map((domain) => domain.value) || []);
|
||||
}, [setAllowedDomains, setHasChanges, setSaveChangesPanelState]);
|
||||
return (
|
||||
<>
|
||||
<CheckboxSettingItem
|
||||
data-testid='allowedDomainsCheckbox'
|
||||
className='access-allowed-domains-section'
|
||||
title={translations.AllowedDomainsTitle}
|
||||
description={translations.AllowedDomainsInfo}
|
||||
descriptionAboveContent={true}
|
||||
inputFieldData={{title: translations.AllowedDomains, name: 'name'}}
|
||||
inputFieldValue={showAllowedDomains}
|
||||
handleChange={handleEnableAllowedDomains}
|
||||
/>
|
||||
{showAllowedDomains &&
|
||||
<SelectTextInput
|
||||
id='allowedDomains'
|
||||
placeholder={formatMessage({id: 'general_tab.AllowedDomainsExample', defaultMessage: 'corp.mattermost.com, mattermost.com'})}
|
||||
aria-label={formatMessage({id: 'general_tab.allowedDomains.ariaLabel', defaultMessage: 'Allowed Domains'})}
|
||||
value={allowedDomains}
|
||||
onChange={handleOnChangeDomains}
|
||||
handleNewSelection={updateAllowedDomains}
|
||||
isClearable={false}
|
||||
description={formatMessage({id: 'general_tab.AllowedDomainsTip', defaultMessage: 'Seperate multiple domains with a space, comma, tab or enter.'})}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AllowedDomainsSelect;
|
@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import type {ConnectedProps} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import type {Dispatch} from 'redux';
|
||||
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
|
||||
import {patchTeam, regenerateTeamInviteId} from 'mattermost-redux/actions/teams';
|
||||
|
||||
import TeamAccessTab from './team_access_tab';
|
||||
|
||||
export type OwnProps = {
|
||||
team: Team;
|
||||
hasChanges: boolean;
|
||||
hasChangeTabError: boolean;
|
||||
setHasChanges: (hasChanges: boolean) => void;
|
||||
setHasChangeTabError: (hasChangesError: boolean) => void;
|
||||
closeModal: () => void;
|
||||
collapseModal: () => void;
|
||||
};
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
patchTeam,
|
||||
regenerateTeamInviteId,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
const connector = connect(null, mapDispatchToProps);
|
||||
|
||||
export type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
|
||||
export default connector(TeamAccessTab);
|
@ -0,0 +1,103 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useState, useEffect} from 'react';
|
||||
import {defineMessages, useIntl} from 'react-intl';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {RefreshIcon} from '@mattermost/compass-icons/components';
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
import {haveITeamPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
|
||||
import Input from 'components/widgets/inputs/input/input';
|
||||
import type {BaseSettingItemProps} from 'components/widgets/modals/components/base_setting_item';
|
||||
import BaseSettingItem from 'components/widgets/modals/components/base_setting_item';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
const translations = defineMessages({
|
||||
OpenInviteDescriptionError: {
|
||||
id: 'team_settings.openInviteDescription.error',
|
||||
defaultMessage: 'There was an error generating the invite code, please try again',
|
||||
},
|
||||
CodeTitle: {
|
||||
id: 'general_tab.codeTitle',
|
||||
defaultMessage: 'Invite Code',
|
||||
},
|
||||
CodeLongDesc: {
|
||||
id: 'general_tab.codeLongDesc',
|
||||
defaultMessage: 'The Invite Code is part of the unique team invitation link which is sent to members you’re inviting to this team. Regenerating the code creates a new invitation link and invalidates the previous link.',
|
||||
},
|
||||
});
|
||||
|
||||
type Props = {
|
||||
regenerateTeamInviteId: (teamId: string) => Promise<ActionResult>;
|
||||
}
|
||||
|
||||
const InviteSectionInput = ({regenerateTeamInviteId}: Props) => {
|
||||
const team = useSelector((state: GlobalState) => getCurrentTeam(state));
|
||||
const canInviteTeamMembers = useSelector((state: GlobalState) => haveITeamPermission(state, team?.id || '', Permissions.INVITE_USER));
|
||||
const [inviteId, setInviteId] = useState<Team['invite_id']>(team?.invite_id ?? '');
|
||||
const [inviteIdError, setInviteIdError] = useState<BaseSettingItemProps['error'] | undefined>();
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
useEffect(() => {
|
||||
setInviteId(team?.invite_id || '');
|
||||
}, [team?.invite_id]);
|
||||
|
||||
const handleRegenerateInviteId = useCallback(async () => {
|
||||
const {data, error} = await regenerateTeamInviteId(team?.id || '');
|
||||
|
||||
if (data?.invite_id) {
|
||||
setInviteId(data.invite_id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
setInviteIdError(translations.OpenInviteDescriptionError);
|
||||
}
|
||||
}, [regenerateTeamInviteId, team?.id]);
|
||||
|
||||
if (!canInviteTeamMembers) {
|
||||
return null;
|
||||
}
|
||||
const inviteSectionInput = (
|
||||
<div
|
||||
data-testid='teamInviteContainer'
|
||||
id='teamInviteContainer'
|
||||
>
|
||||
<Input
|
||||
id='teamInviteId'
|
||||
type='text'
|
||||
value={inviteId}
|
||||
maxLength={32}
|
||||
/>
|
||||
<button
|
||||
data-testid='regenerateButton'
|
||||
id='regenerateButton'
|
||||
className='btn btn-tertiary'
|
||||
onClick={handleRegenerateInviteId}
|
||||
>
|
||||
<RefreshIcon/>
|
||||
{formatMessage({id: 'general_tab.regenerate', defaultMessage: 'Regenerate'})}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseSettingItem
|
||||
className='access-invite-section'
|
||||
title={translations.CodeTitle}
|
||||
description={translations.CodeLongDesc}
|
||||
content={inviteSectionInput}
|
||||
error={inviteIdError}
|
||||
descriptionAboveContent={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default InviteSectionInput;
|
@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {fireEvent, screen} from '@testing-library/react';
|
||||
import React, {type ComponentProps} from 'react';
|
||||
|
||||
import {renderWithContext} from 'tests/react_testing_utils';
|
||||
|
||||
import OpenInvite from './open_invite';
|
||||
|
||||
describe('components/TeamSettings/OpenInvite', () => {
|
||||
const setAllowOpenInvite = jest.fn().mockReturnValue({data: true});
|
||||
const defaultProps: ComponentProps<typeof OpenInvite> = {
|
||||
isGroupConstrained: false,
|
||||
allowOpenInvite: false,
|
||||
setAllowOpenInvite,
|
||||
};
|
||||
|
||||
test('should render correct title and link when the team is constrained', () => {
|
||||
const props = {...defaultProps, isGroupConstrained: true};
|
||||
renderWithContext(<OpenInvite {...props}/>);
|
||||
const title = screen.getByText('Users on this server');
|
||||
expect(title).toBeInTheDocument();
|
||||
const externalLink = screen.getByText('Learn More');
|
||||
expect(externalLink).toBeInTheDocument();
|
||||
expect(externalLink).toHaveAttribute('href', 'https://mattermost.com/pl/default-ldap-group-constrained-team-channel.html?utm_source=mattermost&utm_medium=in-product&utm_content=open_invite&uid=&sid=');
|
||||
});
|
||||
|
||||
test('should render the checkbox when the team is not constrained and not checked', () => {
|
||||
renderWithContext(<OpenInvite {...defaultProps}/>);
|
||||
const checkbox = screen.getByRole('checkbox');
|
||||
expect(checkbox).toBeInTheDocument();
|
||||
expect(checkbox).not.toBeChecked();
|
||||
});
|
||||
|
||||
test('should render the checkbox when the team is not constrained and checked', () => {
|
||||
const props = {...defaultProps, allowOpenInvite: true};
|
||||
renderWithContext(<OpenInvite {...props}/>);
|
||||
const checkbox = screen.getByRole('checkbox');
|
||||
expect(checkbox).toBeInTheDocument();
|
||||
expect(checkbox).toBeChecked();
|
||||
});
|
||||
|
||||
test('should call setAllowOpenInvite when the checkbox is clicked', () => {
|
||||
renderWithContext(<OpenInvite {...defaultProps}/>);
|
||||
const checkbox = screen.getByRole('checkbox');
|
||||
expect(checkbox).toBeInTheDocument();
|
||||
expect(checkbox).not.toBeChecked();
|
||||
fireEvent.click(checkbox);
|
||||
expect(setAllowOpenInvite).toHaveBeenCalledTimes(1);
|
||||
expect(setAllowOpenInvite).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {defineMessages, useIntl} from 'react-intl';
|
||||
|
||||
import ExternalLink from 'components/external_link';
|
||||
import BaseSettingItem from 'components/widgets/modals/components/base_setting_item';
|
||||
import CheckboxSettingItem from 'components/widgets/modals/components/checkbox_setting_item';
|
||||
|
||||
type Props = {
|
||||
allowOpenInvite: boolean;
|
||||
isGroupConstrained?: boolean;
|
||||
setAllowOpenInvite: (value: boolean) => void;
|
||||
};
|
||||
|
||||
const translations = defineMessages({
|
||||
OpenInviteText: {
|
||||
id: 'general_tab.openInviteText',
|
||||
defaultMessage: 'Users on this server',
|
||||
},
|
||||
OpenInviteDesc: {
|
||||
id: 'general_tab.openInviteDesc',
|
||||
defaultMessage: 'When enabled, a link to this team will be included on the landing page allowing anyone with an account to join this team. Changing this setting will create a new invitation link and invalidate the previous link.',
|
||||
},
|
||||
OpenInviteTitle: {
|
||||
id: 'general_tab.openInviteTitle',
|
||||
defaultMessage: 'Allow only users with a specific email domain to join this team',
|
||||
},
|
||||
});
|
||||
|
||||
const OpenInvite = ({isGroupConstrained, allowOpenInvite, setAllowOpenInvite}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
if (isGroupConstrained) {
|
||||
const groupConstrainedContent = (
|
||||
<p id='groupConstrainedContent' >{
|
||||
formatMessage({
|
||||
id: 'team_settings.openInviteDescription.groupConstrained',
|
||||
defaultMessage: 'Members of this team are added and removed by linked groups. <link>Learn More</link>',
|
||||
}, {
|
||||
link: (msg: React.ReactNode) => (
|
||||
<ExternalLink
|
||||
href='https://mattermost.com/pl/default-ldap-group-constrained-team-channel.html'
|
||||
location='open_invite'
|
||||
>
|
||||
{msg}
|
||||
</ExternalLink>
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
);
|
||||
return (
|
||||
<BaseSettingItem
|
||||
className='access-invite-domains-section'
|
||||
title={translations.OpenInviteText}
|
||||
description={translations.OpenInviteDesc}
|
||||
descriptionAboveContent={true}
|
||||
content={groupConstrainedContent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CheckboxSettingItem
|
||||
className='access-invite-domains-section'
|
||||
inputFieldData={{title: translations.OpenInviteTitle, name: 'name'}}
|
||||
inputFieldValue={allowOpenInvite}
|
||||
handleChange={setAllowOpenInvite}
|
||||
title={translations.OpenInviteText}
|
||||
description={translations.OpenInviteDesc}
|
||||
descriptionAboveContent={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default OpenInvite;
|
@ -0,0 +1,34 @@
|
||||
.modal-access-tab-content {
|
||||
.divider-light {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.mm-modal-generic-section-item__content {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
#allowedDomains {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.access-invite-section {
|
||||
gap: 0;
|
||||
|
||||
#teamInviteContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.Input_container {
|
||||
margin-right: 8px;
|
||||
|
||||
#teamInviteId {
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background-color: var(--center-channel-bg);
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import type {ComponentProps} from 'react';
|
||||
import {act} from 'react-dom/test-utils';
|
||||
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
|
||||
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import AccessTab from './team_access_tab';
|
||||
|
||||
describe('components/TeamSettings', () => {
|
||||
const getTeam = jest.fn().mockResolvedValue({data: true});
|
||||
const patchTeam = jest.fn().mockReturnValue({data: true});
|
||||
const regenerateTeamInviteId = jest.fn().mockReturnValue({data: true});
|
||||
const removeTeamIcon = jest.fn().mockReturnValue({data: true});
|
||||
const setTeamIcon = jest.fn().mockReturnValue({data: true});
|
||||
const baseActions = {
|
||||
getTeam,
|
||||
patchTeam,
|
||||
regenerateTeamInviteId,
|
||||
removeTeamIcon,
|
||||
setTeamIcon,
|
||||
};
|
||||
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(),
|
||||
collapseModal: jest.fn(),
|
||||
};
|
||||
|
||||
test('should not render team invite section if no permissions for team inviting', () => {
|
||||
const props = {...defaultProps, canInviteTeamMembers: false};
|
||||
renderWithContext(<AccessTab {...props}/>);
|
||||
const inviteContainer = screen.queryByTestId('teamInviteContainer');
|
||||
expect(inviteContainer).toBeNull();
|
||||
});
|
||||
|
||||
test('should call regenerateTeamInviteId on handleRegenerateInviteId', () => {
|
||||
const state = {
|
||||
entities: {
|
||||
roles: {
|
||||
roles: {
|
||||
team_admin: {
|
||||
name: 'team_admin',
|
||||
permissions: [Permissions.INVITE_USER],
|
||||
},
|
||||
},
|
||||
},
|
||||
users: {
|
||||
profiles: {
|
||||
test_user: TestHelper.getUserMock({id: 'test_user', roles: 'team_admin'}),
|
||||
},
|
||||
currentUserId: 'test_user',
|
||||
},
|
||||
teams: {
|
||||
currentTeamId: 'team_id',
|
||||
teams: {
|
||||
team_id: {...defaultProps.team},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = renderWithContext(<AccessTab {...defaultProps}/>, state);
|
||||
wrapper.getByTestId('regenerateButton').click();
|
||||
expect(baseActions.regenerateTeamInviteId).toHaveBeenCalledTimes(1);
|
||||
expect(baseActions.regenerateTeamInviteId).toHaveBeenCalledWith(defaultProps.team?.id);
|
||||
});
|
||||
|
||||
test('should not render allowed domains checkbox if no permissions for team inviting', () => {
|
||||
const props = {...defaultProps, canInviteTeamMembers: false};
|
||||
renderWithContext(<AccessTab {...props}/>);
|
||||
const allowedDomainsCheckbox = screen.queryByTestId('allowedDomainsCheckbox');
|
||||
expect(allowedDomainsCheckbox).toBeNull();
|
||||
});
|
||||
|
||||
test('should not show allowed domains input if allowed domains is empty', () => {
|
||||
const props = {...defaultProps, team: TestHelper.getTeamMock({allowed_domains: ''})};
|
||||
renderWithContext(<AccessTab {...props}/>);
|
||||
const allowedDomainsInput = screen.queryByText('Seperate multiple domains with a space, comma, tab or enter.');
|
||||
expect(allowedDomainsInput).toBeNull();
|
||||
});
|
||||
|
||||
test('should show allowed domains input if allowed domains is not empty', () => {
|
||||
const props = {...defaultProps, team: TestHelper.getTeamMock({allowed_domains: 'test.com'})};
|
||||
renderWithContext(<AccessTab {...props}/>);
|
||||
const allowedDomainsInput = screen.getByText('Seperate multiple domains with a space, comma, tab or enter.');
|
||||
expect(allowedDomainsInput).toBeInTheDocument();
|
||||
const allowedDomainsInputValue = screen.getByText('test.com');
|
||||
expect(allowedDomainsInputValue).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should call patchTeam on handleAllowedDomainsSubmit', async () => {
|
||||
const props = {...defaultProps, team: TestHelper.getTeamMock({allowed_domains: 'test.com'})};
|
||||
renderWithContext(<AccessTab {...props}/>);
|
||||
const allowedDomainsInput = screen.getAllByRole('textbox')[0];
|
||||
const newDomain = 'best.com';
|
||||
await act(async () => {
|
||||
await allowedDomainsInput.focus();
|
||||
await userEvent.type(allowedDomainsInput, `${newDomain},`);
|
||||
});
|
||||
|
||||
const newDomainText = screen.getByText(newDomain);
|
||||
expect(newDomainText).toBeInTheDocument();
|
||||
|
||||
const saveButton = screen.getByTestId('mm-save-changes-panel__save-btn');
|
||||
await act(async () => {
|
||||
userEvent.click(saveButton);
|
||||
});
|
||||
expect(baseActions.patchTeam).toHaveBeenCalledTimes(1);
|
||||
expect(baseActions.patchTeam).toHaveBeenCalledWith({
|
||||
allowed_domains: 'test.com, best.com',
|
||||
id: defaultProps.team?.id,
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,161 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// 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';
|
||||
import InviteSectionInput from './invite_section_input';
|
||||
import OpenInvite from './open_invite';
|
||||
|
||||
import type {PropsFromRedux, OwnProps} from '.';
|
||||
|
||||
import './team_access_tab.scss';
|
||||
|
||||
const generateAllowedDomainOptions = (allowedDomains?: string) => {
|
||||
if (!allowedDomains || allowedDomains.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const domainList = allowedDomains.includes(',') ? allowedDomains.split(',') : [allowedDomains];
|
||||
return domainList.map((domain) => domain.trim());
|
||||
};
|
||||
|
||||
type Props = PropsFromRedux & OwnProps;
|
||||
|
||||
const AccessTab = ({closeModal, collapseModal, hasChangeTabError, hasChanges, setHasChangeTabError, setHasChanges, 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({
|
||||
id: team.id,
|
||||
allowed_domains: allowedDomains.length === 1 ? allowedDomains[0] : allowedDomains.join(', '),
|
||||
});
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, [actions, allowedDomains, team]);
|
||||
|
||||
const handleOpenInviteSubmit = useCallback(async (): Promise<boolean> => {
|
||||
if (allowOpenInvite === team.allow_open_invite) {
|
||||
return true;
|
||||
}
|
||||
const data = {
|
||||
id: team.id,
|
||||
allow_open_invite: allowOpenInvite,
|
||||
};
|
||||
|
||||
const {error} = await actions.patchTeam(data);
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, [actions, allowOpenInvite, team]);
|
||||
|
||||
const updateOpenInvite = useCallback((value: boolean) => {
|
||||
setHasChanges(true);
|
||||
setSaveChangesPanelState('editing');
|
||||
setAllowOpenInvite(value);
|
||||
}, [setHasChanges]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setSaveChangesPanelState('editing');
|
||||
setHasChanges(false);
|
||||
setHasChangeTabError(false);
|
||||
}, [setHasChangeTabError, setHasChanges]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
setAllowedDomains(generateAllowedDomainOptions(team.allowed_domains));
|
||||
setAllowOpenInvite(team.allow_open_invite ?? false);
|
||||
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();
|
||||
if (!allowedDomainSuccess || !openInviteSuccess) {
|
||||
setSaveChangesPanelState('error');
|
||||
return;
|
||||
}
|
||||
setSaveChangesPanelState('saved');
|
||||
setHasChangeTabError(false);
|
||||
}, [handleAllowedDomainsSubmit, handleOpenInviteSubmit, setHasChangeTabError]);
|
||||
|
||||
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: 'Collapes 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'>
|
||||
{team.group_constrained ?
|
||||
undefined :
|
||||
<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 ?
|
||||
undefined :
|
||||
<InviteSectionInput regenerateTeamInviteId={actions.regenerateTeamInviteId}/>
|
||||
}
|
||||
{hasChanges ?
|
||||
<SaveChangesPanel
|
||||
handleCancel={handleCancel}
|
||||
handleSubmit={handleSaveChanges}
|
||||
handleClose={handleClose}
|
||||
tabChangeError={hasChangeTabError}
|
||||
state={saveChangesPanelState}
|
||||
/> : undefined}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default AccessTab;
|
@ -8,35 +8,29 @@ import type {Dispatch} from 'redux';
|
||||
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
|
||||
import {getTeam, patchTeam, removeTeamIcon, setTeamIcon, regenerateTeamInviteId} from 'mattermost-redux/actions/teams';
|
||||
import {Permissions} from 'mattermost-redux/constants';
|
||||
import {getTeam, patchTeam, removeTeamIcon, setTeamIcon} from 'mattermost-redux/actions/teams';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import {haveITeamPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||
|
||||
import {getIsMobileView} from 'selectors/views/browser';
|
||||
|
||||
import type {GlobalState} from 'types/store/index';
|
||||
|
||||
import TeamGeneralTab from './team_general_tab';
|
||||
import TeamInfoTab from './team_info_tab';
|
||||
|
||||
export type OwnProps = {
|
||||
updateSection: (section: string) => void;
|
||||
team: Team & { last_team_icon_update?: number };
|
||||
activeSection: string;
|
||||
team: Team;
|
||||
hasChanges: boolean;
|
||||
hasChangeTabError: boolean;
|
||||
setHasChanges: (hasChanges: boolean) => void;
|
||||
setHasChangeTabError: (hasChangesError: boolean) => void;
|
||||
closeModal: () => void;
|
||||
collapseModal: () => void;
|
||||
};
|
||||
|
||||
function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
||||
function mapStateToProps(state: GlobalState) {
|
||||
const config = getConfig(state);
|
||||
const maxFileSize = parseInt(config.MaxFileSize ?? '', 10);
|
||||
|
||||
const canInviteTeamMembers = haveITeamPermission(state, ownProps.team?.id || '', Permissions.INVITE_USER);
|
||||
|
||||
return {
|
||||
maxFileSize,
|
||||
canInviteTeamMembers,
|
||||
isMobileView: getIsMobileView(state),
|
||||
};
|
||||
}
|
||||
|
||||
@ -45,7 +39,6 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
||||
actions: bindActionCreators({
|
||||
getTeam,
|
||||
patchTeam,
|
||||
regenerateTeamInviteId,
|
||||
removeTeamIcon,
|
||||
setTeamIcon,
|
||||
}, dispatch),
|
||||
@ -56,4 +49,4 @@ const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export type PropsFromRedux = ConnectedProps<typeof connector>;
|
||||
|
||||
export default connector(TeamGeneralTab);
|
||||
export default connector(TeamInfoTab);
|
@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import type {ChangeEvent} from 'react';
|
||||
import {defineMessages, useIntl} from 'react-intl';
|
||||
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
|
||||
import Input from 'components/widgets/inputs/input/input';
|
||||
import BaseSettingItem, {type BaseSettingItemProps} from 'components/widgets/modals/components/base_setting_item';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
|
||||
const translations = defineMessages({
|
||||
TeamDescriptionInfo: {
|
||||
id: 'general_tab.teamDescriptionInfo',
|
||||
defaultMessage: 'Team description provides additional information to help users select the right team. Maximum of 50 characters.',
|
||||
},
|
||||
});
|
||||
|
||||
type Props = {
|
||||
handleDescriptionChanges: (name: string) => void;
|
||||
description: Team['description'];
|
||||
clientError?: BaseSettingItemProps['error'];
|
||||
};
|
||||
|
||||
const TeamDescriptionSection = ({handleDescriptionChanges, clientError, description}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const updateDescription = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
handleDescriptionChanges(e.target.value);
|
||||
}, [handleDescriptionChanges]);
|
||||
|
||||
const descriptionSectionInput = (
|
||||
<Input
|
||||
id='teamDescription'
|
||||
data-testid='teamDescriptionInput'
|
||||
containerClassName='description-section-input'
|
||||
type='textarea'
|
||||
maxLength={Constants.MAX_TEAMDESCRIPTION_LENGTH}
|
||||
onChange={updateDescription}
|
||||
value={description}
|
||||
label={formatMessage({id: 'general_tab.teamDescription', defaultMessage: 'Description'})}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseSettingItem
|
||||
description={translations.TeamDescriptionInfo}
|
||||
content={descriptionSectionInput}
|
||||
className='description-setting-item'
|
||||
error={clientError}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamDescriptionSection;
|
@ -0,0 +1,26 @@
|
||||
.modal-info-tab-content {
|
||||
display: flex;
|
||||
|
||||
.name-description-container {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.description-section-input {
|
||||
fieldset {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#teamName,
|
||||
#teamDescription {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.description-setting-item {
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background-color: var(--center-channel-bg);
|
||||
}
|
@ -0,0 +1,183 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {screen} from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import type {ComponentProps} from 'react';
|
||||
import {act} from 'react-dom/test-utils';
|
||||
|
||||
import {renderWithContext, userEvent} from 'tests/react_testing_utils';
|
||||
import Constants from 'utils/constants';
|
||||
import {TestHelper} from 'utils/test_helper';
|
||||
|
||||
import InfoTab from './team_info_tab';
|
||||
|
||||
describe('components/TeamSettings', () => {
|
||||
const getTeam = jest.fn().mockResolvedValue({data: true});
|
||||
const patchTeam = jest.fn().mockReturnValue({data: true});
|
||||
const removeTeamIcon = jest.fn().mockReturnValue({data: true});
|
||||
const setTeamIcon = jest.fn().mockReturnValue({data: true});
|
||||
const baseActions = {
|
||||
getTeam,
|
||||
patchTeam,
|
||||
removeTeamIcon,
|
||||
setTeamIcon,
|
||||
};
|
||||
const defaultProps: ComponentProps<typeof InfoTab> = {
|
||||
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(),
|
||||
closeModal: jest.fn(),
|
||||
collapseModal: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
global.URL.createObjectURL = jest.fn();
|
||||
});
|
||||
|
||||
test('should show an error when pdf file is uploaded', () => {
|
||||
renderWithContext(<InfoTab {...defaultProps}/>);
|
||||
const file = new File(['pdf'], 'pdf.pdf', {type: 'application/pdf'});
|
||||
const input = screen.getByTestId('uploadPicture');
|
||||
userEvent.upload(input, file);
|
||||
|
||||
const error = screen.getByTestId('mm-modal-generic-section-item__error');
|
||||
expect(error).toBeVisible();
|
||||
expect(error).toHaveTextContent('Only BMP, JPG or PNG images may be used for team icons');
|
||||
});
|
||||
|
||||
test('should show an error when too large file is uploaded with 40mb', () => {
|
||||
renderWithContext(<InfoTab {...defaultProps}/>);
|
||||
const file = new File(['test'], 'test.png', {type: 'image/png'});
|
||||
Object.defineProperty(file, 'size', {value: defaultProps.maxFileSize + 1});
|
||||
const input = screen.getByTestId('uploadPicture');
|
||||
userEvent.upload(input, file);
|
||||
|
||||
const error = screen.getByTestId('mm-modal-generic-section-item__error');
|
||||
expect(error).toBeVisible();
|
||||
expect(error).toHaveTextContent('Unable to upload team icon. File is too large.');
|
||||
});
|
||||
|
||||
test('should call setTeamIcon when an image is uploaded and saved', async () => {
|
||||
renderWithContext(<InfoTab {...defaultProps}/>);
|
||||
const file = new File(['test'], 'test.png', {type: 'image/png'});
|
||||
const input = screen.getByTestId('uploadPicture');
|
||||
await act(async () => {
|
||||
userEvent.upload(input, file);
|
||||
});
|
||||
|
||||
const saveButton = screen.getByTestId('mm-save-changes-panel__save-btn');
|
||||
await act(async () => {
|
||||
userEvent.click(saveButton);
|
||||
});
|
||||
|
||||
expect(setTeamIcon).toHaveBeenCalledTimes(1);
|
||||
expect(setTeamIcon).toHaveBeenCalledWith(defaultProps.team?.id, file);
|
||||
});
|
||||
|
||||
test('should call setTeamIcon when an image is removed', async () => {
|
||||
renderWithContext(<InfoTab {...defaultProps}/>);
|
||||
const file = new File(['test'], 'test.png', {type: 'image/png'});
|
||||
const input = screen.getByTestId('uploadPicture');
|
||||
await act(async () => {
|
||||
userEvent.upload(input, file);
|
||||
});
|
||||
|
||||
const saveButton = screen.getByTestId('mm-save-changes-panel__save-btn');
|
||||
await act(async () => {
|
||||
userEvent.click(saveButton);
|
||||
});
|
||||
|
||||
const removeImageButton = screen.getByTestId('removeImageButton');
|
||||
await act(async () => {
|
||||
userEvent.click(removeImageButton);
|
||||
});
|
||||
|
||||
expect(removeTeamIcon).toHaveBeenCalledTimes(1);
|
||||
expect(removeTeamIcon).toHaveBeenCalledWith(defaultProps.team?.id);
|
||||
});
|
||||
|
||||
test('should show an error when team name is empty', async () => {
|
||||
renderWithContext(<InfoTab {...defaultProps}/>);
|
||||
const input = screen.getByTestId('teamNameInput');
|
||||
act(() => {
|
||||
userEvent.clear(input);
|
||||
});
|
||||
const saveButton = screen.getByTestId('mm-save-changes-panel__save-btn');
|
||||
await act(async () => {
|
||||
userEvent.click(saveButton);
|
||||
});
|
||||
|
||||
const error = screen.getByTestId('mm-modal-generic-section-item__error');
|
||||
expect(error).toBeVisible();
|
||||
expect(error).toHaveTextContent('This field is required');
|
||||
});
|
||||
|
||||
test('should show an error when team name is too short', async () => {
|
||||
renderWithContext(<InfoTab {...defaultProps}/>);
|
||||
const input = screen.getByTestId('teamNameInput');
|
||||
await act(async () => {
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, 'a');
|
||||
});
|
||||
const saveButton = screen.getByTestId('mm-save-changes-panel__save-btn');
|
||||
await act(async () => {
|
||||
userEvent.click(saveButton);
|
||||
});
|
||||
|
||||
const error = screen.getByTestId('mm-modal-generic-section-item__error');
|
||||
expect(error).toBeVisible();
|
||||
expect(error).toHaveTextContent(`Team Name must be ${Constants.MIN_TEAMNAME_LENGTH} or more characters up to a maximum of ${Constants.MAX_TEAMNAME_LENGTH}. You can add a longer team description.`);
|
||||
});
|
||||
|
||||
test('should call patchTeam when team name is changed and clicked saved', async () => {
|
||||
renderWithContext(<InfoTab {...defaultProps}/>);
|
||||
const input = screen.getByTestId('teamNameInput');
|
||||
userEvent.clear(input);
|
||||
userEvent.type(input, 'new_team_name');
|
||||
const saveButton = screen.getByTestId('mm-save-changes-panel__save-btn');
|
||||
await act(async () => {
|
||||
userEvent.click(saveButton);
|
||||
});
|
||||
|
||||
expect(patchTeam).toHaveBeenCalledTimes(1);
|
||||
expect(patchTeam).toHaveBeenCalledWith({id: defaultProps.team?.id, display_name: 'new_team_name', description: defaultProps.team?.description});
|
||||
});
|
||||
|
||||
test('should call patchTeam when team description is changed and clicked saved', async () => {
|
||||
renderWithContext(<InfoTab {...defaultProps}/>);
|
||||
const input = screen.getByTestId('teamDescriptionInput');
|
||||
await act(async () => {
|
||||
await userEvent.clear(input);
|
||||
await userEvent.type(input, 'new_team_description');
|
||||
});
|
||||
const saveButton = screen.getByTestId('mm-save-changes-panel__save-btn');
|
||||
await act(async () => {
|
||||
userEvent.click(saveButton);
|
||||
});
|
||||
|
||||
expect(patchTeam).toHaveBeenCalledTimes(1);
|
||||
expect(patchTeam).toHaveBeenCalledWith({id: defaultProps.team?.id, display_name: defaultProps.team?.display_name, description: 'new_team_description'});
|
||||
});
|
||||
|
||||
test('should call patchTeam when team name and description are change and clicked saved', async () => {
|
||||
renderWithContext(<InfoTab {...defaultProps}/>);
|
||||
const nameInput = screen.getByTestId('teamNameInput');
|
||||
const descriptionInput = screen.getByTestId('teamDescriptionInput');
|
||||
userEvent.clear(nameInput);
|
||||
userEvent.type(nameInput, 'new_team_name');
|
||||
userEvent.clear(descriptionInput);
|
||||
userEvent.type(descriptionInput, 'new_team_description');
|
||||
const saveButton = screen.getByTestId('mm-save-changes-panel__save-btn');
|
||||
await act(async () => {
|
||||
userEvent.click(saveButton);
|
||||
});
|
||||
|
||||
expect(patchTeam).toHaveBeenCalledTimes(1);
|
||||
expect(patchTeam).toHaveBeenCalledWith({id: defaultProps.team?.id, display_name: 'new_team_name', description: 'new_team_description'});
|
||||
});
|
||||
});
|
@ -0,0 +1,236 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useState} from 'react';
|
||||
import type {ChangeEvent} from 'react';
|
||||
import {defineMessages, useIntl} 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';
|
||||
|
||||
import TeamDescriptionSection from './team_description_section';
|
||||
import TeamNameSection from './team_name_section';
|
||||
import TeamPictureSection from './team_picture_section';
|
||||
|
||||
import type {PropsFromRedux, OwnProps} from '.';
|
||||
|
||||
import './team_info_tab.scss';
|
||||
|
||||
const ACCEPTED_TEAM_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/bmp'];
|
||||
const translations = defineMessages({
|
||||
Required: {
|
||||
id: 'general_tab.required',
|
||||
defaultMessage: 'This field is required',
|
||||
},
|
||||
TeamNameRestrictions: {
|
||||
id: 'general_tab.teamNameRestrictions',
|
||||
defaultMessage: 'Team Name must be {min} or more characters up to a maximum of {max}. You can add a longer team description.',
|
||||
values: {min: Constants.MIN_TEAMNAME_LENGTH, max: Constants.MAX_TEAMNAME_LENGTH},
|
||||
},
|
||||
TeamIconInvalidFileType: {
|
||||
id: 'general_tab.teamIconInvalidFileType',
|
||||
defaultMessage: 'Only BMP, JPG or PNG images may be used for team icons',
|
||||
},
|
||||
TeamIconTooLarge: {
|
||||
id: 'general_tab.teamIconTooLarge',
|
||||
defaultMessage: 'Unable to upload team icon. File is too large.',
|
||||
},
|
||||
TeamIconError: {
|
||||
id: 'general_tab.teamIconError',
|
||||
defaultMessage: 'An error occurred while selecting the image.',
|
||||
},
|
||||
});
|
||||
type Props = PropsFromRedux & OwnProps;
|
||||
|
||||
const InfoTab = ({team, hasChanges, maxFileSize, closeModal, collapseModal, hasChangeTabError, setHasChangeTabError, setHasChanges, 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>();
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
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) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
setNameClientError(translations.Required);
|
||||
return false;
|
||||
} else if (name.length < Constants.MIN_TEAMNAME_LENGTH) {
|
||||
setNameClientError(translations.TeamNameRestrictions);
|
||||
return false;
|
||||
}
|
||||
setNameClientError(undefined);
|
||||
const {error} = await actions.patchTeam({id: team.id, display_name: name, description});
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, [actions, description, name, team.description, team.display_name, team.id]);
|
||||
|
||||
const handleTeamIconSubmit = useCallback(async (): Promise<boolean> => {
|
||||
if (!teamIconFile) {
|
||||
return true;
|
||||
}
|
||||
setLoading(true);
|
||||
setImageClientError(undefined);
|
||||
const {error} = await actions.setTeamIcon(team.id, teamIconFile);
|
||||
setLoading(false);
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, [actions, team, teamIconFile]);
|
||||
|
||||
const handleSaveChanges = useCallback(async () => {
|
||||
const nameDescriptionSuccess = await handleNameDescriptionSubmit();
|
||||
const teamIconSuccess = await handleTeamIconSubmit();
|
||||
if (!teamIconSuccess || !nameDescriptionSuccess) {
|
||||
setSaveChangesPanelState('error');
|
||||
return;
|
||||
}
|
||||
setSaveChangesPanelState('saved');
|
||||
setHasChangeTabError(false);
|
||||
}, [handleNameDescriptionSubmit, handleTeamIconSubmit, setHasChangeTabError]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setSaveChangesPanelState('editing');
|
||||
setHasChanges(false);
|
||||
setHasChangeTabError(false);
|
||||
}, [setHasChangeTabError, setHasChanges]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
setName(team.display_name ?? team.name);
|
||||
setDescription(team.description);
|
||||
setTeamIconFile(undefined);
|
||||
setImageClientError(undefined);
|
||||
setNameClientError(undefined);
|
||||
handleClose();
|
||||
}, [handleClose, team.description, team.display_name, team.name]);
|
||||
|
||||
const handleTeamIconRemove = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setImageClientError(undefined);
|
||||
setTeamIconFile(undefined);
|
||||
handleClose();
|
||||
|
||||
const {error} = await actions.removeTeamIcon(team.id);
|
||||
setLoading(false);
|
||||
if (error) {
|
||||
setSaveChangesPanelState('error');
|
||||
setHasChanges(true);
|
||||
setHasChangeTabError(true);
|
||||
}
|
||||
}, [actions, handleClose, setHasChangeTabError, setHasChanges, team.id]);
|
||||
|
||||
const updateTeamIcon = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (e && e.target && e.target.files && e.target.files[0]) {
|
||||
const file = e.target.files[0];
|
||||
|
||||
if (!ACCEPTED_TEAM_IMAGE_TYPES.includes(file.type)) {
|
||||
setImageClientError(translations.TeamIconInvalidFileType);
|
||||
} else if (file.size > maxFileSize) {
|
||||
setImageClientError(translations.TeamIconTooLarge);
|
||||
} else {
|
||||
setTeamIconFile(file);
|
||||
setImageClientError(undefined);
|
||||
setSaveChangesPanelState('editing');
|
||||
setHasChanges(true);
|
||||
}
|
||||
} else {
|
||||
setTeamIconFile(undefined);
|
||||
setImageClientError(translations.TeamIconError);
|
||||
}
|
||||
}, [maxFileSize, setHasChanges]);
|
||||
|
||||
const handleNameChanges = useCallback((name: string) => {
|
||||
setHasChanges(true);
|
||||
setSaveChangesPanelState('editing');
|
||||
setName(name);
|
||||
}, [setHasChanges]);
|
||||
|
||||
const handleDescriptionChanges = useCallback((description: string) => {
|
||||
setHasChanges(true);
|
||||
setSaveChangesPanelState('editing');
|
||||
setDescription(description);
|
||||
}, [setHasChanges]);
|
||||
|
||||
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: 'Collapes 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' >
|
||||
<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}
|
||||
/>
|
||||
{hasChanges ?
|
||||
<SaveChangesPanel
|
||||
handleCancel={handleCancel}
|
||||
handleSubmit={handleSaveChanges}
|
||||
handleClose={handleClose}
|
||||
tabChangeError={hasChangeTabError}
|
||||
state={saveChangesPanelState}
|
||||
/> : undefined}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return <ModalSection content={modalSectionContent}/>;
|
||||
};
|
||||
export default InfoTab;
|
@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import type {ChangeEvent} from 'react';
|
||||
import {defineMessages, useIntl} from 'react-intl';
|
||||
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
|
||||
import Input from 'components/widgets/inputs/input/input';
|
||||
import BaseSettingItem, {type BaseSettingItemProps} from 'components/widgets/modals/components/base_setting_item';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
|
||||
const translations = defineMessages({
|
||||
TeamInfo: {
|
||||
id: 'general_tab.teamInfo',
|
||||
defaultMessage: 'Team info',
|
||||
},
|
||||
TeamNameInfo: {
|
||||
id: 'general_tab.teamNameInfo',
|
||||
defaultMessage: 'This name will appear on your sign-in screen and at the top of the left sidebar.',
|
||||
},
|
||||
});
|
||||
|
||||
type Props = {
|
||||
handleNameChanges: (name: string) => void;
|
||||
name: Team['display_name'];
|
||||
clientError: BaseSettingItemProps['error'];
|
||||
};
|
||||
|
||||
const TeamNameSection = ({clientError, handleNameChanges, name}: Props) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const updateName = useCallback((e: ChangeEvent<HTMLInputElement>) => handleNameChanges(e.target.value), [handleNameChanges]);
|
||||
|
||||
const nameSectionInput = (
|
||||
<Input
|
||||
id='teamName'
|
||||
data-testid='teamNameInput'
|
||||
type='text'
|
||||
maxLength={Constants.MAX_TEAMNAME_LENGTH}
|
||||
onChange={updateName}
|
||||
value={name}
|
||||
label={formatMessage({id: 'general_tab.teamName', defaultMessage: 'Team Name'})}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseSettingItem
|
||||
title={translations.TeamInfo}
|
||||
description={translations.TeamNameInfo}
|
||||
content={nameSectionInput}
|
||||
error={clientError}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamNameSection;
|
@ -0,0 +1,61 @@
|
||||
.picture-setting-item {
|
||||
flex: 1;
|
||||
margin-left: 40px;
|
||||
|
||||
&__remove-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 18px;
|
||||
color: var(--dnd-indicator);
|
||||
text-align: left;
|
||||
|
||||
svg {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.team-picture-section {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
|
||||
&__team-icon {
|
||||
display: flex;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--sidebar-header-bg);
|
||||
border-radius: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__team-name {
|
||||
color: #fff;
|
||||
font-family: Metropolis;
|
||||
font-size: 56px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 56px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icon-pencil-outline {
|
||||
position: absolute;
|
||||
right: -6px;
|
||||
bottom: -6px;
|
||||
padding: 4px;
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||
background: var(--center-channel-bg, #fff);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 2px 3px 0 var(--elevation-1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.team-img-preview {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,180 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {type ChangeEvent, useRef, useState, useEffect, useCallback} from 'react';
|
||||
import {defineMessages, useIntl} from 'react-intl';
|
||||
|
||||
import {TrashCanOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
|
||||
import EditIcon from 'components/widgets/icons/fa_edit_icon';
|
||||
import BaseSettingItem from 'components/widgets/modals/components/base_setting_item';
|
||||
import type {BaseSettingItemProps} from 'components/widgets/modals/components/base_setting_item';
|
||||
|
||||
import Constants from 'utils/constants';
|
||||
import * as FileUtils from 'utils/file_utils';
|
||||
import {imageURLForTeam} from 'utils/utils';
|
||||
import './team_picture_section.scss';
|
||||
|
||||
const translations = defineMessages({
|
||||
Title: {
|
||||
id: 'setting_picture.title',
|
||||
defaultMessage: 'Team Icon',
|
||||
},
|
||||
Profile: {
|
||||
id: 'setting_picture.help.profile',
|
||||
defaultMessage: 'Upload a picture in BMP, JPG, JPEG, or PNG format. Maximum file size: {max}',
|
||||
values: {max: '50MB'},
|
||||
},
|
||||
});
|
||||
|
||||
type Props = {
|
||||
team: Team;
|
||||
file?: File | null;
|
||||
teamName: string;
|
||||
disabled: boolean;
|
||||
onFileChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
onRemove: () => void;
|
||||
clientError?: BaseSettingItemProps['error'];
|
||||
};
|
||||
|
||||
const TeamPictureSection = ({team, file, teamName, disabled, onFileChange, onRemove, clientError}: Props) => {
|
||||
const selectInput = useRef<HTMLInputElement>(null);
|
||||
const [image, setImage] = useState<string>('');
|
||||
const [orientationStyles, setOrientationStyles] = useState<{transform: string; transformOrigin: string}>();
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
const teamImageSource = imageURLForTeam(team);
|
||||
|
||||
const handleInputFile = useCallback(() => {
|
||||
if (selectInput.current) {
|
||||
selectInput.current.value = '';
|
||||
selectInput.current.click();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const editIcon = () => {
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
data-testid='uploadPicture'
|
||||
ref={selectInput}
|
||||
className='hidden'
|
||||
accept={Constants.ACCEPT_STATIC_IMAGE}
|
||||
disabled={disabled}
|
||||
type='file'
|
||||
onChange={onFileChange}
|
||||
aria-hidden={true}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<span
|
||||
disabled={disabled}
|
||||
onClick={handleInputFile}
|
||||
>
|
||||
<EditIcon/>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const teamImage = () => {
|
||||
if (file) {
|
||||
const imageStyles = {
|
||||
backgroundImage: 'url(' + image + ')',
|
||||
backgroundSize: 'cover',
|
||||
backgroundRepeat: 'round',
|
||||
...orientationStyles,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
id='teamIconImage'
|
||||
alt='team image preview'
|
||||
style={imageStyles}
|
||||
className='team-img-preview'
|
||||
onClick={handleInputFile}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (teamImageSource) {
|
||||
return (
|
||||
<img
|
||||
id='teamIconImage'
|
||||
className='team-img-preview'
|
||||
src={teamImageSource}
|
||||
onClick={handleInputFile}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className='team-picture-section__team-icon' >
|
||||
<span
|
||||
id='teamIconInitial'
|
||||
onClick={handleInputFile}
|
||||
className='team-picture-section__team-name'
|
||||
>{teamName.charAt(0).toUpperCase() + teamName.charAt(1)}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const setPicture = (file: File) => {
|
||||
if (file) {
|
||||
const previewBlob = URL.createObjectURL(file);
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const orientation = FileUtils.getExifOrientation(e.target!.result! as ArrayBuffer);
|
||||
const orientationStyles = FileUtils.getOrientationStyles(orientation);
|
||||
|
||||
setImage(previewBlob);
|
||||
setOrientationStyles(orientationStyles);
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (file) {
|
||||
setPicture(file);
|
||||
}
|
||||
}, [file]);
|
||||
|
||||
const removeImageButton = () => {
|
||||
if (file || teamImageSource) {
|
||||
return (
|
||||
<button
|
||||
onClick={onRemove}
|
||||
data-testid='removeImageButton'
|
||||
className='style--none picture-setting-item__remove-button'
|
||||
>
|
||||
<TrashCanOutlineIcon/>
|
||||
{formatMessage({id: 'setting_picture.remove_image', defaultMessage: 'Remove image'})}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const teamPictureSection = (
|
||||
<>
|
||||
<div className='team-picture-section' >
|
||||
{teamImage()}
|
||||
{editIcon()}
|
||||
</div>
|
||||
{removeImageButton()}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseSettingItem
|
||||
title={translations.Title}
|
||||
description={teamImageSource ? undefined : translations.Profile}
|
||||
content={teamPictureSection}
|
||||
className='picture-setting-item'
|
||||
error={clientError}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamPictureSection;
|
@ -5,24 +5,29 @@ import React from 'react';
|
||||
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
|
||||
import GeneralTab from 'components/team_general_tab';
|
||||
import AccessTab from './team_access_tab';
|
||||
import InfoTab from './team_info_tab';
|
||||
|
||||
type Props = {
|
||||
activeTab: string;
|
||||
activeSection: string;
|
||||
updateSection: (section: string) => void;
|
||||
hasChanges: boolean;
|
||||
hasChangeTabError: boolean;
|
||||
setHasChanges: (hasChanges: boolean) => void;
|
||||
setHasChangeTabError: (hasChangesError: boolean) => void;
|
||||
closeModal: () => void;
|
||||
collapseModal: () => void;
|
||||
team?: Team;
|
||||
team: Team;
|
||||
};
|
||||
|
||||
const TeamSettings = ({
|
||||
activeTab = '',
|
||||
activeSection = '',
|
||||
updateSection,
|
||||
closeModal,
|
||||
collapseModal,
|
||||
team,
|
||||
hasChanges,
|
||||
hasChangeTabError,
|
||||
setHasChanges,
|
||||
setHasChangeTabError,
|
||||
}: Props): JSX.Element | null => {
|
||||
if (!team) {
|
||||
return null;
|
||||
@ -30,17 +35,30 @@ const TeamSettings = ({
|
||||
|
||||
let result;
|
||||
switch (activeTab) {
|
||||
case 'general':
|
||||
case 'info':
|
||||
result = (
|
||||
<div>
|
||||
<GeneralTab
|
||||
team={team}
|
||||
activeSection={activeSection}
|
||||
updateSection={updateSection}
|
||||
closeModal={closeModal}
|
||||
collapseModal={collapseModal}
|
||||
/>
|
||||
</div>
|
||||
<InfoTab
|
||||
team={team}
|
||||
hasChanges={hasChanges}
|
||||
setHasChanges={setHasChanges}
|
||||
hasChangeTabError={hasChangeTabError}
|
||||
setHasChangeTabError={setHasChangeTabError}
|
||||
closeModal={closeModal}
|
||||
collapseModal={collapseModal}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'access':
|
||||
result = (
|
||||
<AccessTab
|
||||
team={team}
|
||||
hasChanges={hasChanges}
|
||||
setHasChanges={setHasChanges}
|
||||
hasChangeTabError={hasChangeTabError}
|
||||
setHasChangeTabError={setHasChangeTabError}
|
||||
closeModal={closeModal}
|
||||
collapseModal={collapseModal}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
|
@ -1,93 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/team_settings_modal should match snapshot 1`] = `
|
||||
<Modal
|
||||
animation={true}
|
||||
aria-labelledby="teamSettingsModalLabel"
|
||||
autoFocus={true}
|
||||
backdrop={true}
|
||||
bsClass="modal"
|
||||
dialogClassName="a11y__modal settings-modal settings-modal--action"
|
||||
dialogComponentClass={[Function]}
|
||||
enforceFocus={true}
|
||||
id="teamSettingsModal"
|
||||
keyboard={true}
|
||||
manager={
|
||||
ModalManager {
|
||||
"add": [Function],
|
||||
"containers": Array [],
|
||||
"data": Array [],
|
||||
"handleContainerOverflow": true,
|
||||
"hideSiblingNodes": true,
|
||||
"isTopModal": [Function],
|
||||
"modals": Array [],
|
||||
"remove": [Function],
|
||||
}
|
||||
}
|
||||
onExited={[Function]}
|
||||
onHide={[Function]}
|
||||
renderBackdrop={[Function]}
|
||||
restoreFocus={true}
|
||||
role="dialog"
|
||||
show={true}
|
||||
>
|
||||
<ModalHeader
|
||||
bsClass="modal-header"
|
||||
closeButton={true}
|
||||
closeLabel="Close"
|
||||
id="teamSettingsModalLabel"
|
||||
>
|
||||
<ModalTitle
|
||||
bsClass="modal-title"
|
||||
componentClass="h1"
|
||||
>
|
||||
<MemoizedFormattedMessage
|
||||
defaultMessage="Team Settings"
|
||||
id="team_settings_modal.title"
|
||||
/>
|
||||
</ModalTitle>
|
||||
</ModalHeader>
|
||||
<ModalBody
|
||||
bsClass="modal-body"
|
||||
componentClass="div"
|
||||
>
|
||||
<div
|
||||
className="settings-table"
|
||||
>
|
||||
<div
|
||||
className="settings-links"
|
||||
>
|
||||
<Suspense
|
||||
fallback={null}
|
||||
>
|
||||
<lazy
|
||||
activeTab="general"
|
||||
tabs={
|
||||
Array [
|
||||
Object {
|
||||
"icon": "icon icon-settings-outline",
|
||||
"iconTitle": "Settings Icon",
|
||||
"name": "general",
|
||||
"uiName": "General",
|
||||
},
|
||||
]
|
||||
}
|
||||
updateTab={[Function]}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
<div
|
||||
className="settings-content minimize-settings"
|
||||
>
|
||||
<Connect(TeamSettings)
|
||||
activeSection=""
|
||||
activeTab="general"
|
||||
closeModal={[Function]}
|
||||
collapseModal={[Function]}
|
||||
updateSection={[Function]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
`;
|
@ -3,8 +3,6 @@
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getLicense} from 'mattermost-redux/selectors/entities/general';
|
||||
|
||||
import {isModalOpen} from 'selectors/views/modals';
|
||||
|
||||
import {ModalIdentifiers} from 'utils/constants';
|
||||
@ -17,7 +15,6 @@ function mapStateToProps(state: GlobalState) {
|
||||
const modalId = ModalIdentifiers.TEAM_SETTINGS;
|
||||
return {
|
||||
show: isModalOpen(state, modalId),
|
||||
isCloud: getLicense(state).Cloud === 'true',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,35 +1,29 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import {fireEvent, screen} from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import TeamSettingsModal from 'components/team_settings_modal/team_settings_modal';
|
||||
|
||||
import {renderWithContext} from 'tests/react_testing_utils';
|
||||
|
||||
describe('components/team_settings_modal', () => {
|
||||
const baseProps = {
|
||||
isCloud: false,
|
||||
onExited: jest.fn(),
|
||||
};
|
||||
|
||||
test('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
test('should hide the modal when the close button is clicked', async () => {
|
||||
renderWithContext(
|
||||
<TeamSettingsModal
|
||||
{...baseProps}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should call onExited callback when the modal is hidden', () => {
|
||||
const wrapper = shallow(
|
||||
<TeamSettingsModal
|
||||
{...baseProps}
|
||||
/>,
|
||||
);
|
||||
|
||||
(wrapper.instance() as TeamSettingsModal).handleHidden();
|
||||
expect(baseProps.onExited).toHaveBeenCalledTimes(1);
|
||||
const modal = screen.getByRole('dialog', {name: 'Close Team Settings'});
|
||||
expect(modal.className).toBe('fade in modal');
|
||||
fireEvent.click(screen.getByText('Close'));
|
||||
expect(modal.className).toBe('fade modal');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,126 +1,101 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import React, {useState, useRef, useCallback} from 'react';
|
||||
import {Modal, type ModalBody} from 'react-bootstrap';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import TeamSettings from 'components/team_settings';
|
||||
|
||||
import * as Utils from 'utils/utils';
|
||||
|
||||
const SettingsSidebar = React.lazy(() => import('components/settings_sidebar'));
|
||||
|
||||
type Props = {
|
||||
onExited: () => void;
|
||||
isCloud?: boolean;
|
||||
}
|
||||
|
||||
export type State = {
|
||||
activeTab: string;
|
||||
activeSection: string;
|
||||
show: boolean;
|
||||
}
|
||||
const TeamSettingsModal = ({onExited}: 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 modalBodyRef = useRef<ModalBody>(null);
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
export default class TeamSettingsModal extends React.PureComponent<Props, State> {
|
||||
modalBodyRef: React.RefObject<Modal>;
|
||||
const updateTab = useCallback((tab: string) => {
|
||||
if (hasChanges) {
|
||||
setHasChangeTabError(true);
|
||||
return;
|
||||
}
|
||||
setActiveTab(tab);
|
||||
setHasChanges(false);
|
||||
setHasChangeTabError(false);
|
||||
}, [hasChanges]);
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const handleHide = useCallback(() => setShow(false), []);
|
||||
|
||||
this.state = {
|
||||
activeTab: 'general',
|
||||
activeSection: '',
|
||||
show: true,
|
||||
};
|
||||
const handleClose = useCallback(() => {
|
||||
setActiveTab('info');
|
||||
setHasChanges(false);
|
||||
setHasChangeTabError(false);
|
||||
onExited();
|
||||
}, [onExited]);
|
||||
|
||||
this.modalBodyRef = React.createRef();
|
||||
}
|
||||
const handleCollapse = useCallback(() => {
|
||||
const el = ReactDOM.findDOMNode(modalBodyRef.current) as HTMLDivElement;
|
||||
el?.closest('.modal-dialog')!.classList.remove('display--content');
|
||||
setActiveTab('');
|
||||
}, []);
|
||||
|
||||
updateTab = (tab: string) => {
|
||||
this.setState({
|
||||
activeTab: tab,
|
||||
activeSection: '',
|
||||
});
|
||||
};
|
||||
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: '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'})},
|
||||
];
|
||||
|
||||
updateSection = (section: string) => {
|
||||
this.setState({activeSection: section});
|
||||
};
|
||||
|
||||
collapseModal = () => {
|
||||
const el = ReactDOM.findDOMNode(this.modalBodyRef.current) as HTMLDivElement;
|
||||
const modalDialog = el.closest('.modal-dialog');
|
||||
modalDialog?.classList.remove('display--content');
|
||||
|
||||
this.setState({
|
||||
activeTab: '',
|
||||
activeSection: '',
|
||||
});
|
||||
};
|
||||
|
||||
handleHide = () => {
|
||||
this.setState({show: false});
|
||||
};
|
||||
|
||||
// called after the dialog is fully hidden and faded out
|
||||
handleHidden = () => {
|
||||
this.setState({
|
||||
activeTab: 'general',
|
||||
activeSection: '',
|
||||
});
|
||||
this.props.onExited();
|
||||
};
|
||||
|
||||
render() {
|
||||
const tabs = [];
|
||||
tabs.push({name: 'general', uiName: Utils.localizeMessage('team_settings_modal.generalTab', 'General'), icon: 'icon icon-settings-outline', iconTitle: Utils.localizeMessage('generic_icons.settings', 'Settings Icon')});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
dialogClassName='a11y__modal settings-modal settings-modal--action'
|
||||
show={this.state.show}
|
||||
onHide={this.handleHide}
|
||||
onExited={this.handleHidden}
|
||||
role='dialog'
|
||||
aria-labelledby='teamSettingsModalLabel'
|
||||
id='teamSettingsModal'
|
||||
return (
|
||||
<Modal
|
||||
dialogClassName='a11y__modal settings-modal'
|
||||
show={show}
|
||||
onHide={handleHide}
|
||||
onExited={handleClose}
|
||||
role='dialog'
|
||||
aria-labelledby='teamSettingsModalLabel'
|
||||
id='teamSettingsModal'
|
||||
>
|
||||
<Modal.Header
|
||||
id='teamSettingsModalLabel'
|
||||
closeButton={true}
|
||||
>
|
||||
<Modal.Header
|
||||
id='teamSettingsModalLabel'
|
||||
closeButton={true}
|
||||
>
|
||||
<Modal.Title componentClass='h1'>
|
||||
<FormattedMessage
|
||||
id='team_settings_modal.title'
|
||||
defaultMessage='Team Settings'
|
||||
/>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body ref={this.modalBodyRef}>
|
||||
<div className='settings-table'>
|
||||
<div className='settings-links'>
|
||||
<React.Suspense fallback={null}>
|
||||
<SettingsSidebar
|
||||
tabs={tabs}
|
||||
activeTab={this.state.activeTab}
|
||||
updateTab={this.updateTab}
|
||||
/>
|
||||
</React.Suspense>
|
||||
</div>
|
||||
<div className='settings-content minimize-settings'>
|
||||
<TeamSettings
|
||||
activeTab={this.state.activeTab}
|
||||
activeSection={this.state.activeSection}
|
||||
updateSection={this.updateSection}
|
||||
closeModal={this.handleHide}
|
||||
collapseModal={this.collapseModal}
|
||||
<Modal.Title componentClass='h1'>
|
||||
{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
|
||||
tabs={tabs}
|
||||
activeTab={activeTab}
|
||||
updateTab={updateTab}
|
||||
/>
|
||||
</div>
|
||||
</React.Suspense>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
<div className='settings-content minimize-settings'>
|
||||
<TeamSettings
|
||||
activeTab={activeTab}
|
||||
hasChanges={hasChanges}
|
||||
setHasChanges={setHasChanges}
|
||||
hasChangeTabError={hasChangeTabError}
|
||||
setHasChangeTabError={setHasChangeTabError}
|
||||
closeModal={handleHide}
|
||||
collapseModal={handleCollapse}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamSettingsModal;
|
||||
|
@ -17,6 +17,10 @@
|
||||
.Input_container {
|
||||
width: 100%;
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.Input {
|
||||
position: relative;
|
||||
left: 0;
|
||||
@ -58,6 +62,10 @@
|
||||
> :not(:first-child) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.Input_limit-exceeded {
|
||||
|
@ -21,7 +21,7 @@ export enum SIZE {
|
||||
|
||||
export type CustomMessageInputType = {type: 'info' | 'error' | 'warning' | 'success'; value: React.ReactNode} | null;
|
||||
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement | HTMLTextAreaElement> {
|
||||
required?: boolean;
|
||||
hasError?: boolean;
|
||||
addon?: React.ReactElement;
|
||||
@ -71,7 +71,7 @@ const Input = React.forwardRef((
|
||||
onClear,
|
||||
...otherProps
|
||||
}: InputProps,
|
||||
ref?: React.Ref<HTMLInputElement>,
|
||||
ref?: React.Ref<HTMLInputElement | HTMLTextAreaElement>,
|
||||
) => {
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
@ -94,7 +94,7 @@ const Input = React.forwardRef((
|
||||
}
|
||||
}, [customMessage]);
|
||||
|
||||
const handleOnFocus = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||
const handleOnFocus = (event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setFocused(true);
|
||||
|
||||
if (onFocus) {
|
||||
@ -102,7 +102,7 @@ const Input = React.forwardRef((
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnBlur = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||
const handleOnBlur = (event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setFocused(false);
|
||||
validateInput();
|
||||
|
||||
@ -111,7 +111,7 @@ const Input = React.forwardRef((
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleOnChange = (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setCustomInputLabel(null);
|
||||
|
||||
if (onChange) {
|
||||
@ -157,6 +157,45 @@ const Input = React.forwardRef((
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const generateInput = () => {
|
||||
if (otherProps.type === 'textarea') {
|
||||
return (
|
||||
<textarea
|
||||
ref={ref as React.RefObject<HTMLTextAreaElement>}
|
||||
id={`input_${name || ''}`}
|
||||
className={classNames('Input form-control', inputSize, inputClassName, {Input__focus: showLegend})}
|
||||
value={value}
|
||||
placeholder={focused ? (label && placeholder) || label : label || placeholder}
|
||||
aria-label={label || placeholder}
|
||||
rows={3}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
{...otherProps}
|
||||
maxLength={limit ? undefined : maxLength}
|
||||
onFocus={handleOnFocus}
|
||||
onBlur={handleOnBlur}
|
||||
onChange={handleOnChange}
|
||||
/>);
|
||||
}
|
||||
return (
|
||||
<input
|
||||
ref={ref as React.RefObject<HTMLInputElement>}
|
||||
id={`input_${name || ''}`}
|
||||
className={classNames('Input form-control', inputSize, inputClassName, {Input__focus: showLegend})}
|
||||
value={value}
|
||||
placeholder={focused ? (label && placeholder) || label : label || placeholder}
|
||||
aria-label={label || placeholder}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
{...otherProps}
|
||||
maxLength={limit ? undefined : maxLength}
|
||||
onFocus={handleOnFocus}
|
||||
onBlur={handleOnBlur}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames('Input_container', containerClassName, {disabled})}>
|
||||
<fieldset
|
||||
@ -173,21 +212,7 @@ const Input = React.forwardRef((
|
||||
<div className={classNames('Input_wrapper', wrapperClassName)}>
|
||||
{inputPrefix}
|
||||
{textPrefix && <span>{textPrefix}</span>}
|
||||
<input
|
||||
ref={ref}
|
||||
id={`input_${name || ''}`}
|
||||
className={classNames('Input form-control', inputSize, inputClassName, {Input__focus: showLegend})}
|
||||
value={value}
|
||||
placeholder={focused ? (label && placeholder) || label : label || placeholder}
|
||||
aria-label={label || placeholder}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
{...otherProps}
|
||||
maxLength={limit ? undefined : maxLength}
|
||||
onFocus={handleOnFocus}
|
||||
onBlur={handleOnBlur}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
{generateInput()}
|
||||
{limitExceeded > 0 && (
|
||||
<span className='Input_limit-exceeded'>
|
||||
{'-'}{limitExceeded}
|
||||
|
@ -24,8 +24,8 @@ type URLInputProps = {
|
||||
shortenLength?: number;
|
||||
error?: string;
|
||||
className?: string;
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onBlur?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
||||
onBlur?: (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
||||
};
|
||||
|
||||
function UrlInput({
|
||||
@ -55,7 +55,7 @@ function UrlInput({
|
||||
const isShortenedURL = shortenLength && fullURL.length > shortenLength;
|
||||
const hasError = Boolean(error);
|
||||
|
||||
const handleOnInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleOnInputChange = (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (onChange) {
|
||||
@ -63,7 +63,7 @@ function UrlInput({
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnInputBlur = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleOnInputBlur = (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
setEditing(hasError);
|
||||
|
@ -9,11 +9,11 @@
|
||||
padding: 0;
|
||||
margin: 0 0 8px;
|
||||
color: var(--center-channel-color);
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 14px;
|
||||
font-family: 'Metropolis', sans-serif;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
&__description {
|
||||
@ -29,6 +29,24 @@
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
&__error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
margin: 12px 0 0 0;
|
||||
color: var(--error-text);
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -45,9 +63,12 @@
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
gap: 8px;
|
||||
line-height: 20px;
|
||||
|
||||
span {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
&__fieldset-radio {
|
||||
|
@ -1,42 +1,76 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import type {PrimitiveType, FormatXMLElementFn} from 'intl-messageformat';
|
||||
import React from 'react';
|
||||
import type {MessageDescriptor} from 'react-intl';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import {AlertCircleOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
|
||||
import './base_setting_item.scss';
|
||||
|
||||
type ExtendedMessageDescriptor = MessageDescriptor & {
|
||||
values?: Record<string, PrimitiveType | FormatXMLElementFn<string, string>>;
|
||||
};
|
||||
|
||||
export type BaseSettingItemProps = {
|
||||
title?: MessageDescriptor;
|
||||
description?: MessageDescriptor;
|
||||
title?: ExtendedMessageDescriptor;
|
||||
description?: ExtendedMessageDescriptor;
|
||||
error?: ExtendedMessageDescriptor;
|
||||
};
|
||||
|
||||
type Props = BaseSettingItemProps & {
|
||||
content: JSX.Element;
|
||||
className?: string;
|
||||
descriptionAboveContent?: boolean;
|
||||
}
|
||||
|
||||
function BaseSettingItem({title, description, content}: Props): JSX.Element {
|
||||
function BaseSettingItem({title, description, content, className, error, descriptionAboveContent = false}: Props): JSX.Element {
|
||||
const {formatMessage} = useIntl();
|
||||
const Title = title && (
|
||||
<h4 className='mm-modal-generic-section-item__title'>
|
||||
{formatMessage({id: title.id, defaultMessage: title.defaultMessage})}
|
||||
<h4
|
||||
data-testid='mm-modal-generic-section-item__title'
|
||||
className='mm-modal-generic-section-item__title'
|
||||
>
|
||||
{formatMessage({id: title.id, defaultMessage: title.defaultMessage}, title.values)}
|
||||
</h4>
|
||||
);
|
||||
|
||||
const Description = description && (
|
||||
<p className='mm-modal-generic-section-item__description'>
|
||||
{formatMessage({id: description.id, defaultMessage: description.defaultMessage})}
|
||||
<p
|
||||
data-testid='mm-modal-generic-section-item__description'
|
||||
className='mm-modal-generic-section-item__description'
|
||||
>
|
||||
{formatMessage({id: description.id, defaultMessage: description.defaultMessage}, description.values)}
|
||||
</p>
|
||||
);
|
||||
|
||||
const Error = error && (
|
||||
<div
|
||||
data-testid='mm-modal-generic-section-item__error'
|
||||
className='mm-modal-generic-section-item__error'
|
||||
>
|
||||
<AlertCircleOutlineIcon/>
|
||||
{formatMessage({id: error.id, defaultMessage: error.defaultMessage}, error.values)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const getClassName = classNames('mm-modal-generic-section-item', className);
|
||||
|
||||
return (
|
||||
<div className='mm-modal-generic-section-item'>
|
||||
<div className={getClassName}>
|
||||
{Title}
|
||||
<div className='mm-modal-generic-section-item__content'>
|
||||
{descriptionAboveContent ? Description : undefined}
|
||||
<div
|
||||
data-testid='mm-modal-generic-section-item__content'
|
||||
className='mm-modal-generic-section-item__content'
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
{Description}
|
||||
{descriptionAboveContent ? undefined : Description}
|
||||
{Error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -18,6 +18,8 @@ type Props = BaseSettingItemProps & {
|
||||
inputFieldData: FieldsetCheckbox;
|
||||
inputFieldValue: boolean;
|
||||
handleChange: (e: boolean) => void;
|
||||
className?: string;
|
||||
descriptionAboveContent?: boolean;
|
||||
}
|
||||
function CheckboxSettingItem({
|
||||
title,
|
||||
@ -25,6 +27,8 @@ function CheckboxSettingItem({
|
||||
inputFieldData,
|
||||
inputFieldValue,
|
||||
handleChange,
|
||||
className,
|
||||
descriptionAboveContent = false,
|
||||
}: Props): JSX.Element {
|
||||
const content = (
|
||||
<fieldset
|
||||
@ -53,6 +57,8 @@ function CheckboxSettingItem({
|
||||
content={content}
|
||||
title={title}
|
||||
description={description}
|
||||
className={className}
|
||||
descriptionAboveContent={descriptionAboveContent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
.mm-modal-generic-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
&__info-ctr {
|
||||
display: flex;
|
||||
|
@ -8,7 +8,7 @@ import {useIntl} from 'react-intl';
|
||||
import './modal_section.scss';
|
||||
|
||||
type Props = {
|
||||
title: MessageDescriptor;
|
||||
title?: MessageDescriptor;
|
||||
description?: MessageDescriptor;
|
||||
content: JSX.Element;
|
||||
titleSuffix?: JSX.Element;
|
||||
@ -21,11 +21,11 @@ function ModalSection({
|
||||
titleSuffix,
|
||||
}: Props): JSX.Element {
|
||||
const {formatMessage} = useIntl();
|
||||
const titleContent = (
|
||||
const titleContent = title ? (
|
||||
<h4 className='mm-modal-generic-section__title'>
|
||||
{formatMessage({id: title.id, defaultMessage: title.defaultMessage})}
|
||||
</h4>
|
||||
);
|
||||
) : undefined;
|
||||
|
||||
const descriptionContent = description && (
|
||||
<p className='mm-modal-generic-section__description'>
|
||||
@ -43,12 +43,21 @@ function ModalSection({
|
||||
return titleContent;
|
||||
}
|
||||
|
||||
const titleDescriptionSection = () => {
|
||||
if (title || description) {
|
||||
return (
|
||||
<div className='mm-modal-generic-section__title-description-ctr'>
|
||||
{titleRow()}
|
||||
{descriptionContent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<section className='mm-modal-generic-section'>
|
||||
<div className='mm-modal-generic-section__info-ctr'>
|
||||
{titleRow()}
|
||||
{descriptionContent}
|
||||
</div>
|
||||
{titleDescriptionSection()}
|
||||
<div className='mm-modal-generic-section__content'>
|
||||
{content}
|
||||
</div>
|
||||
|
@ -1,27 +1,72 @@
|
||||
.mm-save-changes-panel {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
display: flex;
|
||||
width: calc(100% - 232px - 40px);
|
||||
width: calc(63%);
|
||||
align-items: center;
|
||||
padding: 18px;
|
||||
background-color: var(--dnd-indicator);
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||
animation-duration: 300ms;
|
||||
animation-fill-mode: forwards;
|
||||
animation-name: entry-animation;
|
||||
animation-timing-function: ease;
|
||||
background: var(--center-channel-bg);
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--elevation-3);
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
transition: all 300ms ease;
|
||||
|
||||
@keyframes entry-animation {
|
||||
from {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
width: calc(95%);
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: var(--dnd-indicator);
|
||||
background: var(--dnd-indicator);
|
||||
}
|
||||
|
||||
&.saved {
|
||||
border-color: var(--online-indicator);
|
||||
background: var(--online-indicator);
|
||||
}
|
||||
|
||||
&__message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
color: var(--sidebar-text);
|
||||
color: var(--center-channel-color);
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
|
||||
span {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
svg {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||
}
|
||||
|
||||
&.error,
|
||||
&.saved {
|
||||
color: var(--button-color);
|
||||
|
||||
svg {
|
||||
color: var(--button-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__btn-ctr {
|
||||
@ -29,34 +74,21 @@
|
||||
flex: 1 1 auto;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
|
||||
#panelCloseButton {
|
||||
color: rgba(var(--button-color-rgb), 0.64);
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&__cancel-btn {
|
||||
display: flex;
|
||||
width: 64px;
|
||||
min-width: 64px;
|
||||
height: 32px;
|
||||
align-items: center;
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-radius: 4px;
|
||||
color: var(--sidebar-text);
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
gap: 10px;
|
||||
line-height: 10px;
|
||||
}
|
||||
|
||||
&__save-btn {
|
||||
display: flex;
|
||||
width: 59px;
|
||||
height: 32px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
background: var(--sidebar-text);
|
||||
background: rgba(var(--denim-button-bg-rgb), 0.08);
|
||||
border-radius: 4px;
|
||||
color: var(--denim-button-bg);
|
||||
font-family: inherit;
|
||||
@ -64,5 +96,33 @@
|
||||
font-weight: 600;
|
||||
gap: 10px;
|
||||
line-height: 10px;
|
||||
|
||||
&.error {
|
||||
background: rgba(var(--button-color-rgb), 0.12);
|
||||
color: var(--button-color);
|
||||
}
|
||||
}
|
||||
|
||||
&__save-btn {
|
||||
display: flex;
|
||||
min-width: 59px;
|
||||
height: 32px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
background: var(--denim-button-bg);
|
||||
border-radius: 4px;
|
||||
color: var(--button-color);
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
gap: 10px;
|
||||
line-height: 10px;
|
||||
|
||||
&.error {
|
||||
background-color: var(--button-color);
|
||||
color: var(--denim-button-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,32 +1,91 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import React, {useEffect} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import './save_changes_panel.scss';
|
||||
import {AlertCircleOutlineIcon} from '@mattermost/compass-icons/components';
|
||||
|
||||
import './save_changes_panel.scss';
|
||||
|
||||
export type SaveChangesPanelState = 'editing' | 'saved' | 'error' | undefined;
|
||||
|
||||
type Props = {
|
||||
handleSubmit: () => void;
|
||||
handleCancel: () => void;
|
||||
handleClose: () => void;
|
||||
tabChangeError?: boolean;
|
||||
state: SaveChangesPanelState;
|
||||
}
|
||||
function SaveChangesPanel({handleSubmit, handleCancel}: Props) {
|
||||
return (
|
||||
<div className='mm-save-changes-panel'>
|
||||
<p className='mm-save-changes-panel__message'>
|
||||
<AlertCircleOutlineIcon
|
||||
size={18}
|
||||
color={'currentcolor'}
|
||||
/>
|
||||
function SaveChangesPanel({handleSubmit, handleCancel, handleClose, tabChangeError = false, state = 'editing'}: Props) {
|
||||
const panelClassName = classNames('mm-save-changes-panel', {error: tabChangeError || state === 'error'}, {saved: state === 'saved'});
|
||||
const messageClassName = classNames('mm-save-changes-panel__message', {error: tabChangeError || state === 'error'}, {saved: state === 'saved'});
|
||||
const cancelButtonClassName = classNames('mm-save-changes-panel__cancel-btn', {error: tabChangeError || state === 'error'}, {saved: state === 'saved'});
|
||||
const saveButtonClassName = classNames('mm-save-changes-panel__save-btn', {error: tabChangeError || state === 'error'}, {saved: state === 'saved'});
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
if (state === 'saved') {
|
||||
timeoutId = setTimeout(() => {
|
||||
handleClose();
|
||||
}, 1200);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [handleClose, state]);
|
||||
|
||||
const generateMessage = () => {
|
||||
if (tabChangeError || state === 'editing') {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='saveChangesPanel.message'
|
||||
defaultMessage='You have unsaved changes'
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
if (state === 'error') {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='saveChangesPanel.error'
|
||||
defaultMessage='There was an error saving your settings'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='saveChangesPanel.saved'
|
||||
defaultMessage='Settings saved'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const generateControlButtons = () => {
|
||||
if (state === 'saved') {
|
||||
return (
|
||||
<div className='mm-save-changes-panel__btn-ctr'>
|
||||
<button
|
||||
id='panelCloseButton'
|
||||
data-testid='panelCloseButton'
|
||||
type='button'
|
||||
className='btn btn-icon btn-sm'
|
||||
onClick={handleClose}
|
||||
>
|
||||
<i
|
||||
className='icon icon-close'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mm-save-changes-panel__btn-ctr'>
|
||||
<button
|
||||
className='mm-save-changes-panel__cancel-btn'
|
||||
data-testid='mm-save-changes-panel__cancel-btn'
|
||||
className={cancelButtonClassName}
|
||||
onClick={handleCancel}
|
||||
>
|
||||
<FormattedMessage
|
||||
@ -35,15 +94,35 @@ function SaveChangesPanel({handleSubmit, handleCancel}: Props) {
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
className='mm-save-changes-panel__save-btn'
|
||||
data-testid='mm-save-changes-panel__save-btn'
|
||||
className={saveButtonClassName}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='saveChangesPanel.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
{state === 'error' ?
|
||||
<FormattedMessage
|
||||
id='saveChangesPanel.tryAgain'
|
||||
defaultMessage='Try again'
|
||||
/> :
|
||||
<FormattedMessage
|
||||
id='saveChangesPanel.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={panelClassName}>
|
||||
<p className={messageClassName}>
|
||||
<AlertCircleOutlineIcon
|
||||
size={18}
|
||||
color={'currentcolor'}
|
||||
/>
|
||||
{generateMessage()}
|
||||
</p>
|
||||
{generateControlButtons()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -3716,34 +3716,26 @@
|
||||
"general_button.esc": "Esc",
|
||||
"general_tab.allowedDomains": "Allow only users with a specific email domain to join this team",
|
||||
"general_tab.allowedDomains.ariaLabel": "Allowed Domains",
|
||||
"general_tab.allowedDomainsEdit": "Click 'Edit' to add an email domain whitelist.",
|
||||
"general_tab.AllowedDomainsExample": "corp.mattermost.com, mattermost.com",
|
||||
"general_tab.AllowedDomainsInfo": "Users can only join the team if their email matches a specific domain (e.g. \"mattermost.com\") or list of comma-separated domains (e.g. \"corp.mattermost.com, mattermost.com\").",
|
||||
"general_tab.chooseDescription": "Please choose a new description for your team",
|
||||
"general_tab.codeDesc": "Click 'Edit' to regenerate Invite Code.",
|
||||
"general_tab.AllowedDomainsInfo": "When enabled, users can only join the team if their email matches a specific domain (e.g. \"mattermost.org\")",
|
||||
"general_tab.AllowedDomainsTip": "Seperate multiple domains with a space, comma, tab or enter.",
|
||||
"general_tab.AllowedDomainsTitle": "Users with a specific email domain",
|
||||
"general_tab.codeLongDesc": "The Invite Code is part of the unique team invitation link which is sent to members you’re inviting to this team. Regenerating the code creates a new invitation link and invalidates the previous link.",
|
||||
"general_tab.codeTitle": "Invite Code",
|
||||
"general_tab.emptyDescription": "Click 'Edit' to add a team description.",
|
||||
"general_tab.getTeamInviteLink": "Get Team Invite Link",
|
||||
"general_tab.no": "No",
|
||||
"general_tab.openInviteDesc": "When allowed, a link to this team will be included on the landing page allowing anyone with an account to join this team. Changing from \"Yes\" to \"No\" will regenerate the invitation code, create a new invitation link and invalidate the previous link.",
|
||||
"general_tab.openInviteDesc": "When allowed, a link to this team will be included on the landing page allowing anyone with an account to join this team. Changing from 'Yes' to 'No' will regenerate the invitation code, create a new invitation link and invalidate the previous link.",
|
||||
"general_tab.openInviteText": "Users on this server",
|
||||
"general_tab.openInviteTitle": "Allow any user with an account on this server to join this team",
|
||||
"general_tab.regenerate": "Regenerate",
|
||||
"general_tab.required": "This field is required",
|
||||
"general_tab.teamDescription": "Team Description",
|
||||
"general_tab.teamDescription": "Description",
|
||||
"general_tab.teamDescriptionInfo": "Team description provides additional information to help users select the right team. Maximum of 50 characters.",
|
||||
"general_tab.teamIcon": "Team Icon",
|
||||
"general_tab.teamIconEditHint": "Click 'Edit' to upload an image.",
|
||||
"general_tab.teamIconEditHintMobile": "Click to upload an image",
|
||||
"general_tab.teamIconError": "An error occurred while selecting the image.",
|
||||
"general_tab.teamIconInvalidFileType": "Only BMP, JPG or PNG images may be used for team icons",
|
||||
"general_tab.teamIconLastUpdated": "Image last updated {date}",
|
||||
"general_tab.teamIconTooLarge": "Unable to upload team icon. File is too large.",
|
||||
"general_tab.teamInfo": "Team info",
|
||||
"general_tab.teamName": "Team Name",
|
||||
"general_tab.teamNameInfo": "Set the name of the team as it appears on your sign-in screen and at the top of the left-hand sidebar.",
|
||||
"general_tab.teamNameInfo": "This name will appear on your sign-in screen and at the top of the left sidebar.",
|
||||
"general_tab.teamNameRestrictions": "Team Name must be {min} or more characters up to a maximum of {max}. You can add a longer team description.",
|
||||
"general_tab.title": "General Settings",
|
||||
"general_tab.yes": "Yes",
|
||||
"generic_btn.cancel": "Cancel",
|
||||
"generic_btn.save": "Save",
|
||||
"generic_icons.add": "Add Icon",
|
||||
@ -3796,7 +3788,6 @@
|
||||
"generic_icons.reload": "Reload Icon",
|
||||
"generic_icons.reply": "Reply Icon",
|
||||
"generic_icons.search": "Search Icon",
|
||||
"generic_icons.settings": "Settings Icon",
|
||||
"generic_icons.success": "Success Icon",
|
||||
"generic_icons.upgradeBadge": "Upgrade badge",
|
||||
"generic_icons.upload": "Upload Icon",
|
||||
@ -4800,8 +4791,11 @@
|
||||
"save_button.save": "Save",
|
||||
"save_button.saving": "Saving",
|
||||
"saveChangesPanel.cancel": "Undo",
|
||||
"saveChangesPanel.error": "There was an error saving your settings",
|
||||
"saveChangesPanel.message": "You have unsaved changes",
|
||||
"saveChangesPanel.save": "Save",
|
||||
"saveChangesPanel.saved": "Settings saved",
|
||||
"saveChangesPanel.tryAgain": "Try again",
|
||||
"search_bar.files_tab": "Files",
|
||||
"search_bar.messages_tab": "Messages",
|
||||
"search_bar.search": "Search",
|
||||
@ -4904,11 +4898,12 @@
|
||||
"setting_picture.cancel": "Cancel",
|
||||
"setting_picture.help.profile": "Upload a picture in BMP, JPG, JPEG, or PNG format. Maximum file size: {max}",
|
||||
"setting_picture.help.profile.example": "Upload a picture in BMP, JPG or PNG format. Maximum file size: {max}",
|
||||
"setting_picture.help.team": "Upload a team icon in BMP, JPG or PNG format.\nSquare images with a solid background color are recommended.",
|
||||
"setting_picture.remove": "Remove This Icon",
|
||||
"setting_picture.remove_image": "Remove image",
|
||||
"setting_picture.remove_profile_picture": "Remove Profile Picture",
|
||||
"setting_picture.save": "Save",
|
||||
"setting_picture.select": "Select",
|
||||
"setting_picture.title": "Team Icon",
|
||||
"setting_picture.uploading": "Uploading...",
|
||||
"shared_channel_indicator.tooltip": "Shared with trusted organizations",
|
||||
"shared_user_indicator.aria_label": "shared user indicator",
|
||||
@ -5258,11 +5253,11 @@
|
||||
"team_members_dropdown.teamAdmin": "Team Admin",
|
||||
"team_members_dropdown.teamAdmins": "Team Admins",
|
||||
"team_members_dropdown.teamMembers": "Team Members",
|
||||
"team_settings_modal.generalTab": "General",
|
||||
"team_settings_modal.accessTab": "Access",
|
||||
"team_settings_modal.infoTab": "Info",
|
||||
"team_settings_modal.title": "Team Settings",
|
||||
"team_settings.openInviteDescription.ariaLabel": "Invite Code",
|
||||
"team_settings.openInviteDescription.groupConstrained": "No, members of this team are added and removed by linked groups. <link>Learn More</link>",
|
||||
"team_settings.openInviteSetting.groupConstrained": "No, members of this team are added and removed by linked groups.",
|
||||
"team_settings.openInviteDescription.error": "There was an error generating the invite code, please try again",
|
||||
"team_settings.openInviteDescription.groupConstrained": "Members of this team are added and removed by linked groups. <link>Learn More</link>",
|
||||
"team_sidebar.join": "Other teams you can join",
|
||||
"team.button.ariaLabel": "{teamName} team",
|
||||
"team.button.mentions.ariaLabel": "{teamName} team, {mentionCount} mentions",
|
||||
|
@ -44,6 +44,8 @@
|
||||
}
|
||||
|
||||
.settings-table {
|
||||
min-height: 475px;
|
||||
|
||||
.settings-links {
|
||||
width: 232px;
|
||||
padding: 16px;
|
||||
|
@ -234,6 +234,7 @@
|
||||
top: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
background-color: var(--center-channel-bg);
|
||||
|
||||
.modal-title {
|
||||
width: 100%;
|
||||
@ -295,6 +296,8 @@
|
||||
padding: 0;
|
||||
|
||||
.user-settings {
|
||||
overflow: auto;
|
||||
max-height: 100vh;
|
||||
padding: 100px 20px 30px;
|
||||
}
|
||||
}
|
||||
|
@ -212,7 +212,7 @@
|
||||
.settings-content {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
padding: 0 20px 30px;
|
||||
padding: 20px 30px;
|
||||
|
||||
.modal-header {
|
||||
display: none;
|
||||
@ -492,6 +492,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.nav {
|
||||
position: fixed;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.nav-pills {
|
||||
> li {
|
||||
margin: 0;
|
||||
@ -570,6 +575,7 @@
|
||||
}
|
||||
|
||||
.tab-header {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
@ -1046,7 +1046,7 @@ export function defaultImageURLForUser(userId: UserProfile['id']) {
|
||||
}
|
||||
|
||||
// in contrast to Client4.getTeamIconUrl, for ui logic this function returns null if last_team_icon_update is unset
|
||||
export function imageURLForTeam(team: Team & {last_team_icon_update?: number}) {
|
||||
export function imageURLForTeam(team: Team) {
|
||||
return team.last_team_icon_update ? Client4.getTeamIconUrl(team.id, team.last_team_icon_update) : null;
|
||||
}
|
||||
|
||||
|
@ -39,6 +39,7 @@ export type Team = {
|
||||
scheme_id: string;
|
||||
group_constrained: boolean;
|
||||
policy_id?: string | null;
|
||||
last_team_icon_update?: number;
|
||||
};
|
||||
|
||||
export type TeamsState = {
|
||||
|
Loading…
Reference in New Issue
Block a user