diff --git a/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_account_settings_spec.js b/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_account_settings_spec.js index db9270ab68..c7ea056a69 100644 --- a/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_account_settings_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_account_settings_spec.js @@ -95,7 +95,7 @@ describe('Verify Accessibility Support in different sections in Settings and Pro it('MM-T1465_1 Verify Label & Tab behavior in section links', () => { // * Verify aria-label and tab support in section of Account settings modal - cy.uiOpenProfileModal(); + cy.uiOpenProfileModal('Profile Settings'); cy.findByRole('tab', {name: 'profile settings'}).should('be.visible').focus().should('be.focused'); ['profile settings', 'security'].forEach((text) => { // * Verify aria-label on each tab and it supports navigating to the next tab with arrow keys @@ -114,9 +114,10 @@ describe('Verify Accessibility Support in different sections in Settings and Pro it('MM-T1465_2 Verify Accessibility Support in each section in Settings and Profile Dialog', () => { cy.visit(url); + cy.postMessage('hello'); - // # Open account settings modal - cy.uiOpenProfileModal(); + // # Open profile settings modal + cy.uiOpenProfileModal('Profile Settings'); // * Verify if the focus goes to the individual fields in Profile section cy.findByRole('tab', {name: 'profile settings'}).click().tab(); @@ -149,6 +150,7 @@ describe('Verify Accessibility Support in different sections in Settings and Pro it('MM-T1481 Verify Correct Radio button behavior in Settings and Profile', () => { cy.visit(url); + cy.postMessage('hello'); cy.uiOpenSettingsModal(); cy.get('#notificationsButton').click(); @@ -160,7 +162,8 @@ describe('Verify Accessibility Support in different sections in Settings and Pro it('MM-T1482 Input fields in Settings and Profile should read labels', () => { cy.visit(url); - cy.uiOpenProfileModal(); + cy.postMessage('hello'); + cy.uiOpenProfileModal('Profile Settings'); accountSettings.profile.forEach((section) => { if (section.type === 'text') { @@ -178,6 +181,7 @@ describe('Verify Accessibility Support in different sections in Settings and Pro it('MM-T1485 Language dropdown should read labels', () => { cy.visit(url); + cy.postMessage('hello'); cy.uiOpenSettingsModal(); cy.get('#displayButton').click(); @@ -216,9 +220,10 @@ describe('Verify Accessibility Support in different sections in Settings and Pro it('MM-T1488 Profile Picture should read labels', () => { cy.visit(url); + cy.postMessage('hello'); // # Go to Edit Profile picture - cy.uiOpenProfileModal(); + cy.uiOpenProfileModal('Profile Settings'); cy.get('#pictureEdit').click(); // * Verify image alt in profile image @@ -269,9 +274,10 @@ describe('Verify Accessibility Support in different sections in Settings and Pro it('MM-T1496 Security Settings screen should read labels', () => { cy.visit(url); + cy.postMessage('hello'); // # Go to Security Settings - cy.uiOpenProfileModal(); + cy.uiOpenProfileModal('Profile Settings'); cy.get('#securityButton').click(); // * Check Tab behavior in MFA section diff --git a/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_sidebar_spec.ts b/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_sidebar_spec.ts index 9c67b1250b..232f62acdf 100644 --- a/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_sidebar_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/accessibility/accessibility_sidebar_spec.ts @@ -69,7 +69,7 @@ describe('Verify Accessibility Support in Channel Sidebar Navigation', () => { cy.uiGetLHSAddChannelButton().focus().tab().tab({shift: true}); // * Verify if the Plus button has focus - cy.findByRole('button', {name: 'Add Channel Dropdown'}).should('be.focused'); + cy.uiGetLHSAddChannelButton().should('be.focused'); cy.focused().tab(); // * Verify if the Plus button has focus diff --git a/e2e-tests/cypress/tests/integration/channels/account_settings/account_settings_spec.ts b/e2e-tests/cypress/tests/integration/channels/account_settings/account_settings_spec.ts index 8bebdc5ff9..8bcf9ce98a 100644 --- a/e2e-tests/cypress/tests/integration/channels/account_settings/account_settings_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/account_settings/account_settings_spec.ts @@ -21,17 +21,16 @@ describe('Account Settings', () => { offTopic = offTopicUrl; testUser = user; testTeam = team; + + cy.postMessage('hello'); }); }); it('MM-T2049 Account Settings link in own popover', () => { // # Click avatar to open profile popover - cy.get('div.status-wrapper').should('be.visible').click(); + cy.uiOpenProfileModal('Profile Settings'); - // # Click account settings link - cy.get('#accountSettings').should('be.visible').click(); - - // # Check if account settings modal is open + // # Check if profile settings modal is open cy.get('#accountSettingsModal').should('be.visible'); cy.uiClose(); @@ -62,33 +61,22 @@ describe('Account Settings', () => { it('MM-T2074 New email not visible to other users until it has been confirmed', () => { // # Login as admin - cy.apiLogout(); cy.apiAdminLogin(); - // * Set require email verification to true - cy.visit('/admin_console/authentication/email'); - cy.get('[id="EmailSettings.EnableSignInWithEmailtrue"]').should('be.visible').click(); - cy.get('#saveSetting').invoke('attr', 'disabled'). - then((disabled) => { - disabled ? '' : cy.uiSave(); - }); - cy.visit(offTopic); - // # Create user cy.apiCreateUser({prefix: 'test'}).then(({user: newUser}) => { // # Add user to team cy.apiAddUserToTeam(testTeam.id, newUser.id).then(() => { // # Create DM channel cy.apiCreateDirectChannel([testUser.id, newUser.id]).then(({channel}) => { - // # Login to first user - cy.uiLogout(); - cy.uiLogin(testUser); + cy.apiLogin(testUser); cy.visit(offTopic); + cy.postMessage('hello'); // * Update email const oldEMail = testUser.email; const newEMail = 'test@example.com'; - cy.uiOpenProfileModal(); + cy.uiOpenProfileModal('Profile Settings'); cy.get('#emailEdit').should('be.visible').click(); cy.get('#primaryEmail').should('be.visible').type(newEMail); cy.get('#confirmEmail').should('be.visible').type(newEMail); @@ -99,8 +87,7 @@ describe('Account Settings', () => { cy.postMessageAs({sender: testUser, message: `@${newUser.username}`, channelId: channel.id}); // # Login to 2nd user - cy.uiLogout(); - cy.uiLogin(newUser); + cy.apiLogin(newUser); // * Check if email updated cy.visit(`/${testTeam.name}/messages/@${testUser.username}`); diff --git a/e2e-tests/cypress/tests/integration/channels/account_settings/profile/account_settings_position_spec.ts b/e2e-tests/cypress/tests/integration/channels/account_settings/profile/account_settings_position_spec.ts index 31bc03f147..ca9c602388 100644 --- a/e2e-tests/cypress/tests/integration/channels/account_settings/profile/account_settings_position_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/account_settings/profile/account_settings_position_spec.ts @@ -16,6 +16,9 @@ describe('Profile > Profile Settings > Position', () => { cy.apiInitSetup({loginAfter: true}).then(({offTopicUrl}) => { // # Visit off-topic channel cy.visit(offTopicUrl); + + // # Post message in the main channel + cy.postMessage('hello from master hacker'); }); }); @@ -23,20 +26,21 @@ describe('Profile > Profile Settings > Position', () => { const position = 'Master hacker'; // # Open 'Profile' modal and view the default 'Profile Settings' - cy.uiOpenProfileModal().within(() => { + cy.uiOpenProfileModal('Profile Settings').within(() => { // # Open 'Position' setting cy.findByRole('heading', {name: 'Position'}).should('be.visible').click(); // # Enter new 'Position' - cy.findByRole('textbox', {name: 'Position'}).should('be.visible').type(position); + cy.findByRole('textbox', {name: 'Position'}). + should('be.visible'). + and('be.focused'). + type(position). + should('have.value', position); // # Save and close the modal cy.uiSaveAndClose(); }); - // # Post message in the main channel - cy.postMessage('hello from master hacker'); - // # Click on the profile image cy.get('.profile-icon > img').as('profileIconForPopover').click(); @@ -48,7 +52,7 @@ describe('Profile > Profile Settings > Position', () => { const longPosition = 'Master Hacker II'.repeat(8); // # Open 'Profile' modal and view the default 'Profile Settings' - cy.uiOpenProfileModal().within(() => { + cy.uiOpenProfileModal('Profile Settings').within(() => { const minPositionHeader = () => cy.findByRole('heading', {name: 'Position'}); const maxPositionInput = () => cy.findByRole('textbox', {name: 'Position'}); diff --git a/e2e-tests/cypress/tests/integration/channels/account_settings/profile/email_spec.ts b/e2e-tests/cypress/tests/integration/channels/account_settings/profile/email_spec.ts index c0ce921498..44bac52cc8 100644 --- a/e2e-tests/cypress/tests/integration/channels/account_settings/profile/email_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/account_settings/profile/email_spec.ts @@ -53,7 +53,7 @@ describe('Profile > Profile Settings > Email', () => { beforeEach(() => { // # Go to Profile - cy.uiOpenProfileModal(); + cy.uiOpenProfileModal('Profile Settings'); }); afterEach(() => { @@ -174,7 +174,7 @@ describe('Profile > Profile Settings > Email', () => { expect(subject).to.equal(`[${siteName}] Your email address has changed`); }); - cy.uiOpenProfileModal(); + cy.uiOpenProfileModal('Profile Settings'); // * Verify new email address cy.get('#emailDesc').should('be.visible').should('have.text', email); diff --git a/e2e-tests/cypress/tests/integration/channels/account_settings/profile/fullname_edit_spec.ts b/e2e-tests/cypress/tests/integration/channels/account_settings/profile/fullname_edit_spec.ts index 52e5ae2291..c7332c7dd3 100644 --- a/e2e-tests/cypress/tests/integration/channels/account_settings/profile/fullname_edit_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/account_settings/profile/fullname_edit_spec.ts @@ -23,7 +23,7 @@ describe('Profile > Profile Settings > Full Name', () => { it('MM-T2043 Enter first name', () => { // # Go to Profile - cy.uiOpenProfileModal(); + cy.uiOpenProfileModal('Profile Settings'); // # Click "Edit" to the right of "Full Name" cy.get('#nameEdit').should('be.visible').click(); @@ -46,7 +46,7 @@ describe('Profile > Profile Settings > Full Name', () => { it('MM-T2042 Full Name starting blank stays blank', () => { // # Go to Profile - cy.uiOpenProfileModal(); + cy.uiOpenProfileModal('Profile Settings'); // # Click "Edit" to the right of "Full Name" cy.get('#nameEdit').should('be.visible').click(); diff --git a/e2e-tests/cypress/tests/integration/channels/account_settings/profile/fullname_truncate_spec.ts b/e2e-tests/cypress/tests/integration/channels/account_settings/profile/fullname_truncate_spec.ts index eb493a3bbe..78a0f2a8b2 100644 --- a/e2e-tests/cypress/tests/integration/channels/account_settings/profile/fullname_truncate_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/account_settings/profile/fullname_truncate_spec.ts @@ -33,7 +33,7 @@ describe('Profile > Profile Settings> Full Name', () => { it('MM-T2046 Full Name - Truncated in popover', () => { // # Go to Profile -> Profile Settings -> Full Name -> Edit - cy.uiOpenProfileModal(); + cy.uiOpenProfileModal('Profile Settings'); // # Open Full Name section cy.get('#nameDesc').click(); @@ -50,7 +50,7 @@ describe('Profile > Profile Settings> Full Name', () => { // * Full name field shows first and last name. cy.contains('#nameDesc', `${firstName} ${lastName}`); - // # Close account settings modal + // # Close profile settings modal cy.uiClose(); }); diff --git a/e2e-tests/cypress/tests/integration/channels/account_settings/profile/help_text_link_spec.ts b/e2e-tests/cypress/tests/integration/channels/account_settings/profile/help_text_link_spec.ts index 8a1f825e17..fbea02d9ca 100644 --- a/e2e-tests/cypress/tests/integration/channels/account_settings/profile/help_text_link_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/account_settings/profile/help_text_link_spec.ts @@ -20,7 +20,7 @@ describe('Account Settings', () => { it('MM-T2045 Full Name - Link in help text', () => { // # Go to Profile - cy.uiOpenProfileModal(); + cy.uiOpenProfileModal('Profile Settings'); // * Ensure that the Profile tab is loaded cy.get('#generalSettingsTitle').should('be.visible').should('contain', 'Profile'); diff --git a/e2e-tests/cypress/tests/integration/channels/account_settings/profile/nickname_spec.ts b/e2e-tests/cypress/tests/integration/channels/account_settings/profile/nickname_spec.ts index b90905468e..f7e19a32f6 100644 --- a/e2e-tests/cypress/tests/integration/channels/account_settings/profile/nickname_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/account_settings/profile/nickname_spec.ts @@ -27,7 +27,7 @@ describe('Settings > Sidebar > General', () => { it('MM-T3848 No nickname is present', () => { // # Open 'Profile' modal and view the default 'Profile' - cy.uiOpenProfileModal().within(() => { + cy.uiOpenProfileModal('Profile Settings').within(() => { // # Open 'Nickname' setting cy.uiGetHeading('Nickname').click(); @@ -56,7 +56,7 @@ describe('Settings > Sidebar > General', () => { const newNickname = 'victor_nick'; // # Open 'Profile' modal and view the default 'Profile Settings' - cy.uiOpenProfileModal().within(() => { + cy.uiOpenProfileModal('Profile Settings').within(() => { // # Open 'Nickname' setting cy.uiGetHeading('Nickname').click(); @@ -133,7 +133,7 @@ describe('Settings > Sidebar > General', () => { it('MM-T2061 Nickname should reset on cancel of edit', () => { // # Open 'Profile' modal and view the default 'Profile Settings' - cy.uiOpenProfileModal().within(() => { + cy.uiOpenProfileModal('Profile Settings').within(() => { // # Open 'Nickname' setting cy.uiGetHeading('Nickname').click(); @@ -156,7 +156,7 @@ describe('Settings > Sidebar > General', () => { it('MM-T2062 Clear nickname and save', () => { // # Open 'Profile' modal and view the default 'Profile Settings' - cy.uiOpenProfileModal().within(() => { + cy.uiOpenProfileModal('Profile Settings').within(() => { // # Open 'Nickname' setting cy.uiGetHeading('Nickname').click(); diff --git a/e2e-tests/cypress/tests/integration/channels/account_settings/profile/profile_picture_change_spec.ts b/e2e-tests/cypress/tests/integration/channels/account_settings/profile/profile_picture_change_spec.ts index 0e38ad7f93..02d4d00046 100644 --- a/e2e-tests/cypress/tests/integration/channels/account_settings/profile/profile_picture_change_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/account_settings/profile/profile_picture_change_spec.ts @@ -6,7 +6,7 @@ import * as TIMEOUTS from '../../../../fixtures/timeouts'; -describe('Account Settings', () => { +describe('Profile Settings', () => { beforeEach(() => { cy.apiAdminLogin().apiInitSetup({loginAfter: true}).its('user').as('user'); }); @@ -21,7 +21,7 @@ describe('Account Settings', () => { getProfilePictureId().as('idOld'); // # Open Profile > Profile Settings > Profile Picture > Edit - cy.uiOpenProfileModal().findByRole('button', {name: /picture edit/i}).click(); + cy.uiOpenProfileModal('Profile Settings').findByRole('button', {name: /picture edit/i}).click(); // # Click the X to remove the old profile picture but do not click save cy.findByRole('button', {name: /remove profile picture/i}).click(); @@ -74,7 +74,7 @@ function verifyProfilePictureDoesNotUpdateAfterCancel() { }); // # Open Profile > Profile Settings > Profile Picture > Edit - cy.uiOpenProfileModal().findByRole('button', {name: /picture edit/i}).click(); + cy.uiOpenProfileModal('Profile Settings').findByRole('button', {name: /picture edit/i}).click(); // # Select a new profile picture cy.findByTestId('uploadPicture').attachFile('png-image-file.png'); diff --git a/e2e-tests/cypress/tests/integration/channels/account_settings/profile/profile_picture_spec.ts b/e2e-tests/cypress/tests/integration/channels/account_settings/profile/profile_picture_spec.ts index 3682bb4e83..ee730b1ecf 100644 --- a/e2e-tests/cypress/tests/integration/channels/account_settings/profile/profile_picture_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/account_settings/profile/profile_picture_spec.ts @@ -32,7 +32,7 @@ describe('Profile > Profile Settings > Profile Picture', () => { and('not.include', customImageMatch); // # Go to Profile - cy.uiOpenProfileModal(); + cy.uiOpenProfileModal('Profile Settings'); // # Click "Edit" to the right of "Profile Picture" cy.get('#pictureEdit').should('be.visible').click(); @@ -51,7 +51,7 @@ describe('Profile > Profile Settings > Profile Picture', () => { and('include', customImageMatch); // # Go to Profile - cy.uiOpenProfileModal(); + cy.uiOpenProfileModal('Profile Settings'); // # Click "Edit" to the right of "Profile Picture" cy.get('#pictureEdit').should('be.visible').click(); @@ -75,7 +75,7 @@ describe('Profile > Profile Settings > Profile Picture', () => { it('MM-T2077 Profile picture: non image file shows error', () => { // # Go to Profile - cy.uiOpenProfileModal(); + cy.uiOpenProfileModal('Profile Settings'); // # Click "Edit" to the right of "Profile Picture" cy.get('#pictureEdit').should('be.visible').click(); diff --git a/e2e-tests/cypress/tests/integration/channels/account_settings/profile/username_spec.ts b/e2e-tests/cypress/tests/integration/channels/account_settings/profile/username_spec.ts index 5ab66c198a..ff3f9d16d5 100644 --- a/e2e-tests/cypress/tests/integration/channels/account_settings/profile/username_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/account_settings/profile/username_spec.ts @@ -41,7 +41,7 @@ describe('Settings > Sidebar > General > Edit', () => { beforeEach(() => { // # Go to Profile - cy.uiOpenProfileModal(); + cy.uiOpenProfileModal('Profile Settings'); }); it('MM-T2050 Username cannot be blank', () => { @@ -101,7 +101,7 @@ describe('Settings > Sidebar > General > Edit', () => { // # Login the temporary user cy.apiLogin(tempUser); cy.visit(offTopicUrl); - cy.uiOpenProfileModal(); + cy.uiOpenProfileModal('Profile Settings'); // # Step 1 // # Edit the username field @@ -214,8 +214,8 @@ describe('Settings > Sidebar > General > Edit', () => { cy.apiLogin(testUser); cy.visit(offTopicUrl); - // # Open account settings modal - cy.uiOpenProfileModal(); + // # Open profile settings modal + cy.uiOpenProfileModal('Profile Settings'); // # Open Full Name section cy.get('#usernameDesc').click(); diff --git a/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_1_spec.ts b/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_1_spec.ts index 9ac545ed7d..db4b4c2bb5 100644 --- a/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_1_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/auth_sso/authentication_1_spec.ts @@ -79,7 +79,7 @@ describe('Authentication', () => { cy.visit('/'); // # Open Profile - cy.uiOpenProfileModal(); + cy.uiOpenProfileModal('Profile Settings'); // # Click "Edit" to the right of "Email" cy.get('#emailEdit').should('be.visible').click(); diff --git a/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js b/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js index d83797db07..95e31a75d4 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/channel/more_channels_spec.js @@ -60,8 +60,8 @@ describe('Channels', () => { cy.apiLogin(otherUser); cy.visit(`/${testTeam.name}/channels/town-square`); - // # Go to LHS and click 'Browse Channels' - cy.uiBrowseOrCreateChannel('Browse Channels').click(); + // # Go to LHS and click 'Browse channels' + cy.uiBrowseOrCreateChannel('Browse channels').click(); cy.get('#moreChannelsModal').should('be.visible').within(() => { // * Dropdown should be visible, defaulting to "Public Channels" @@ -103,8 +103,8 @@ describe('Channels', () => { cy.findByText('Archive').should('be.visible').click(); }); - // # Go to LHS and click 'Browse Channels' - cy.uiBrowseOrCreateChannel('Browse Channels').click(); + // # Go to LHS and click 'Browse channels' + cy.uiBrowseOrCreateChannel('Browse channels').click(); cy.get('#moreChannelsModal').should('be.visible').within(() => { // # CLick dropdown to open selection @@ -191,8 +191,8 @@ describe('Channels', () => { }); }); - // # Go to LHS and click 'Browse Channels' - cy.uiBrowseOrCreateChannel('Browse Channels').click(); + // # Go to LHS and click 'Browse channels' + cy.uiBrowseOrCreateChannel('Browse channels').click(); // * Dropdown should be visible, defaulting to "Public Channels" cy.get('#channelsMoreDropdown').should('be.visible').within((el) => { @@ -244,8 +244,8 @@ function verifyMoreChannelsModalWithArchivedSelection(isEnabled, testUser, testT } function verifyMoreChannelsModal(isEnabled) { - // # Go to LHS and click 'Browse Channels' - cy.uiBrowseOrCreateChannel('Browse Channels').click(); + // # Go to LHS and click 'Browse channels' + cy.uiBrowseOrCreateChannel('Browse channels').click(); // * Verify that the more channels modal is open and with or without option to view archived channels cy.get('#moreChannelsModal').should('be.visible').within(() => { diff --git a/e2e-tests/cypress/tests/integration/channels/channel/more_public_channels_spec.js b/e2e-tests/cypress/tests/integration/channels/channel/more_public_channels_spec.js index 5808654971..8624b60edf 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel/more_public_channels_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/channel/more_public_channels_spec.js @@ -49,8 +49,8 @@ describe('more public channels', () => { // # Go to town square cy.visit(`/${testTeam.name}/channels/town-square`); - // # Go to LHS and click 'Browse Channels' - cy.uiBrowseOrCreateChannel('Browse Channels').click(); + // # Go to LHS and click 'Browse channels' + cy.uiBrowseOrCreateChannel('Browse channels').click(); // * Assert that the moreChannelsModel is visible cy.findByRole('dialog', {name: 'More Channels'}).should('be.visible').within(() => { @@ -82,8 +82,8 @@ describe('more public channels', () => { // # Go to town square cy.visit(`/${testTeam.name}/channels/town-square`); - // # Go to LHS and click 'Browse Channels' - cy.uiBrowseOrCreateChannel('Browse Channels').click(); + // # Go to LHS and click 'Browse channels' + cy.uiBrowseOrCreateChannel('Browse channels').click(); // * Assert the moreChannelsModel is visible cy.findByRole('dialog', {name: 'More Channels'}).should('be.visible').within(() => { diff --git a/e2e-tests/cypress/tests/integration/channels/channel_settings/channel_name_validations_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel_settings/channel_name_validations_spec.ts index 1c2c415969..3b1041ac10 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel_settings/channel_name_validations_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/channel_settings/channel_name_validations_spec.ts @@ -8,8 +8,7 @@ // *************************************************************** // Stage: @prod -// Group: @channels @channel -// Group: @channels @channel_settings +// Group: @channels @channel @channel_settings import * as TIMEOUTS from '../../../fixtures/timeouts'; import {getRandomId} from '../../../utils'; @@ -54,7 +53,7 @@ describe('Channel routing', () => { it('MM-T884_2 Creating new channel validates against two user IDs being used as channel name', () => { // # click on create public channel - cy.uiBrowseOrCreateChannel('Create New Channel').click(); + cy.uiBrowseOrCreateChannel('Create new channel').click(); // * Verify that the new channel modal is visible cy.get('#new-channel-modal').should('be.visible').within(() => { @@ -74,7 +73,7 @@ describe('Channel routing', () => { it('MM-T884_3 Creating a new channel validates against gm-like names being used as channel name', () => { // # click on create public channel - cy.uiBrowseOrCreateChannel('Create New Channel').click(); + cy.uiBrowseOrCreateChannel('Create new channel').click(); // * Verify that the new channel modal is visible cy.findByRole('dialog', {name: 'Create a new channel'}).within(() => { diff --git a/e2e-tests/cypress/tests/integration/channels/channel_sidebar/category_sorting_1_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel_sidebar/category_sorting_1_spec.ts index bf2f6e9011..e4ba8271b3 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel_sidebar/category_sorting_1_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/channel_sidebar/category_sorting_1_spec.ts @@ -140,10 +140,10 @@ function createCategoryFromSidebarMenu() { const categoryName = `category-${getRandomId()}`; // # Click on the sidebar menu dropdown - cy.findByLabelText('Add Channel Dropdown').click(); + cy.uiGetLHSAddChannelButton().click(); // # Click on create category link - cy.findByText('Create New Category').should('be.visible').click(); + cy.findByText('Create new category').should('be.visible').click(); // # Verify that Create Category modal has shown up. // # Wait for a while until the modal has fully loaded, especially during first-time access. diff --git a/e2e-tests/cypress/tests/integration/channels/channel_sidebar/category_sorting_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel_sidebar/category_sorting_spec.ts index 69086ce680..0e46cd6d62 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel_sidebar/category_sorting_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/channel_sidebar/category_sorting_spec.ts @@ -23,10 +23,10 @@ describe('Category sorting', () => { it('MM-T3916 Create Category character limit', () => { // # Click on the sidebar menu dropdown - cy.findByLabelText('Add Channel Dropdown').click(); + cy.uiGetLHSAddChannelButton().click(); // # Click on create category link - cy.findByText('Create New Category').should('be.visible').click(); + cy.findByText('Create new category').should('be.visible').click(); // # Add a name 26 characters in length e.g `abcdefghijklmnopqrstuvwxyz` cy.get('#editCategoryModal').should('be.visible').wait(TIMEOUTS.HALF_SEC).within(() => { diff --git a/e2e-tests/cypress/tests/integration/channels/channel_sidebar/custom_categories_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel_sidebar/custom_categories_spec.ts index 30918ebbb3..226ad58698 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel_sidebar/custom_categories_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/channel_sidebar/custom_categories_spec.ts @@ -23,14 +23,14 @@ describe('Channel sidebar', () => { }); }); - it('MM-T3161_1 should create a new category from sidebar menu -- KNOWN ISSUE: MM-42576', () => { + it('MM-T3161_1 should create a new category from sidebar menu', () => { const categoryName = createCategoryFromSidebarMenu(); // * Check if the category exists cy.findByLabelText(categoryName).should('be.visible'); }); - it('MM-T3161_2 should create a new category from category menu -- KNOWN ISSUE: MM-42576', () => { + it('MM-T3161_2 should create a new category from category menu', () => { const categoryName = createCategoryFromSidebarMenu(); // # Create new category from category menu @@ -43,7 +43,7 @@ describe('Channel sidebar', () => { cy.findByLabelText(newCategoryName).should('be.visible'); }); - it('MM-T3161_3 move an existing channel to a new category -- KNOWN ISSUE: MM-42576', () => { + it('MM-T3161_3 move an existing channel to a new category', () => { const newCategoryName = `category-${getRandomId()}`; // # Move to a new category @@ -53,7 +53,7 @@ describe('Channel sidebar', () => { cy.findByLabelText(newCategoryName).should('be.visible'); }); - it('MM-T3163 Rename a category -- KNOWN ISSUE: MM-42576', () => { + it('MM-T3163 Rename a category', () => { const categoryName = createCategoryFromSidebarMenu(); // # Rename category from category menu @@ -71,7 +71,7 @@ describe('Channel sidebar', () => { cy.findByLabelText(renameCategory).should('be.visible'); }); - it('MM-T3165 Delete a category -- KNOWN ISSUE: MM-42576', () => { + it('MM-T3165 Delete a category', () => { const categoryName = createCategoryFromSidebarMenu(); // # Delete category from category menu @@ -90,10 +90,10 @@ function createCategoryFromSidebarMenu() { const categoryName = `category-${getRandomId()}`; // # Click on the sidebar menu dropdown - cy.findByLabelText('Add Channel Dropdown').click(); + cy.uiGetLHSAddChannelButton().click(); // # Click on create category link - cy.findByText('Create New Category').should('be.visible').click(); + cy.findByText('Create new category').should('be.visible').click(); // # Verify that Create Category modal has shown up. // # Wait for a while until the modal has fully loaded, especially during first-time access. diff --git a/e2e-tests/cypress/tests/integration/channels/channel_sidebar/new_channel_dropdown_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel_sidebar/new_channel_dropdown_spec.ts index 17114e21aa..c1eb246381 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel_sidebar/new_channel_dropdown_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/channel_sidebar/new_channel_dropdown_spec.ts @@ -62,10 +62,10 @@ describe('Channel sidebar', () => { cy.url().should('include', `/${teamName}/channels/town-square`); // # Click the New Channel Dropdown button - cy.get('.AddChannelDropdown_dropdownButton').should('be.visible').click(); + cy.uiGetLHSAddChannelButton().should('be.visible').click(); - // # Click the Browse Channels dropdown item - cy.get('.AddChannelDropdown .MenuItem:contains(Browse Channels) button').should('be.visible').click(); + // # Click the Browse channels dropdown item + cy.get('.AddChannelDropdown .MenuItem:contains(Browse channels) button').should('be.visible').click(); // * Verify that the more channels modal is visible cy.get('.more-modal').should('be.visible'); diff --git a/e2e-tests/cypress/tests/integration/channels/collapsed_reply_threads/replies_spec.js b/e2e-tests/cypress/tests/integration/channels/collapsed_reply_threads/replies_spec.js index 16b30acf5b..6d7c252bfe 100644 --- a/e2e-tests/cypress/tests/integration/channels/collapsed_reply_threads/replies_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/collapsed_reply_threads/replies_spec.js @@ -10,6 +10,8 @@ // Stage: @prod // Group: @channels @collapsed_reply_threads +import * as TIMEOUTS from '../../../fixtures/timeouts'; + describe('Collapsed Reply Threads', () => { let testTeam; let testUser; @@ -174,4 +176,45 @@ describe('Collapsed Reply Threads', () => { cy.uiCloseRHS(); }); }); + + it('MM-T5413 should auto-scroll to bottom upon pasting long text in reply', () => { + // # Post a root post as current user + cy.postMessageAs({ + sender: testUser, + message: 'Another interesting post,', + channelId: testChannel.id, + }).then(({id: rootId}) => { + // # Post multiple replies as other user so that the new messages line is pushed up + Cypress._.times(20, (i) => { + cy.postMessageAs({ + sender: otherUser, + message: 'Reply ' + i, + channelId: testChannel.id, + rootId, + }); + }); + + // # Click root post + cy.get(`#post_${rootId}`).click(); + + // # Wait for RHS to open and scroll to position + cy.wait(TIMEOUTS.ONE_SEC); + + // * RHS should open and the editor's actions should not be visible. + cy.get('#rhsContainer').findByTestId('SendMessageButton').should('not.be.visible'); + + // # Close RHS + cy.uiCloseRHS(); + + // # Click root post + cy.get(`#post_${rootId}`).click(); + + // # Paste a multiline string in the RHS textbox. + const text = 'word '.repeat(2000); + cy.get('#rhsContainer').findByTestId('reply_textbox').clear().invoke('val', text).trigger('input'); + + // * RHS should open and the editor should be visible and focused + cy.get('#rhsContainer').findByTestId('SendMessageButton').should('be.visible'); + }); + }); }); diff --git a/e2e-tests/cypress/tests/integration/channels/emoji/recently_used_emoji_1_spec.ts b/e2e-tests/cypress/tests/integration/channels/emoji/recently_used_emoji_1_spec.ts index 0d016ea33d..fcaeb3b38b 100644 --- a/e2e-tests/cypress/tests/integration/channels/emoji/recently_used_emoji_1_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/emoji/recently_used_emoji_1_spec.ts @@ -105,7 +105,10 @@ describe('Recent Emoji', () => { cy.uiGetPostTextBox().type('{enter} {enter}').wait(TIMEOUTS.TWO_SEC); // # Hover over the last post by opening dot menu on it - cy.clickPostDotMenu(); + cy.getLastPostId().then((postId) => { + // # Click on post dot menu so we can check for reaction icon + cy.get(`#post_${postId}`).trigger('mouseover'); + }); cy.get('#recent_reaction_0').should('exist').then((recentReaction) => { // * Assert that custom emoji is present as most recent in quick reaction menu @@ -152,7 +155,10 @@ describe('Recent Emoji', () => { cy.reload(); // # Hover over the last post by opening dot menu on it - cy.clickPostDotMenu(); + cy.getLastPostId().then((postId) => { + // # Click on post dot menu so we can check for reaction icon + cy.get(`#post_${postId}`).trigger('mouseover'); + }); cy.get('#recent_reaction_0').should('exist').then((recentReaction) => { // * Assert that instead of custom emoji the system emoji is present as most recent in quick reaction menu diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_input_fields_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_input_fields_spec.js index 8626033949..08f25fc8d4 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_input_fields_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_input_fields_spec.js @@ -196,7 +196,7 @@ describe('Verify Accessibility Support in different input fields', () => { cy.get('#FormattingControl_ul').should('be.focused').and('have.attr', 'aria-label', 'bulleted list').tab(); // * Verify if the focus is on the numbered list button - cy.get('#FormattingControl_ol').should('be.focused').and('have.attr', 'aria-label', 'numbered list').tab(); + cy.get('#FormattingControl_ol').should('be.focused').and('have.attr', 'aria-label', 'numbered list').tab().tab(); // * Verify if the focus is on the formatting options button cy.get('#toggleFormattingBarButton').should('be.focused').and('have.attr', 'aria-label', 'formatting').tab(); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_spec.js index a068d15f9f..ae6df74721 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/accessibility/accessibility_modals_dialogs_spec.js @@ -101,7 +101,7 @@ describe('Verify Accessibility Support in Modals & Dialogs', () => { cy.reload(); // * Verify the aria-label in more public channels button - cy.uiBrowseOrCreateChannel('Browse Channels').click(); + cy.uiBrowseOrCreateChannel('Browse channels').click(); // * Verify the accessibility support in More Channels Dialog cy.findByRole('dialog', {name: 'More Channels'}).within(() => { diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/guest_invitation_ui_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/guest_invitation_ui_spec.ts index aa12dae759..396869cd8e 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/guest_invitation_ui_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/guest_invitation_ui_spec.ts @@ -223,4 +223,15 @@ describe('Guest Account - Guest User Invitation Flow', () => { // * Verify invite more button is present cy.findByTestId('invite-more').should('be.visible'); }); + + it('hides the copy link button when inviting guests', () => { + // # Open team menu and click 'Invite People' + cy.uiOpenTeamMenu('Invite People'); + + // # Select Guest + cy.findByTestId('inviteGuestLink').should('be.visible').click(); + + // * The button "Copy invite link" should not exist + cy.findByTestId('InviteView__copyInviteLink').should('not.exist'); + }); }); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/ldap/ldap_group_sync_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/ldap/ldap_group_sync_spec.js index c7660753e0..a509642532 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/ldap/ldap_group_sync_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/ldap/ldap_group_sync_spec.js @@ -230,7 +230,7 @@ context('ldap', () => { // # Go to team page to look for this channel in public channel directory cy.visit(`/${testTeam.name}`); - cy.uiBrowseOrCreateChannel('Browse Channels').click(); + cy.uiBrowseOrCreateChannel('Browse channels').click(); // * Search private channel name and make sure it isn't there in public channel directory cy.get('#searchChannelsTextbox').type(testChannel.display_name); @@ -451,8 +451,8 @@ context('ldap', () => { // # Visit off-topic channel cy.visit(`/${testTeam.name}/channels/off-topic`); - // # Go to LHS and click 'Browse Channels' - cy.uiBrowseOrCreateChannel('Browse Channels').click(); + // # Go to LHS and click 'Browse channels' + cy.uiBrowseOrCreateChannel('Browse channels').click(); // * Search public channel and ensure it appears in the list cy.get('#searchChannelsTextbox').type(publicChannel.display_name); @@ -468,8 +468,8 @@ context('ldap', () => { // # Visit off-topic channel cy.visit(`/${testTeam.name}/channels/off-topic`); - // # Go to LHS and click 'Browse Channels' - cy.uiBrowseOrCreateChannel('Browse Channels').click(); + // # Go to LHS and click 'Browse channels' + cy.uiBrowseOrCreateChannel('Browse channels').click(); // * Search private channel name and make sure it isn't there in public channel directory cy.get('#searchChannelsTextbox').type(publicChannel.display_name); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/ldap/ldap_guest_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/ldap/ldap_guest_spec.js index fda7dca517..79dbf33ab8 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/ldap/ldap_guest_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/ldap/ldap_guest_spec.js @@ -159,7 +159,7 @@ describe('LDAP guest', () => { // # Create team if no membership cy.skipOrCreateTeam(testSettings, getRandomId()).then(() => { // * Verify user is a member - cy.findByRole('button', {name: 'Add Channel Dropdown'}).should('exist'); + cy.uiGetLHSAddChannelButton().should('exist'); // # Demote the user demoteUserToGuest(user2Data); @@ -173,7 +173,7 @@ describe('LDAP guest', () => { cy.uiAddDirectMessage().should('exist'); // * Check the user is a guest - cy.findByRole('button', {name: 'Add Channel Dropdown'}).should('not.exist'); + cy.uiGetLHSAddChannelButton().should('not.exist'); }); }); }); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/oauth/oauth_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/oauth/oauth_spec.ts index 3ea722512b..d0bfbc6de0 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/oauth/oauth_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/oauth/oauth_spec.ts @@ -292,7 +292,7 @@ describe('Integrations page', () => { // * The app should no longer exist cy.get(`[data-app="${oauthClientID}"]`).should('not.exist'); - // # Close the account settings modal + // # Close the profile settings modal cy.get('#accountSettingsHeader').within(() => { cy.get('button.close').click(); }); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/permissions/team_permissions_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/permissions/team_permissions_spec.ts index 98cb658c67..336b125d15 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/permissions/team_permissions_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/permissions/team_permissions_spec.ts @@ -148,7 +148,7 @@ describe('Team Permissions', () => { cy.visit(`/${testTeam.name}/channels/town-square`); // # Click on create new channel at LHS - cy.uiBrowseOrCreateChannel('Create New Channel').click(); + cy.uiBrowseOrCreateChannel('Create new channel').click(); // * Verify that the create private channel is disabled cy.findByRole('dialog', {name: 'Create a new channel'}).find('#public-private-selector-button-P').should('have.class', 'disabled'); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/about/edition_and_license_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/about/edition_and_license_spec.js index 27508d7a1d..8ebba6a80c 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/about/edition_and_license_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/about/edition_and_license_spec.js @@ -105,7 +105,7 @@ function verifyCreatePublicChannel(teamName, testUsers) { cy.visit(`/${teamName}/channels/town-square`); // # Click on create new channel at LHS - cy.uiBrowseOrCreateChannel('Create New Channel').click(); + cy.uiBrowseOrCreateChannel('Create new channel').click(); cy.findByRole('dialog', {name: 'Create a new channel'}).within(() => { // * Verify if creating a public channel is disabled or not diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/system_scheme_permission_part2_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/system_scheme_permission_part2_spec.js index 53e063aef9..33ae5b8c77 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/system_scheme_permission_part2_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/system_scheme_permission_part2_spec.js @@ -53,7 +53,7 @@ describe('System Scheme', () => { cy.findByTestId('systemScheme-link').should('be.visible').click().wait(TIMEOUTS.HALF_SEC); // # Click on `Reset to defaults` - cy.findByTestId('resetPermissionsToDefault').should('be.visible').click().wait(TIMEOUTS.HALF_SEC); + cy.findByTestId('resetPermissionsToDefault').scrollIntoView().should('be.visible').click().wait(TIMEOUTS.HALF_SEC); // # Confirm the dialog cy.get('#confirmModalButton').click().wait(TIMEOUTS.TWO_SEC); @@ -78,10 +78,10 @@ describe('System Scheme', () => { cy.findByTestId('all_users-private_channel-create_private_channel-checkbox').should('not.have.class', 'checked'); // # Click on `Reset to defaults` - cy.findByTestId('resetPermissionsToDefault').should('be.visible').click().wait(TIMEOUTS.HALF_SEC); + cy.findByTestId('resetPermissionsToDefault').scrollIntoView().should('be.visible').click().wait(TIMEOUTS.HALF_SEC); // # Confirm the dialog - cy.get('#confirmModalButton').click().wait(TIMEOUTS.HALF_SEC); + cy.get('#confirmModalButton').scrollIntoView().click().wait(TIMEOUTS.HALF_SEC); // # Save changes cy.get('#saveSetting').click().wait(TIMEOUTS.TWO_SEC); diff --git a/e2e-tests/cypress/tests/integration/channels/integrations/builtin_commands/common_commands_1_spec.js b/e2e-tests/cypress/tests/integration/channels/integrations/builtin_commands/common_commands_1_spec.js index 9619c38566..2f0d4f0aa3 100644 --- a/e2e-tests/cypress/tests/integration/channels/integrations/builtin_commands/common_commands_1_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/integrations/builtin_commands/common_commands_1_spec.js @@ -176,7 +176,7 @@ describe('Integrations', () => { // # Post "/marketplace" as SystemAdmin cy.postMessage('/marketplace '); - cy.get('#modal_marketplace').should('be.visible'); + cy.findByRole('heading', {name: 'App Marketplace'}).should('be.visible'); }); }); }); diff --git a/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_shift_a_account_settings_spec.js b/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_shift_a_account_settings_spec.js index 9816358250..09f7aea902 100644 --- a/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_shift_a_account_settings_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_shift_a_account_settings_spec.js @@ -21,7 +21,7 @@ describe('Keyboard Shortcuts', () => { // # Type CTRL/CMD+SHIFT+A to open 'Settings' cy.uiGetPostTextBox().cmdOrCtrlShortcut('{shift}A'); - // * Ensure account settings modal is open + // * Ensure profile settings modal is open cy.get('#accountSettingsModal').should('be.visible'); cy.uiClose(); diff --git a/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_shift_slash/not_open_emoji_picker_spec.js b/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_shift_slash/not_open_emoji_picker_spec.js index ec1d9f39f6..39e722b0b1 100644 --- a/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_shift_slash/not_open_emoji_picker_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_shift_slash/not_open_emoji_picker_spec.js @@ -111,7 +111,7 @@ describe('Keyboard shortcut CTRL/CMD+Shift+\\ for adding reaction to last messag cy.uiOpenTeamMenu('View Members'); verifyEmojiPickerNotOpen(); - cy.uiOpenProfileModal(); + cy.uiOpenProfileModal('Profile Settings'); verifyEmojiPickerNotOpen(); ['Edit Channel Header', 'Rename Channel'].forEach((modal) => { diff --git a/e2e-tests/cypress/tests/integration/channels/mark_as_unread/helpers.js b/e2e-tests/cypress/tests/integration/channels/mark_as_unread/helpers.js index 4482bcb782..fbcbea8f4b 100644 --- a/e2e-tests/cypress/tests/integration/channels/mark_as_unread/helpers.js +++ b/e2e-tests/cypress/tests/integration/channels/mark_as_unread/helpers.js @@ -21,6 +21,7 @@ export function markAsUnreadShouldBeAbsent(postId, prefix = 'post', location = ' within(() => { cy.findByText('Mark as Unread').should('not.exist'); }); + cy.get('body').type('esc'); } export function switchToChannel(channel) { diff --git a/e2e-tests/cypress/tests/integration/channels/menus/main_menu_spec.js b/e2e-tests/cypress/tests/integration/channels/menus/main_menu_spec.js index e3c1b0b727..4cf116830a 100644 --- a/e2e-tests/cypress/tests/integration/channels/menus/main_menu_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/menus/main_menu_spec.js @@ -25,7 +25,7 @@ describe('Main menu', () => { cy.apiLogin(testUser); cy.visit(`/${testTeam.name}/channels/town-square`); - cy.uiOpenProfileModal(); + cy.uiOpenProfileModal('Profile Settings'); cy.findByRole('set status').should('not.exist'); }); diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/emoji_recently_used_spec.js b/e2e-tests/cypress/tests/integration/channels/messaging/emoji_recently_used_spec.js index 60178697bd..93f0a70cfe 100644 --- a/e2e-tests/cypress/tests/integration/channels/messaging/emoji_recently_used_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/messaging/emoji_recently_used_spec.js @@ -53,7 +53,7 @@ describe('Messaging', () => { // # Create a post and hover over post menu and verify One click reactions are not part of the post menu on hover cy.visit(offTopicPath); - // # Toggle One-click reactions option in Account Settings>Display>One-click reactions on messages to ON + // # Toggle One-click reactions option in Settings > Display> One-click reactions on messages to ON cy.uiOpenSettingsModal('Display').within(() => { cy.findByText('Display', {timeout: timeouts.ONE_MIN}).click(); cy.findByText('Quick reactions on messages').click(); @@ -158,7 +158,7 @@ describe('Messaging', () => { cy.visit(offTopicPath).wait(timeouts.HALF_SEC); - // # Open Account Settings > Display + // # Open Settings > Display // * Verify One-click reactions on messages option is not available cy.uiOpenSettingsModal('Display').within(() => { cy.findByText('Display', {timeout: timeouts.ONE_MIN}).click(); diff --git a/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/existing_channel_name_spec.js b/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/existing_channel_name_spec.js index 5e0ccafda9..24ad292117 100644 --- a/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/existing_channel_name_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/existing_channel_name_spec.js @@ -77,7 +77,7 @@ describe('Channel', () => { */ function verifyExistingChannelError(newChannelName, makePrivate = false) { // Click on '+' button for Public or Private Channel - cy.uiBrowseOrCreateChannel('Create New Channel').click(); + cy.uiBrowseOrCreateChannel('Create new channel').click(); if (makePrivate) { cy.get('#public-private-selector-button-P').click(); diff --git a/e2e-tests/cypress/tests/integration/channels/profile_settings/profile_settings_spec.js b/e2e-tests/cypress/tests/integration/channels/profile_settings/profile_settings_spec.js index 73416dcc87..8df7c392cd 100644 --- a/e2e-tests/cypress/tests/integration/channels/profile_settings/profile_settings_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/profile_settings/profile_settings_spec.js @@ -22,7 +22,7 @@ describe('Profile Settings', () => { }); it('MM-T2044 Clear fields, values revert', () => { - cy.uiOpenProfileModal(); + cy.uiOpenProfileModal('Profile Settings'); // # Click "Edit" to the right of "Full Name" cy.get('#nameEdit').should('be.visible').click(); diff --git a/e2e-tests/cypress/tests/integration/channels/settings/sidebar/fullname_spec.js b/e2e-tests/cypress/tests/integration/channels/settings/sidebar/fullname_spec.js index 74a0220bab..87d55273a1 100644 --- a/e2e-tests/cypress/tests/integration/channels/settings/sidebar/fullname_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/settings/sidebar/fullname_spec.js @@ -33,7 +33,7 @@ describe('Settings > Sidebar > General', () => { // # Login as test user, visit off-topic and go to the Profile cy.apiLogin(testUser); cy.visit(offTopicUrl); - cy.uiOpenProfileModal(); + cy.uiOpenProfileModal('Profile Settings'); // # Open Full Name section cy.get('#nameDesc').click(); diff --git a/e2e-tests/cypress/tests/support/ui/account_settings_modal.d.ts b/e2e-tests/cypress/tests/support/ui/account_settings_modal.d.ts index 3375acaa1a..af61eb6098 100644 --- a/e2e-tests/cypress/tests/support/ui/account_settings_modal.d.ts +++ b/e2e-tests/cypress/tests/support/ui/account_settings_modal.d.ts @@ -18,19 +18,19 @@ declare namespace Cypress { interface Chainable { /** - * Open the account settings modal + * Open the profile settings modal * @param {string} section - such as `'General'`, `'Security'`, `'Notifications'`, `'Display'`, `'Sidebar'` and `'Advanced'` * @return the "#accountSettingsModal" * * @example - * cy.uiOpenProfileModal().within(() => { + * cy.uiOpenProfileModal('Profile Settings').within(() => { * // Do something here * }); */ - uiOpenProfileModal(section?: string): Chainable>; + uiOpenProfileModal(section: string): Chainable>; /** - * Close the account settings modal given that the modal itself is opened. + * Close the profile settings modal given that the modal itself is opened. * * @example * cy.uiCloseAccountSettingsModal(); @@ -38,7 +38,7 @@ declare namespace Cypress { uiCloseAccountSettingsModal(): Chainable; /** - * Navigate to account settings and verify the user's first, last name + * Navigate to profile settings and verify the user's first, last name * @param {String} firstname - expected user firstname * @param {String} lastname - expected user lastname */ diff --git a/e2e-tests/cypress/tests/support/ui/account_settings_modal.js b/e2e-tests/cypress/tests/support/ui/account_settings_modal.js index 416d73a9c4..041f5517cd 100644 --- a/e2e-tests/cypress/tests/support/ui/account_settings_modal.js +++ b/e2e-tests/cypress/tests/support/ui/account_settings_modal.js @@ -19,7 +19,7 @@ Cypress.Commands.add('uiOpenProfileModal', (section = '') => { Cypress.Commands.add('verifyAccountNameSettings', (firstname, lastname) => { // # Go to Profile - cy.uiOpenProfileModal(); + cy.uiOpenProfileModal('Profile Settings'); // * Check name value cy.get('#nameDesc').should('have.text', `${firstname} ${lastname}`); diff --git a/e2e-tests/cypress/tests/support/ui/channel.js b/e2e-tests/cypress/tests/support/ui/channel.js index bba74e58a6..8ddd059630 100644 --- a/e2e-tests/cypress/tests/support/ui/channel.js +++ b/e2e-tests/cypress/tests/support/ui/channel.js @@ -11,7 +11,7 @@ Cypress.Commands.add('uiCreateChannel', ({ name = '', createBoard = false, }) => { - cy.uiBrowseOrCreateChannel('Create New Channel').click(); + cy.uiBrowseOrCreateChannel('Create new channel').click(); cy.get('#new-channel-modal').should('be.visible'); if (isPrivate) { diff --git a/e2e-tests/cypress/tests/support/ui/channel_sidebar.js b/e2e-tests/cypress/tests/support/ui/channel_sidebar.js index 0b626a5d24..930337ae81 100644 --- a/e2e-tests/cypress/tests/support/ui/channel_sidebar.js +++ b/e2e-tests/cypress/tests/support/ui/channel_sidebar.js @@ -7,8 +7,8 @@ Cypress.Commands.add('uiCreateSidebarCategory', (categoryName = `category-${getR // # Click the New Category/Channel Dropdown button cy.uiGetLHSAddChannelButton().click(); - // # Click the Create New Category dropdown item - cy.get('.AddChannelDropdown').should('be.visible').contains('.MenuItem', 'Create New Category').click(); + // # Click the Create new category dropdown item + cy.get('.AddChannelDropdown').should('be.visible').contains('.MenuItem', 'Create new category').click(); cy.findByRole('dialog', {name: 'Rename Category'}).should('be.visible').within(() => { // # Fill in the category name and click 'Create' diff --git a/e2e-tests/cypress/tests/support/ui/sidebar_left.ts b/e2e-tests/cypress/tests/support/ui/sidebar_left.ts index 1168b705dd..44877544e6 100644 --- a/e2e-tests/cypress/tests/support/ui/sidebar_left.ts +++ b/e2e-tests/cypress/tests/support/ui/sidebar_left.ts @@ -32,7 +32,7 @@ Cypress.Commands.add('uiOpenTeamMenu', (item = '') => { Cypress.Commands.add('uiGetLHSAddChannelButton', () => { return cy.uiGetLHS(). - findByRole('button', {name: 'Add Channel Dropdown'}); + find('.AddChannelDropdown_dropdownButton'); }); Cypress.Commands.add('uiGetLHSTeamMenu', () => { @@ -89,7 +89,7 @@ Cypress.Commands.add('uiGetLhsSection', (section) => { }); Cypress.Commands.add('uiBrowseOrCreateChannel', (item) => { - cy.findByRole('button', {name: 'Add Channel Dropdown'}). + cy.get('.AddChannelDropdown_dropdownButton'). should('be.visible'). click(); cy.get('.dropdown-menu').should('be.visible'); @@ -213,7 +213,7 @@ declare global { * @param {string} item - dropdown menu. If set, it will do click action. * * @example - * cy.uiBrowseOrCreateChannel('Browse Channels'); + * cy.uiBrowseOrCreateChannel('Browse channels'); */ uiBrowseOrCreateChannel(item: string): Chainable; diff --git a/e2e-tests/cypress/tests/support/ui_commands.ts b/e2e-tests/cypress/tests/support/ui_commands.ts index a137537502..a5a0d81a0b 100644 --- a/e2e-tests/cypress/tests/support/ui_commands.ts +++ b/e2e-tests/cypress/tests/support/ui_commands.ts @@ -788,7 +788,7 @@ declare global { updateChannelHeader(text: string): ChainableT; /** - * Navigate to system console-PluginManagement from account settings + * Navigate to system console-PluginManagement from profile settings */ checkRunLDAPSync: typeof checkRunLDAPSync; diff --git a/model/config.go b/model/config.go index 4868229bbf..a91ac29c40 100644 --- a/model/config.go +++ b/model/config.go @@ -239,10 +239,10 @@ const ( Office365SettingsDefaultTokenEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/token" Office365SettingsDefaultUserAPIEndpoint = "https://graph.microsoft.com/v1.0/me" - CloudSettingsDefaultCwsURL = "https://customers.mattermost.com" + CloudSettingsDefaultCwsURL = "https://customers.cloud.mattermost.com" CloudSettingsDefaultCwsAPIURL = "https://portal.internal.prod.cloud.mattermost.com" // TODO: update to "https://portal.test.cloud.mattermost.com" when ready to use test license key - CloudSettingsDefaultCwsURLTest = "https://customers.mattermost.com" + CloudSettingsDefaultCwsURLTest = "https://customers.cloud.mattermost.com" // TODO: update to // "https://api.internal.test.cloud.mattermost.com" when ready to use test license key CloudSettingsDefaultCwsAPIURLTest = "https://portal.internal.prod.cloud.mattermost.com" diff --git a/model/hosted_customer.go b/model/hosted_customer.go index 543ea12b74..608892e5e5 100644 --- a/model/hosted_customer.go +++ b/model/hosted_customer.go @@ -8,6 +8,12 @@ type BootstrapSelfHostedSignupRequest struct { Reset bool `json:"reset"` } +type SubscribeNewsletterRequest struct { + Email string `json:"email"` + ServerID string `json:"server_id"` + SubscribedContent string `json:"subscribed_content"` +} + type BootstrapSelfHostedSignupResponse struct { Progress string `json:"progress"` // email listed on the JWT claim diff --git a/model/license.go b/model/license.go index 7fa211e4e8..53742b9345 100644 --- a/model/license.go +++ b/model/license.go @@ -442,7 +442,7 @@ func (lr *LicenseRecord) IsValid() *AppError { } if lr.Bytes == "" || len(lr.Bytes) > 10000 { - return NewAppError("LicenseRecord.IsValid", "model.license_record.is_valid.create_at.app_error", nil, "", http.StatusBadRequest) + return NewAppError("LicenseRecord.IsValid", "model.license_record.is_valid.bytes.app_error", nil, "", http.StatusBadRequest) } return nil diff --git a/server/boards/api/files.go b/server/boards/api/files.go index 344933fc5d..4a7eb5e6e7 100644 --- a/server/boards/api/files.go +++ b/server/boards/api/files.go @@ -145,6 +145,12 @@ func (a *API) handleServeFile(w http.ResponseWriter, r *http.Request) { _ = a.app.MoveFile(board.ChannelID, board.TeamID, boardID, filename) } + if err != nil { + // if err is still not nil then it is an error other than `not found` so we must + // return the error to the requestor. fileReader and Fileinfo are nil in this case. + a.errorResponse(w, r, err) + } + defer fileReader.Close() mimeType := "" diff --git a/server/channels/api4/apitestlib.go b/server/channels/api4/apitestlib.go index 921ceb0a2c..44d6540af5 100644 --- a/server/channels/api4/apitestlib.go +++ b/server/channels/api4/apitestlib.go @@ -177,7 +177,7 @@ func setupTestHelper(dbStore store.Store, searchEngine *searchengine.Broker, ent *cfg.PasswordSettings.Symbol = false *cfg.PasswordSettings.Number = false - *cfg.ServiceSettings.ListenAddress = ":0" + *cfg.ServiceSettings.ListenAddress = "localhost:0" }) if err := th.Server.Start(); err != nil { panic(err) diff --git a/server/channels/api4/hosted_customer.go b/server/channels/api4/hosted_customer.go index bbed311ea4..ba791c28cc 100644 --- a/server/channels/api4/hosted_customer.go +++ b/server/channels/api4/hosted_customer.go @@ -36,6 +36,8 @@ func (api *API) InitHostedCustomer() { api.BaseRoutes.HostedCustomer.Handle("/invoices", api.APISessionRequired(selfHostedInvoices)).Methods("GET") // GET /api/v4/hosted_customer/invoices/{invoice_id:in_[A-Za-z0-9]+}/pdf api.BaseRoutes.HostedCustomer.Handle("/invoices/{invoice_id:in_[A-Za-z0-9]+}/pdf", api.APISessionRequired(selfHostedInvoicePDF)).Methods("GET") + + api.BaseRoutes.HostedCustomer.Handle("/subscribe-newsletter", api.APIHandler(handleSubscribeToNewsletter)).Methods(http.MethodPost) } func ensureSelfHostedAdmin(c *Context, where string) { @@ -293,3 +295,33 @@ func selfHostedInvoicePDF(c *Context, w http.ResponseWriter, r *http.Request) { r, ) } + +func handleSubscribeToNewsletter(c *Context, w http.ResponseWriter, r *http.Request) { + const where = "Api4.handleSubscribeToNewsletter" + ensured := ensureCloudInterface(c, where) + if !ensured { + return + } + + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + c.Err = model.NewAppError(where, "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err) + return + } + + req := new(model.SubscribeNewsletterRequest) + err = json.Unmarshal(bodyBytes, req) + if err != nil { + c.Err = model.NewAppError(where, "api.cloud.request_error", nil, "", http.StatusBadRequest).Wrap(err) + return + } + + req.ServerID = c.App.Srv().TelemetryId() + + if err := c.App.Cloud().SubscribeToNewsletter("", req); err != nil { + c.Err = model.NewAppError(where, "api.server.cws.subscribe_to_newsletter.app_error", nil, "CWS Server failed to subscribe to newsletter.", http.StatusInternalServerError).Wrap(err) + return + } + + ReturnStatusOK(w) +} diff --git a/server/channels/api4/license_test.go b/server/channels/api4/license_test.go index 7762e3fc0f..08a9e57305 100644 --- a/server/channels/api4/license_test.go +++ b/server/channels/api4/license_test.go @@ -120,7 +120,7 @@ func TestUploadLicenseFile(t *testing.T) { require.Equal(t, http.StatusBadRequest, resp.StatusCode) }) - t.Run("try to get gone through trial, with TE build", func(t *testing.T) { + t.Run("try to get one through trial, with TE build", func(t *testing.T) { th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = false }) th.App.Srv().Platform().SetLicenseManager(nil) diff --git a/server/channels/api4/user.go b/server/channels/api4/user.go index ba2ab6e336..11d4a71e0f 100644 --- a/server/channels/api4/user.go +++ b/server/channels/api4/user.go @@ -2372,10 +2372,14 @@ func createUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { audit.AddEventParameter(auditRec, "user_id", c.Params.UserId) defer c.LogAuditRec(auditRec) - if user, err := c.App.GetUser(c.Params.UserId); err == nil { - audit.AddEventParameterAuditable(auditRec, "user", user) + user, err := c.App.GetUser(c.Params.UserId) + if err != nil { + c.Err = err + return } + audit.AddEventParameterAuditable(auditRec, "user", user) + if c.AppContext.Session().IsOAuth { c.SetPermissionError(model.PermissionCreateUserAccessToken) c.Err.DetailedError += ", attempted access by oauth app" @@ -2405,6 +2409,11 @@ func createUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { return } + if user.IsSystemAdmin() && !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) { + c.SetPermissionError(model.PermissionManageSystem) + return + } + accessToken.UserId = c.Params.UserId accessToken.Token = "" diff --git a/server/channels/api4/user_test.go b/server/channels/api4/user_test.go index 893d7f6aca..b6dde5e2a1 100644 --- a/server/channels/api4/user_test.go +++ b/server/channels/api4/user_test.go @@ -4339,7 +4339,38 @@ func TestCreateUserAccessToken(t *testing.T) { CheckForbiddenStatus(t, resp) }) - t.Run("create user access token for basic user as as system admin", func(t *testing.T) { + t.Run("create user access token for another user, with permission", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + th.AddPermissionToRole(model.PermissionEditOtherUsers.Id, model.SystemUserManagerRoleId) + th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserManagerRoleId+" "+model.SystemUserAccessTokenRoleId, false) + + rtoken, _, err := th.Client.CreateUserAccessToken(th.BasicUser2.Id, "test token") + require.NoError(t, err) + assert.Equal(t, th.BasicUser2.Id, rtoken.UserId) + + oldSessionToken := th.Client.AuthToken + defer func() { th.Client.AuthToken = oldSessionToken }() + + assertToken(t, th, rtoken, th.BasicUser2.Id) + }) + + t.Run("create user access token for system admin, as system user manager", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + th.AddPermissionToRole(model.PermissionEditOtherUsers.Id, model.SystemUserManagerRoleId) + th.App.UpdateUserRoles(th.Context, th.BasicUser.Id, model.SystemUserManagerRoleId+" "+model.SystemUserAccessTokenRoleId, false) + + _, resp, err := th.Client.CreateUserAccessToken(th.SystemAdminUser.Id, "test token") + require.Error(t, err) + CheckForbiddenStatus(t, resp) + }) + + t.Run("create user access token for basic user as a system admin", func(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() diff --git a/server/channels/app/app_test.go b/server/channels/app/app_test.go index ff725fecdf..291a4daa5f 100644 --- a/server/channels/app/app_test.go +++ b/server/channels/app/app_test.go @@ -22,7 +22,7 @@ func TestAppRace(t *testing.T) { for i := 0; i < 10; i++ { a, err := New() require.NoError(t, err) - a.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = ":0" }) + a.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = "localhost:0" }) serverErr := a.StartServer() require.NoError(t, serverErr) a.Srv().Shutdown() diff --git a/server/channels/app/helper_test.go b/server/channels/app/helper_test.go index a1b8340f66..1b45e5c1b2 100644 --- a/server/channels/app/helper_test.go +++ b/server/channels/app/helper_test.go @@ -102,7 +102,7 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.MaxUsersPerTeam = 50 }) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.RateLimitSettings.Enable = false }) prevListenAddress := *th.App.Config().ServiceSettings.ListenAddress - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = ":0" }) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = "localhost:0" }) serverErr := th.Server.Start() if serverErr != nil { panic(serverErr) diff --git a/server/channels/app/oauth.go b/server/channels/app/oauth.go index 300fe046b4..b8f0ec4c52 100644 --- a/server/channels/app/oauth.go +++ b/server/channels/app/oauth.go @@ -309,6 +309,10 @@ func (a *App) GetOAuthAccessTokenForCodeFlow(clientId, grantType, redirectURI, c return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal_user.app_error", nil, "", http.StatusNotFound) } + if user.DeleteAt != 0 { + return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "", http.StatusForbidden) + } + accessData, nErr = a.Srv().Store().OAuth().GetPreviousAccessData(user.Id, clientId) if nErr != nil { return nil, model.NewAppError("GetOAuthAccessToken", "api.oauth.get_access_token.internal.app_error", nil, "", http.StatusBadRequest) diff --git a/server/channels/app/oauth_test.go b/server/channels/app/oauth_test.go index 7443683941..2167973391 100644 --- a/server/channels/app/oauth_test.go +++ b/server/channels/app/oauth_test.go @@ -633,3 +633,47 @@ func TestDeauthorizeOAuthApp(t *testing.T) { require.Equal(t, store.NewErrNotFound("AuthData", fmt.Sprintf("code=%s", code)), nErr) assert.Nil(t, data) } + +func TestDeactivatedUserOAuthApp(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableOAuthServiceProvider = true }) + + oapp := &model.OAuthApp{ + Name: "fakeoauthapp" + model.NewRandomString(10), + CreatorId: th.BasicUser2.Id, + Homepage: "https://nowhere.com", + Description: "test", + CallbackUrls: []string{"https://nowhere.com"}, + } + + oapp, err := th.App.CreateOAuthApp(oapp) + require.Nil(t, err) + + authRequest := &model.AuthorizeRequest{ + ResponseType: model.ImplicitResponseType, + ClientId: oapp.Id, + RedirectURI: oapp.CallbackUrls[0], + Scope: "", + State: "123", + } + + redirectUrl, err := th.App.GetOAuthCodeRedirect(th.BasicUser.Id, authRequest) + assert.Nil(t, err) + + uri, uErr := url.Parse(redirectUrl) + require.NoError(t, uErr) + + queryParams := uri.Query() + code := queryParams.Get("code") + + _, appErr := th.App.UpdateActive(th.Context, th.BasicUser, false) + require.Nil(t, appErr) + + resp, accErr := th.App.GetOAuthAccessTokenForCodeFlow(oapp.Id, model.AccessTokenGrantType, oapp.CallbackUrls[0], code, oapp.ClientSecret, "") + assert.Nil(t, resp) + require.NotNil(t, accErr, "Should not get access token") + require.Equal(t, http.StatusBadRequest, accErr.StatusCode) + assert.Equal(t, "api.oauth.get_access_token.expired_code.app_error", accErr.Id) +} diff --git a/server/channels/app/platform/helper_test.go b/server/channels/app/platform/helper_test.go index ae417426c1..5049603f63 100644 --- a/server/channels/app/platform/helper_test.go +++ b/server/channels/app/platform/helper_test.go @@ -143,8 +143,8 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo *memoryConfig.AnnouncementSettings.AdminNoticesEnabled = false *memoryConfig.AnnouncementSettings.UserNoticesEnabled = false *memoryConfig.MetricsSettings.Enable = true - *memoryConfig.ServiceSettings.ListenAddress = ":0" - *memoryConfig.MetricsSettings.ListenAddress = ":0" + *memoryConfig.ServiceSettings.ListenAddress = "localhost:0" + *memoryConfig.MetricsSettings.ListenAddress = "localhost:0" configStore.Set(memoryConfig) ps, err := New(ServiceConfig{ diff --git a/server/channels/app/platform/service_test.go b/server/channels/app/platform/service_test.go index dbb46e9158..e79d335fe6 100644 --- a/server/channels/app/platform/service_test.go +++ b/server/channels/app/platform/service_test.go @@ -112,6 +112,7 @@ func TestMetrics(t *testing.T) { require.NotNil(t, th.Service.metrics) metricsAddr := strings.Replace(th.Service.metrics.listenAddr, "[::]", "http://localhost", 1) + metricsAddr = strings.Replace(metricsAddr, "127.0.0.1", "http://localhost", 1) resp, err := http.Get(metricsAddr) require.NoError(t, err) diff --git a/server/channels/app/post.go b/server/channels/app/post.go index 3e360bc863..35d6f8b6d6 100644 --- a/server/channels/app/post.go +++ b/server/channels/app/post.go @@ -547,6 +547,16 @@ func (a *App) SendEphemeralPost(c request.CTX, userID string, post *model.Post) post = a.PreparePostForClientWithEmbedsAndImages(c, post, true, false, true) post = model.AddPostActionCookies(post, a.PostActionCookieSecret()) + sanitizedPost, appErr := a.SanitizePostMetadataForUser(c, post, userID) + if appErr != nil { + mlog.Error("Failed to sanitize post metadata for user", mlog.String("user_id", userID), mlog.Err(appErr)) + + // If we failed to sanitize the post, we still want to remove the metadata. + sanitizedPost = post.Clone() + sanitizedPost.Metadata = nil + } + post = sanitizedPost + postJSON, jsonErr := post.ToJSON() if jsonErr != nil { mlog.Warn("Failed to encode post to JSON", mlog.Err(jsonErr)) diff --git a/server/channels/app/server_test.go b/server/channels/app/server_test.go index bb5f18e82d..00d2c66f71 100644 --- a/server/channels/app/server_test.go +++ b/server/channels/app/server_test.go @@ -47,7 +47,7 @@ func newServerWithConfig(t *testing.T, f func(cfg *model.Config)) (*Server, erro func TestStartServerSuccess(t *testing.T) { s, err := newServerWithConfig(t, func(cfg *model.Config) { - *cfg.ServiceSettings.ListenAddress = ":0" + *cfg.ServiceSettings.ListenAddress = "localhost:0" }) require.NoError(t, err) @@ -65,7 +65,7 @@ func TestStartServerPortUnavailable(t *testing.T) { require.NoError(t, err) // Listen on the next available port - listener, err := net.Listen("tcp", ":0") + listener, err := net.Listen("tcp", "localhost:0") require.NoError(t, err) // Attempt to listen on the port used above. @@ -104,7 +104,7 @@ func TestStartServerNoS3Bucket(t *testing.T) { AmazonS3PathPrefix: model.NewString(""), AmazonS3SSL: model.NewBool(false), } - *cfg.ServiceSettings.ListenAddress = ":0" + *cfg.ServiceSettings.ListenAddress = "localhost:0" _, _, err := store.Set(cfg) require.NoError(t, err) @@ -131,7 +131,7 @@ func TestStartServerTLSSuccess(t *testing.T) { s, err := newServerWithConfig(t, func(cfg *model.Config) { testDir, _ := fileutils.FindDir("tests") - *cfg.ServiceSettings.ListenAddress = ":0" + *cfg.ServiceSettings.ListenAddress = "localhost:0" *cfg.ServiceSettings.ConnectionSecurity = "TLS" *cfg.ServiceSettings.TLSKeyFile = path.Join(testDir, "tls_test_key.pem") *cfg.ServiceSettings.TLSCertFile = path.Join(testDir, "tls_test_cert.pem") @@ -185,7 +185,7 @@ func TestStartServerTLSVersion(t *testing.T) { cfg := store.Get() testDir, _ := fileutils.FindDir("tests") - *cfg.ServiceSettings.ListenAddress = ":0" + *cfg.ServiceSettings.ListenAddress = "localhost:0" *cfg.ServiceSettings.ConnectionSecurity = "TLS" *cfg.ServiceSettings.TLSMinVer = "1.2" *cfg.ServiceSettings.TLSKeyFile = path.Join(testDir, "tls_test_key.pem") @@ -229,7 +229,7 @@ func TestStartServerTLSOverwriteCipher(t *testing.T) { s, err := newServerWithConfig(t, func(cfg *model.Config) { testDir, _ := fileutils.FindDir("tests") - *cfg.ServiceSettings.ListenAddress = ":0" + *cfg.ServiceSettings.ListenAddress = "localhost:0" *cfg.ServiceSettings.ConnectionSecurity = "TLS" cfg.ServiceSettings.TLSOverwriteCiphers = []string{ "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", @@ -328,7 +328,7 @@ func TestPanicLog(t *testing.T) { testDir, _ := fileutils.FindDir("tests") s.platform.UpdateConfig(func(cfg *model.Config) { - *cfg.ServiceSettings.ListenAddress = ":0" + *cfg.ServiceSettings.ListenAddress = "localhost:0" *cfg.ServiceSettings.ConnectionSecurity = "TLS" *cfg.ServiceSettings.TLSKeyFile = path.Join(testDir, "tls_test_key.pem") *cfg.ServiceSettings.TLSCertFile = path.Join(testDir, "tls_test_cert.pem") @@ -404,7 +404,7 @@ func TestSentry(t *testing.T) { SentryDSN = dsn.String() s, err := newServerWithConfig(t, func(cfg *model.Config) { - *cfg.ServiceSettings.ListenAddress = ":0" + *cfg.ServiceSettings.ListenAddress = "localhost:0" *cfg.LogSettings.EnableSentry = false *cfg.ServiceSettings.ConnectionSecurity = "TLS" *cfg.ServiceSettings.TLSKeyFile = path.Join(testDir, "tls_test_key.pem") @@ -448,7 +448,7 @@ func TestSentry(t *testing.T) { SentryDSN = dsn.String() s, err := newServerWithConfig(t, func(cfg *model.Config) { - *cfg.ServiceSettings.ListenAddress = ":0" + *cfg.ServiceSettings.ListenAddress = "localhost:0" *cfg.ServiceSettings.ConnectionSecurity = "TLS" *cfg.ServiceSettings.TLSKeyFile = path.Join(testDir, "tls_test_key.pem") *cfg.ServiceSettings.TLSCertFile = path.Join(testDir, "tls_test_cert.pem") diff --git a/server/channels/app/slashcommands/command_loadtest.go b/server/channels/app/slashcommands/command_loadtest.go index cd7ab9f217..a0b02267b4 100644 --- a/server/channels/app/slashcommands/command_loadtest.go +++ b/server/channels/app/slashcommands/command_loadtest.go @@ -629,7 +629,7 @@ func (*LoadTestProvider) URLCommand(a *app.App, c request.CTX, args *model.Comma // provide a shortcut to easily access tests stored in doc/developer/tests if !strings.HasPrefix(url, "http") { - url = "https://raw.githubusercontent.com/mattermost/mattermost-server/master/tests/" + url + url = "https://raw.githubusercontent.com/mattermost/mattermost-server/master/server/tests/" + url if path.Ext(url) == "" { url += ".md" @@ -683,7 +683,7 @@ func (*LoadTestProvider) JsonCommand(a *app.App, c request.CTX, args *model.Comm // provide a shortcut to easily access tests stored in doc/developer/tests if !strings.HasPrefix(url, "http") { - url = "https://raw.githubusercontent.com/mattermost/mattermost-server/master/tests/" + url + url = "https://raw.githubusercontent.com/mattermost/mattermost-server/master/server/tests/" + url if path.Ext(url) == "" { url += ".json" diff --git a/server/channels/app/slashcommands/helper_test.go b/server/channels/app/slashcommands/helper_test.go index af574c5cce..1941b7c355 100644 --- a/server/channels/app/slashcommands/helper_test.go +++ b/server/channels/app/slashcommands/helper_test.go @@ -93,7 +93,7 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.MaxUsersPerTeam = 50 }) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.RateLimitSettings.Enable = false }) prevListenAddress := *th.App.Config().ServiceSettings.ListenAddress - th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = ":0" }) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = "localhost:0" }) serverErr := th.Server.Start() if serverErr != nil { panic(serverErr) diff --git a/server/channels/app/user.go b/server/channels/app/user.go index 6b85b28f7e..f0b6b7c826 100644 --- a/server/channels/app/user.go +++ b/server/channels/app/user.go @@ -933,6 +933,10 @@ func (a *App) userDeactivated(c request.CTX, userID string) *model.AppError { a.disableUserBots(c, userID) } + if nErr := a.Srv().Store().OAuth().RemoveAuthDataByUserId(userID); nErr != nil { + mlog.Warn("unable to remove auth data by user id", mlog.Err(nErr)) + } + return nil } diff --git a/server/channels/einterfaces/cloud.go b/server/channels/einterfaces/cloud.go index b5d6a75b68..70cdc4676a 100644 --- a/server/channels/einterfaces/cloud.go +++ b/server/channels/einterfaces/cloud.go @@ -47,4 +47,5 @@ type CloudInterface interface { CheckCWSConnection(userId string) error SelfServeDeleteWorkspace(userID string, deletionRequest *model.WorkspaceDeletionRequest) error + SubscribeToNewsletter(userID string, req *model.SubscribeNewsletterRequest) error } diff --git a/server/channels/einterfaces/mocks/CloudInterface.go b/server/channels/einterfaces/mocks/CloudInterface.go index db7c86acc2..2dd2711650 100644 --- a/server/channels/einterfaces/mocks/CloudInterface.go +++ b/server/channels/einterfaces/mocks/CloudInterface.go @@ -540,6 +540,20 @@ func (_m *CloudInterface) SelfServeDeleteWorkspace(userID string, deletionReques return r0 } +// SubscribeToNewsletter provides a mock function with given fields: userID, req +func (_m *CloudInterface) SubscribeToNewsletter(userID string, req *model.SubscribeNewsletterRequest) error { + ret := _m.Called(userID, req) + + var r0 error + if rf, ok := ret.Get(0).(func(string, *model.SubscribeNewsletterRequest) error); ok { + r0 = rf(userID, req) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // UpdateCloudCustomer provides a mock function with given fields: userID, customerInfo func (_m *CloudInterface) UpdateCloudCustomer(userID string, customerInfo *model.CloudCustomerInfo) (*model.CloudCustomer, error) { ret := _m.Called(userID, customerInfo) diff --git a/server/channels/store/opentracinglayer/opentracinglayer.go b/server/channels/store/opentracinglayer/opentracinglayer.go index 723b8a4775..5a193bcb76 100644 --- a/server/channels/store/opentracinglayer/opentracinglayer.go +++ b/server/channels/store/opentracinglayer/opentracinglayer.go @@ -5555,6 +5555,24 @@ func (s *OpenTracingLayerOAuthStore) RemoveAuthDataByClientId(clientId string, u return err } +func (s *OpenTracingLayerOAuthStore) RemoveAuthDataByUserId(userId string) error { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.RemoveAuthDataByUserId") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + err := s.OAuthStore.RemoveAuthDataByUserId(userId) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return err +} + func (s *OpenTracingLayerOAuthStore) SaveAccessData(accessData *model.AccessData) (*model.AccessData, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "OAuthStore.SaveAccessData") diff --git a/server/channels/store/retrylayer/retrylayer.go b/server/channels/store/retrylayer/retrylayer.go index 3603bd7f7e..f7ec2bfd27 100644 --- a/server/channels/store/retrylayer/retrylayer.go +++ b/server/channels/store/retrylayer/retrylayer.go @@ -6298,6 +6298,27 @@ func (s *RetryLayerOAuthStore) RemoveAuthDataByClientId(clientId string, userId } +func (s *RetryLayerOAuthStore) RemoveAuthDataByUserId(userId string) error { + + tries := 0 + for { + err := s.OAuthStore.RemoveAuthDataByUserId(userId) + if err == nil { + return nil + } + if !isRepeatableError(err) { + return err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return err + } + timepkg.Sleep(100 * timepkg.Millisecond) + } + +} + func (s *RetryLayerOAuthStore) SaveAccessData(accessData *model.AccessData) (*model.AccessData, error) { tries := 0 diff --git a/server/channels/store/sqlstore/oauth_store.go b/server/channels/store/sqlstore/oauth_store.go index 10b9d696ed..ba738b7aa8 100644 --- a/server/channels/store/sqlstore/oauth_store.go +++ b/server/channels/store/sqlstore/oauth_store.go @@ -269,6 +269,14 @@ func (as SqlOAuthStore) RemoveAuthDataByClientId(clientId string, userId string) return nil } +func (as SqlOAuthStore) RemoveAuthDataByUserId(userId string) error { + _, err := as.GetMasterX().Exec("DELETE FROM OAuthAuthData WHERE UserId = ?", userId) + if err != nil { + return errors.Wrapf(err, "failed to delete AuthData with userId=%s", userId) + } + return nil +} + func (as SqlOAuthStore) PermanentDeleteAuthDataByUser(userId string) error { _, err := as.GetMasterX().Exec("DELETE FROM OAuthAccessData WHERE UserId = ?", userId) if err != nil { diff --git a/server/channels/store/sqlstore/store.go b/server/channels/store/sqlstore/store.go index a1d7380b38..8000384e95 100644 --- a/server/channels/store/sqlstore/store.go +++ b/server/channels/store/sqlstore/store.go @@ -237,7 +237,9 @@ func SetupConnection(connType string, dataSource string, settings *model.SqlSett } for i := 0; i < DBPingAttempts; i++ { - mlog.Info("Pinging SQL", mlog.String("database", connType), mlog.String("dataSource", dataSource)) + // At this point, we have passed sql.Open, so we deliberately ignore any errors. + sanitized, _ := SanitizeDataSource(*settings.DriverName, dataSource) + mlog.Info("Pinging SQL", mlog.String("database", connType), mlog.String("dataSource", sanitized)) ctx, cancel := context.WithTimeout(context.Background(), DBPingTimeoutSecs*time.Second) defer cancel() err = db.PingContext(ctx) diff --git a/server/channels/store/sqlstore/utils.go b/server/channels/store/sqlstore/utils.go index 809439c820..5242f0c527 100644 --- a/server/channels/store/sqlstore/utils.go +++ b/server/channels/store/sqlstore/utils.go @@ -5,6 +5,7 @@ package sqlstore import ( "database/sql" + "errors" "io" "net/url" "strconv" @@ -206,3 +207,29 @@ func ResetReadTimeout(dataSource string) (string, error) { config.ReadTimeout = 0 return config.FormatDSN(), nil } + +func SanitizeDataSource(driverName, dataSource string) (string, error) { + switch driverName { + case model.DatabaseDriverPostgres: + u, err := url.Parse(dataSource) + if err != nil { + return "", err + } + u.User = url.UserPassword("****", "****") + params := u.Query() + params.Del("user") + params.Del("password") + u.RawQuery = params.Encode() + return u.String(), nil + case model.DatabaseDriverMysql: + cfg, err := mysql.ParseDSN(dataSource) + if err != nil { + return "", err + } + cfg.User = "****" + cfg.Passwd = "****" + return cfg.FormatDSN(), nil + default: + return "", errors.New("invalid drivername. Not postgres or mysql.") + } +} diff --git a/server/channels/store/sqlstore/utils_test.go b/server/channels/store/sqlstore/utils_test.go index 811ebf001a..96468323bb 100644 --- a/server/channels/store/sqlstore/utils_test.go +++ b/server/channels/store/sqlstore/utils_test.go @@ -6,6 +6,7 @@ package sqlstore import ( "testing" + "github.com/mattermost/mattermost-server/v6/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -160,3 +161,45 @@ func TestAppendMultipleStatementsFlag(t *testing.T) { }) } } + +func TestSanitizeDataSource(t *testing.T) { + t.Run(model.DatabaseDriverPostgres, func(t *testing.T) { + testCases := []struct { + Original string + Sanitized string + }{ + { + "postgres://mmuser:mostest@localhost/dummy?sslmode=disable", + "postgres://%2A%2A%2A%2A:%2A%2A%2A%2A@localhost/dummy?sslmode=disable", + }, + { + "postgres://localhost/dummy?sslmode=disable&user=mmuser&password=mostest", + "postgres://%2A%2A%2A%2A:%2A%2A%2A%2A@localhost/dummy?sslmode=disable", + }, + } + driver := model.DatabaseDriverPostgres + for _, tc := range testCases { + out, err := SanitizeDataSource(driver, tc.Original) + require.NoError(t, err) + assert.Equal(t, tc.Sanitized, out) + } + }) + + t.Run(model.DatabaseDriverMysql, func(t *testing.T) { + testCases := []struct { + Original string + Sanitized string + }{ + { + "mmuser:mostest@tcp(localhost:3306)/mattermost_test?charset=utf8mb4,utf8&readTimeout=30s&writeTimeout=30s", + "****:****@tcp(localhost:3306)/mattermost_test?readTimeout=30s&writeTimeout=30s&charset=utf8mb4%2Cutf8", + }, + } + driver := model.DatabaseDriverMysql + for _, tc := range testCases { + out, err := SanitizeDataSource(driver, tc.Original) + require.NoError(t, err) + assert.Equal(t, tc.Sanitized, out) + } + }) +} diff --git a/server/channels/store/store.go b/server/channels/store/store.go index e52c2037ab..52774d2e71 100644 --- a/server/channels/store/store.go +++ b/server/channels/store/store.go @@ -564,6 +564,7 @@ type OAuthStore interface { GetAuthData(code string) (*model.AuthData, error) RemoveAuthData(code string) error RemoveAuthDataByClientId(clientId string, userId string) error + RemoveAuthDataByUserId(userId string) error PermanentDeleteAuthDataByUser(userID string) error SaveAccessData(accessData *model.AccessData) (*model.AccessData, error) UpdateAccessData(accessData *model.AccessData) (*model.AccessData, error) diff --git a/server/channels/store/storetest/mocks/OAuthStore.go b/server/channels/store/storetest/mocks/OAuthStore.go index 77d8beeb6e..75cd533163 100644 --- a/server/channels/store/storetest/mocks/OAuthStore.go +++ b/server/channels/store/storetest/mocks/OAuthStore.go @@ -305,6 +305,20 @@ func (_m *OAuthStore) RemoveAuthDataByClientId(clientId string, userId string) e return r0 } +// RemoveAuthDataByUserId provides a mock function with given fields: userId +func (_m *OAuthStore) RemoveAuthDataByUserId(userId string) error { + ret := _m.Called(userId) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(userId) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // SaveAccessData provides a mock function with given fields: accessData func (_m *OAuthStore) SaveAccessData(accessData *model.AccessData) (*model.AccessData, error) { ret := _m.Called(accessData) diff --git a/server/channels/store/timerlayer/timerlayer.go b/server/channels/store/timerlayer/timerlayer.go index 2c156ccbe5..7c8c64a033 100644 --- a/server/channels/store/timerlayer/timerlayer.go +++ b/server/channels/store/timerlayer/timerlayer.go @@ -5036,6 +5036,22 @@ func (s *TimerLayerOAuthStore) RemoveAuthDataByClientId(clientId string, userId return err } +func (s *TimerLayerOAuthStore) RemoveAuthDataByUserId(userId string) error { + start := time.Now() + + err := s.OAuthStore.RemoveAuthDataByUserId(userId) + + elapsed := float64(time.Since(start)) / float64(time.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("OAuthStore.RemoveAuthDataByUserId", success, elapsed) + } + return err +} + func (s *TimerLayerOAuthStore) SaveAccessData(accessData *model.AccessData) (*model.AccessData, error) { start := time.Now() diff --git a/server/channels/web/web_test.go b/server/channels/web/web_test.go index a4458d39f1..3fcddc2023 100644 --- a/server/channels/web/web_test.go +++ b/server/channels/web/web_test.go @@ -107,7 +107,7 @@ func setupTestHelper(tb testing.TB, includeCacheLayer bool) *TestHelper { a := app.New(app.ServerConnector(s.Channels())) prevListenAddress := *s.Config().ServiceSettings.ListenAddress - a.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = ":0" }) + a.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = "localhost:0" }) serverErr := s.Start() if serverErr != nil { panic(serverErr) diff --git a/server/cmd/mattermost/commands/server_test.go b/server/cmd/mattermost/commands/server_test.go index 8f91c3ecd6..eef8af50e7 100644 --- a/server/cmd/mattermost/commands/server_test.go +++ b/server/cmd/mattermost/commands/server_test.go @@ -16,7 +16,7 @@ import ( ) const ( - unitTestListeningPort = ":0" + unitTestListeningPort = "localhost:0" ) //nolint:golint,unused diff --git a/server/config/database.go b/server/config/database.go index f2463e33b4..dd508d9c4f 100644 --- a/server/config/database.go +++ b/server/config/database.go @@ -407,7 +407,10 @@ func (ds *DatabaseStore) RemoveFile(name string) error { // String returns the path to the database backing the config, masking the password. func (ds *DatabaseStore) String() string { - return stripPassword(ds.originalDsn, ds.driverName) + // This is called during the running of MM, so we expect the parsing of DSN + // to be successful. + sanitized, _ := sqlstore.SanitizeDataSource(ds.driverName, ds.originalDsn) + return sanitized } // Close cleans up resources associated with the store. diff --git a/server/config/database_test.go b/server/config/database_test.go index 4eab71fc5c..6954461a08 100644 --- a/server/config/database_test.go +++ b/server/config/database_test.go @@ -1107,12 +1107,12 @@ func TestDatabaseStoreString(t *testing.T) { if *mainHelper.GetSQLSettings().DriverName == "postgres" { maskedDSN := ds.String() assert.True(t, strings.HasPrefix(maskedDSN, "postgres://")) - assert.True(t, strings.Contains(maskedDSN, "mmuser")) + assert.False(t, strings.Contains(maskedDSN, "mmuser")) assert.False(t, strings.Contains(maskedDSN, "mostest")) } else { maskedDSN := ds.String() - assert.True(t, strings.HasPrefix(maskedDSN, "mysql://")) - assert.True(t, strings.Contains(maskedDSN, "mmuser")) + assert.False(t, strings.HasPrefix(maskedDSN, "mysql://")) + assert.False(t, strings.Contains(maskedDSN, "mmuser")) assert.False(t, strings.Contains(maskedDSN, "mostest")) } } diff --git a/server/config/utils.go b/server/config/utils.go index b343106562..f247ea4a43 100644 --- a/server/config/utils.go +++ b/server/config/utils.go @@ -179,27 +179,6 @@ func IsDatabaseDSN(dsn string) bool { strings.HasPrefix(dsn, "postgresql://") } -// stripPassword remove the password from a given DSN -func stripPassword(dsn, schema string) string { - prefix := schema + "://" - dsn = strings.TrimPrefix(dsn, prefix) - - i := strings.Index(dsn, ":") - j := strings.LastIndex(dsn, "@") - - // Return error if no @ sign is found - if j < 0 { - return "(omitted due to error parsing the DSN)" - } - - // Return back the input if no password is found - if i < 0 || i > j { - return prefix + dsn - } - - return prefix + dsn[:i+1] + dsn[j:] -} - func isJSONMap(data string) bool { var m map[string]any return json.Unmarshal([]byte(data), &m) == nil diff --git a/server/config/utils_test.go b/server/config/utils_test.go index e3b903eeb1..9c8de20d23 100644 --- a/server/config/utils_test.go +++ b/server/config/utils_test.go @@ -197,61 +197,6 @@ func TestIsDatabaseDSN(t *testing.T) { } } -func TestStripPassword(t *testing.T) { - for name, test := range map[string]struct { - DSN string - Schema string - ExpectedOut string - }{ - "mysql": { - DSN: "mysql://mmuser:password@tcp(localhost:3306)/mattermost?charset=utf8mb4,utf8&readTimeout=30s", - Schema: "mysql", - ExpectedOut: "mysql://mmuser:@tcp(localhost:3306)/mattermost?charset=utf8mb4,utf8&readTimeout=30s", - }, - "mysql idempotent": { - DSN: "mysql://mmuser:@tcp(localhost:3306)/mattermost?charset=utf8mb4,utf8&readTimeout=30s", - Schema: "mysql", - ExpectedOut: "mysql://mmuser:@tcp(localhost:3306)/mattermost?charset=utf8mb4,utf8&readTimeout=30s", - }, - "mysql: password with : and @": { - DSN: "mysql://mmuser:p:assw@ord@tcp(localhost:3306)/mattermost?charset=utf8mb4,utf8&readTimeout=30s", - Schema: "mysql", - ExpectedOut: "mysql://mmuser:@tcp(localhost:3306)/mattermost?charset=utf8mb4,utf8&readTimeout=30s", - }, - "mysql: password with @ and :": { - DSN: "mysql://mmuser:pa@sswo:rd@tcp(localhost:3306)/mattermost?charset=utf8mb4,utf8&readTimeout=30s", - Schema: "mysql", - ExpectedOut: "mysql://mmuser:@tcp(localhost:3306)/mattermost?charset=utf8mb4,utf8&readTimeout=30s", - }, - "postgres": { - DSN: "postgres://mmuser:password@localhost:5432/mattermost?sslmode=disable&connect_timeout=10", - Schema: "postgres", - ExpectedOut: "postgres://mmuser:@localhost:5432/mattermost?sslmode=disable&connect_timeout=10", - }, - "pipe": { - DSN: "mysql://user@unix(/path/to/socket)/dbname", - Schema: "mysql", - ExpectedOut: "mysql://user@unix(/path/to/socket)/dbname", - }, - "malformed without :": { - DSN: "postgres://mmuserpassword@localhost:5432/mattermost?sslmode=disable&connect_timeout=10", - Schema: "postgres", - ExpectedOut: "postgres://mmuserpassword@localhost:5432/mattermost?sslmode=disable&connect_timeout=10", - }, - "malformed without @": { - DSN: "postgres://mmuser:passwordlocalhost:5432/mattermost?sslmode=disable&connect_timeout=10", - Schema: "postgres", - ExpectedOut: "(omitted due to error parsing the DSN)", - }, - } { - t.Run(name, func(t *testing.T) { - out := stripPassword(test.DSN, test.Schema) - - assert.Equal(t, test.ExpectedOut, out) - }) - } -} - func TestIsJSONMap(t *testing.T) { tests := []struct { name string diff --git a/server/i18n/de.json b/server/i18n/de.json index f5d555cb6e..3e7aed88f5 100644 --- a/server/i18n/de.json +++ b/server/i18n/de.json @@ -9022,50 +9022,6 @@ "id": "api.custom_groups.count_err", "translation": "Fehler beim Zählen der Gruppen" }, - { - "id": "api.templates.server_inactivity_title", - "translation": "Erhöhe die Produktivität mit diesen tollen Funktionen" - }, - { - "id": "api.templates.server_inactivity_subtitle", - "translation": "Hi {{.Name}}, wir haben bemerkt, dass Dein Mattermost Server etwas Staub ansetzt, Schau mal auf die neuen Funktionen, die Dir helfen die Belastung Deines Team zu senken." - }, - { - "id": "api.templates.server_inactivity_subject", - "translation": "Öffne Mattermost um Dein Team produktiver zu machen!" - }, - { - "id": "api.templates.server_inactivity_info_bullet2", - "translation": "Verwalte Aufgaben mit " - }, - { - "id": "api.templates.server_inactivity_info_bullet1", - "translation": "Arbeitsablauf-Verwaltung mit " - }, - { - "id": "api.templates.server_inactivity_info_bullet", - "translation": "Gastzugriff auf angegebene " - }, - { - "id": "api.templates.server_inactivity_info", - "translation": "Komm und teste es!" - }, - { - "id": "api.templates.server_inactivity_button", - "translation": "Öffne Mattermost" - }, - { - "id": "Playbooks", - "translation": "Playbooks" - }, - { - "id": "Channels", - "translation": "Kanäle" - }, - { - "id": "Boards", - "translation": "Boards" - }, { "id": "app.job.get_all_jobs_by_type_and_status.app_error", "translation": "Kann nicht alle Jobs nach Typ und Status holen." @@ -9170,10 +9126,6 @@ "id": "api.elasticsearch.test_elasticsearch_settings_nil.app_error", "translation": "Die Elasticsearch-Einstellungen haben nicht definierte Werte." }, - { - "id": "api.templates.server_inactivity_footer_disclaimer", - "translation": "Du hast diese einmalige E-Mail erhalten, weil dein Mattermost-Server für mehr als {{.Hours}} Stunden inaktiv war. Diese E-Mail wurde automatisch von deinem Mattermost-Server generiert." - }, { "id": "api.file.cloud_upload.app_error", "translation": "Hochladen über mmctl zu einer Cloud Instanz wird nicht unterstützt. Bitte prüfe die Dokumentation: https://docs.mattermost.com/manage/cloud-data-export.html." @@ -9510,10 +9462,6 @@ "id": "model.group.name.reserved_name.app_error", "translation": "Gruppenname existiert bereits als reservierter Name" }, - { - "id": "app.plugin.product_mode.app_error", - "translation": "Plugin {{.Name}} kann im Produktmodus nicht aktiviert werden." - }, { "id": "api.team.invite_guests_to_channels.license.error", "translation": "Deine Lizenz unterstützt Gastkonten nicht" @@ -9586,10 +9534,6 @@ "id": "app.post_prority.get_for_post.app_error", "translation": "Die Priorität der Nachricht kann nicht ermittelt werden" }, - { - "id": "app.draft.update.app_error", - "translation": "Die Aktualisierung des Entwurfs ist nicht möglich." - }, { "id": "app.draft.save.app_error", "translation": "Der Entwurf kann nicht gespeichert werden." @@ -9664,7 +9608,7 @@ }, { "id": "worktemplate.category.product_teams", - "translation": "Produkt-Teams" + "translation": "Produkt" }, { "id": "model.draft.is_valid.priority.app_error", @@ -9680,7 +9624,7 @@ }, { "id": "worktemplate.product_teams.feature_release.description.integration", - "translation": "Steigere die Produktivität in deinem Kanal durch die Integration eines Jira-Bots und eines Github-Bots. Diese werden für dich heruntergeladen." + "translation": "Steigere die Produktivität in deinem Kanal durch die Integration deiner am meistern verwendeten Tools, wie GitHub oder Jira. Diese werden für dich heruntergeladen." }, { "id": "api.templates.cloud_welcome_email.yearly_plan_button", @@ -10101,5 +10045,125 @@ { "id": "app.command.execute.error", "translation": "Kann Befehl nicht ausführen." + }, + { + "id": "api.license.request-trial.bad-request.business-email", + "translation": "Ungültige geschäftliche E-Mail für den Test" + }, + { + "id": "worktemplate.product_teams.sprint_planning.integration", + "translation": "Steigere die Produktivität deines Kanals, indem du die am häufigsten verwendeten Tools wie z. B. Zoom integrierst. Diese werden für dich heruntergeladen." + }, + { + "id": "worktemplate.product_teams.sprint_planning.channel", + "translation": "Chatte mit deinem Team in einem Kanal, der sich leicht mit deinen Boards und Integrationen verbinden lässt." + }, + { + "id": "worktemplate.product_teams.sprint_planning.board", + "translation": "Verfolge den Fortschritt deines Teams bei der Erreichung der wöchentlichen Ziele mit Sprintaufteilung, Priorisierung, Zuweisung von Verantwortlichen und Kommentaren." + }, + { + "id": "worktemplate.product_teams.product_roadmap.channel", + "translation": "Chatte mit deinem Team über das Feedback deiner Kunden, setze Prioritäten und stimmt euch gemeinsam über den Fortschritt ab." + }, + { + "id": "worktemplate.product_teams.product_roadmap.board", + "translation": "Verwende das Produkt-Roadmap-Board, um Benutzer-Feedback zu verwalten, Ressourcen zuzuweisen, Ergebnisse in einer Kalenderansicht anzuzeigen und Probleme nach Priorität zu ordnen." + }, + { + "id": "worktemplate.product_teams.goals_and_okrs.integration", + "translation": "Steigere die Produktivität in deinem Kanal, indem du die am häufigsten verwendeten Tools wie Zoom integrierst, um die Zusammenarbeit zu erleichtern. Diese werden für dich heruntergeladen." + }, + { + "id": "worktemplate.product_teams.goals_and_okrs.channel", + "translation": "Chatte mit deinem Team über Ziele und Fortschritte, asynchron oder in Echtzeit, und bleibe über Änderungen in einem einzigen Kanal auf dem Laufenden." + }, + { + "id": "worktemplate.product_teams.goals_and_okrs.board", + "translation": "Verfolge den Fortschritt deines Teams auf dem Weg zu den Unternehmenszielen mit dem Ziele und OKR Board. Halten Besprechungen mit der Besprechungsagenda auf Kurs." + }, + { + "id": "worktemplate.product_teams.feature_release.description.playbook", + "translation": "Fördere die funktionsübergreifende Zusammenarbeit im Team mit Aufgaben-Checklisten und Automatisierungen, die deinen Entwicklungsprozess unterstützen. Führe anschließend eine Retrospektive durch und nimm Verbesserungen für deine nächste Version vor." + }, + { + "id": "worktemplate.product_teams.feature_release.description.channel", + "translation": "Chatte mit deinem Team über alle Release-Blocker und Änderungen in einem Kanal, der sich leicht mit deinen Boards, Playbooks und anderen Integrationen verbinden lässt." + }, + { + "id": "worktemplate.product_teams.feature_release.description.board", + "translation": "Halte Besprechungen mit der Besprechungsagenda auf dem Laufenden. Verwalte dein Arbeitspensum mit dem Projektaufgabenboard." + }, + { + "id": "worktemplate.product_teams.bug_bash.playbook", + "translation": "Verwende Checklisten, um Testbereiche zuzuweisen, und automatisierte Aufgaben, um einen umfassenden Fehlerbehebungsprozess durchzuführen. Nutze eine Retrospektive, um deinen Prozess zu überprüfen und ihn für das nächste Mal zu verbessern." + }, + { + "id": "worktemplate.product_teams.bug_bash.integration", + "translation": "Steigere die Produktivität in deinem Kanal, indem du die am häufigsten verwendeten Tools, wie z. B. Jira, integrierst, um den Fortschritt bei der Fehlerbehebung zu verfolgen. Diese werden für dich heruntergeladen." + }, + { + "id": "worktemplate.product_teams.bug_bash.channel", + "translation": "Plane und verwalte Fehlerberichte und -behebungen in einem einzigen Kanal, der für dein Team und deine Organisation leicht zugänglich ist." + }, + { + "id": "worktemplate.leadership.goals_and_okrs.integration", + "translation": "Steigere die Produktivität in deinem Kanal, indem du die am häufigsten verwendeten Tools wie Zoom integrierst, um die Zusammenarbeit zu erleichtern. Diese werden für dich heruntergeladen." + }, + { + "id": "worktemplate.leadership.goals_and_okrs.channel", + "translation": "Chatte mit deinem Team über Ziele und Fortschritte, asynchron oder in Echtzeit, und bleibe über Änderungen in einem einzigen Kanal auf dem Laufenden." + }, + { + "id": "worktemplate.leadership.goals_and_okrs.board", + "translation": "Verfolge den Fortschritt deines Teams auf dem Weg zu den Unternehmenszielen mit dem Ziele und OKR Board. Halten Besprechungen mit der Besprechungsagenda auf Kurs." + }, + { + "id": "worktemplate.devops.product_release.playbook", + "translation": "Erstelle wiederholbare Arbeitsabläufe, die einfach zu befolgen und zu implementieren sind, damit die Produktveröffentlichungen zuverlässig und pünktlich erfolgen." + }, + { + "id": "worktemplate.devops.product_release.channel", + "translation": "Chatte einfach und schnell mit deinem Team über tägliche Meilensteine, eventuelle Hindernisse und Änderungen an den zu erbringenden Leistungen." + }, + { + "id": "worktemplate.devops.product_release.board", + "translation": "Verwende das Product Release Board, um den Zeitrahmen und den Prozess für die Freigabe zu unterstützen und sicherzustellen, dass jeder weiß, welche Aufgaben fällig sind." + }, + { + "id": "worktemplate.devops.incident_resolution.description.playbook", + "translation": "Nutze Checklisten und Automatisierungen, um wichtige Teammitglieder einzubeziehen, und teile mit, wie der Vorfall gelöst wird." + }, + { + "id": "worktemplate.devops.incident_resolution.description.channel", + "translation": "Chatte mit deinem Team über Prioritäten, füge Beteiligte hinzu, liefere Updates und arbeite an einer Lösung in einem einzigen Kanal." + }, + { + "id": "worktemplate.devops.incident_resolution.description.board", + "translation": "Verwende das Incident Resolution Board, um wiederholbare Prozesse zu unterstützen und definierte Aufgaben im Team zuzuweisen." + }, + { + "id": "worktemplate.companywide.goals_and_okrs.integration", + "translation": "Steigere die Produktivität in deinem Kanal, indem du die am häufigsten verwendeten Tools wie Zoom integrierst, um die Zusammenarbeit zu erleichtern. Diese werden für dich heruntergeladen." + }, + { + "id": "worktemplate.companywide.goals_and_okrs.channel", + "translation": "Chatte mit deinem Team über Ziele und Fortschritte, asynchron oder in Echtzeit, und bleibe über Änderungen in einem einzigen Kanal auf dem Laufenden." + }, + { + "id": "worktemplate.companywide.goals_and_okrs.board", + "translation": "Verfolge den Fortschritt deines Teams auf dem Weg zu den Unternehmenszielen mit dem Ziele und OKR Board. Halten Besprechungen mit der Besprechungsagenda auf Kurs." + }, + { + "id": "worktemplate.companywide.create_project.integration", + "translation": "Steigere die Produktivität deines Kanals durch die Integration am deiner am häufigsten verwendeten Tools. Diese werden für dich heruntergeladen." + }, + { + "id": "worktemplate.companywide.create_project.channel", + "translation": "Chatte mit deinem Team über ein neues Projekt und entscheide, wie es strukturiert werden soll, und zwar in einem Kanal zur Zusammenarbeit." + }, + { + "id": "worktemplate.companywide.create_project.board", + "translation": "Verwend eine Kanban-Board, um deine Projektaufgaben und -fortschritte zu definieren und zu verfolgen." } ] diff --git a/server/i18n/en.json b/server/i18n/en.json index 506628e0bb..e91fbf2656 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -2591,6 +2591,10 @@ "id": "api.server.cws.needs_enterprise_edition", "translation": "Service only available in Mattermost Enterprise edition" }, + { + "id": "api.server.cws.subscribe_to_newsletter.app_error", + "translation": "CWS Server failed to subscribe to newsletter." + }, { "id": "api.server.hosted_signup_unavailable.error", "translation": "Portal unavailable for self-hosted signup." @@ -9227,6 +9231,10 @@ "id": "model.job.is_valid.type.app_error", "translation": "Invalid job type." }, + { + "id": "model.license_record.is_valid.bytes.app_error", + "translation": "Invalid value for bytes when uploading a license." + }, { "id": "model.license_record.is_valid.create_at.app_error", "translation": "Invalid value for create_at when uploading a license." diff --git a/server/i18n/en_AU.json b/server/i18n/en_AU.json index 9bb7f749bd..8a71e91c0d 100644 --- a/server/i18n/en_AU.json +++ b/server/i18n/en_AU.json @@ -9026,50 +9026,6 @@ "id": "app.job.get_all_jobs_by_type_and_status.app_error", "translation": "Unable to get the all jobs by type and status." }, - { - "id": "api.templates.server_inactivity_title", - "translation": "Unlock increased productivity with these awesome features" - }, - { - "id": "api.templates.server_inactivity_subtitle", - "translation": "Hey {{.Name}}, your Mattermost server has been a bit inactive. Would you like to take a look at some features that can help lighten your team's workload?" - }, - { - "id": "api.templates.server_inactivity_subject", - "translation": "Open Mattermost to increase your team’s productivity!" - }, - { - "id": "api.templates.server_inactivity_info_bullet2", - "translation": "Manage tasks using " - }, - { - "id": "api.templates.server_inactivity_info_bullet1", - "translation": "Workflow management with " - }, - { - "id": "api.templates.server_inactivity_info_bullet", - "translation": "Guest Access to specified " - }, - { - "id": "api.templates.server_inactivity_info", - "translation": "Come and check it out!" - }, - { - "id": "api.templates.server_inactivity_button", - "translation": "Open Mattermost" - }, - { - "id": "Playbooks", - "translation": "Playbooks" - }, - { - "id": "Channels", - "translation": "Channels" - }, - { - "id": "Boards", - "translation": "Boards" - }, { "id": "app.prepackged-plugin.invalid_version.app_error", "translation": "A prepackged plugin version could not be parsed." @@ -9166,10 +9122,6 @@ "id": "api.elasticsearch.test_elasticsearch_settings_nil.app_error", "translation": "Elasticsearch settings has unset values." }, - { - "id": "api.templates.server_inactivity_footer_disclaimer", - "translation": "You received this one-time email because your Mattermost server was inactive for more than {{.Hours}} hours. This email was automatically generated by your Mattermost server." - }, { "id": "app.recent_searches.app_error", "translation": "An error occurred while fetching recent searches" @@ -9518,10 +9470,6 @@ "id": "model.insights.get_start_of_day_for_time_range.time_range.app_error", "translation": "Invalid time range." }, - { - "id": "app.plugin.product_mode.app_error", - "translation": "Plugin {{.Name}} cannot be enabled in product mode." - }, { "id": "api.team.invite_guests_to_channels.license.error", "translation": "Your workspace licence does not support guest accounts" @@ -9594,10 +9542,6 @@ "id": "app.post_prority.get_for_post.app_error", "translation": "Unable to get post priority for post" }, - { - "id": "app.draft.update.app_error", - "translation": "Unable to update the draft." - }, { "id": "app.draft.save.app_error", "translation": "Unable to save the draft." diff --git a/server/i18n/es.json b/server/i18n/es.json index 0e1791f125..6a8f3c43ce 100644 --- a/server/i18n/es.json +++ b/server/i18n/es.json @@ -8975,10 +8975,6 @@ "id": "model.config.is_valid.elastic_search.bulk_indexing_batch_size.app_error", "translation": "El tamaño del lote de indexación masiva de Elasticsearch debe ser al menos de {{.BatchSize}}." }, - { - "id": "api.templates.server_inactivity_footer_disclaimer", - "translation": "Recibiste este correo electrónico único porque tu servidor Mattermost estuvo inactivo durante más de {{.Hours}} horas. Este correo electrónico fue generado automáticamente por tu servidor Mattermost." - }, { "id": "api.custom_groups.no_remote_id", "translation": " " @@ -8987,10 +8983,6 @@ "id": "app.system.get_onboarding_request.app_error", "translation": "No se pudo obtener el estado de finalización de inducción." }, - { - "id": "Boards", - "translation": " " - }, { "id": "app.custom_group.unique_name", "translation": " " @@ -9059,26 +9051,6 @@ "id": "api.getThreadsForUser.bad_only_params", "translation": " " }, - { - "id": "api.templates.server_inactivity_button", - "translation": " " - }, - { - "id": "api.templates.server_inactivity_info", - "translation": "¡Ven y dale un vistazo!" - }, - { - "id": "api.templates.server_inactivity_info_bullet2", - "translation": " " - }, - { - "id": "api.templates.server_inactivity_subtitle", - "translation": "Hey {{.Name}}, notamos que tu servidor Mattermost está acumulando algo de polvo. Da un vistazo a algunas características que pueden aligerar la carga de trabajo de tu equipo." - }, - { - "id": "api.templates.server_inactivity_title", - "translation": " " - }, { "id": "app.job.get_all_jobs_by_type_and_status.app_error", "translation": "No se pudo obtener todos los jobs por tipo y estado." @@ -9115,14 +9087,6 @@ "id": "api.elasticsearch.test_elasticsearch_settings_nil.app_error", "translation": "Los ajustes de Búsqueda Elástica tienen valores no establecidos." }, - { - "id": "api.templates.server_inactivity_info_bullet1", - "translation": " " - }, - { - "id": "api.templates.server_inactivity_subject", - "translation": "¡Ven y abre Mattermost para aumentar la productividad de tu equipo!" - }, { "id": "api.team.invite_members_to_team_and_channels.invalid_body.app_error", "translation": "Cuerpo de solicitud no válido." @@ -9207,10 +9171,6 @@ "id": "api.templates.invite_team_and_channel_body.title", "translation": "{{ .SenderName }} te invitó a unirte al Canal {{ .ChannelName }} en el Equipo {{ .TeamDisplayName}}" }, - { - "id": "api.templates.server_inactivity_info_bullet", - "translation": " " - }, { "id": "api.templates.cloud_upgrade_confirmation.title", "translation": "¡Ahora estás actualizado!" @@ -9227,14 +9187,6 @@ "id": "api.team.invite_members.unable_to_send_email_with_defaults.app_error", "translation": " " }, - { - "id": "Playbooks", - "translation": " " - }, - { - "id": "Channels", - "translation": " " - }, { "id": "api.user.authorize_oauth_user.saml_response_too_long.app_error", "translation": " " @@ -9487,10 +9439,6 @@ "id": "app.post.get_top_dms_for_user_since.app_error", "translation": "No es posible obtener los DMs principales para el usuario." }, - { - "id": "app.plugin.product_mode.app_error", - "translation": "El plugin {{.Name}} no se pudo activar en el modo producto." - }, { "id": "app.notify_admin.send_notification_post.app_error", "translation": "No es posible enviar la publicación de notificación." diff --git a/server/i18n/fr.json b/server/i18n/fr.json index 50b5d159c8..77720df20d 100644 --- a/server/i18n/fr.json +++ b/server/i18n/fr.json @@ -8571,34 +8571,6 @@ "id": "api.license.request_renewal_link.cannot_renew_on_cws", "translation": "Le renouvellement de cette licence sur le portail n'est pas possible" }, - { - "id": "api.templates.server_inactivity_subtitle", - "translation": "Bonjour {{.Name}}, nous avons remarqué que votre serveur Mattermost collecte un peu la poussière. Découvrez quelques fonctionnalités qui peuvent vous aider à alléger la charge de travail de votre équipe." - }, - { - "id": "api.templates.server_inactivity_info_bullet2", - "translation": "Gérez des tâches en utilisant " - }, - { - "id": "api.templates.server_inactivity_info", - "translation": "Venez et regardez ça !" - }, - { - "id": "api.templates.server_inactivity_button", - "translation": "Ouvrir Mattermost" - }, - { - "id": "Playbooks", - "translation": "Playbooks" - }, - { - "id": "Channels", - "translation": "Channels" - }, - { - "id": "Boards", - "translation": "Boards" - }, { "id": "app.prepackged-plugin.invalid_version.app_error", "translation": "La version du plugin pré-packagé n'a pas pu être traitée." @@ -8755,26 +8727,6 @@ "id": "api.user.view_archived_channels.get_posts_for_channel.app_error", "translation": "Impossible de retrouver les messages pour un canal archivé" }, - { - "id": "api.templates.server_inactivity_title", - "translation": "Augmentez votre productivité en déverrouillant ces fonctionnalités géniales" - }, - { - "id": "api.templates.server_inactivity_subject", - "translation": "Ouvrez Mattermost pour augmenter la productivité de votre équipe !" - }, - { - "id": "api.templates.server_inactivity_info_bullet1", - "translation": "Gestion du déroulement des opérations avec " - }, - { - "id": "api.templates.server_inactivity_info_bullet", - "translation": "Accès des invités à l'espace spécifié " - }, - { - "id": "api.templates.server_inactivity_footer_disclaimer", - "translation": "Vous avez reçu ce courriel unique parce que votre serveur Mattermost est inactif depuis plus de {{.Hours}} heures. Ce courriel a été généré automatiquement par votre serveur Mattermost." - }, { "id": "api.templates.invite_team_and_channels_subject", "translation": "[{{ .SiteName}}] {{ .SenderName }} vous a invité à rejoindre {{ .ChannelsLen }} canaux de l'équipe {{ .TeamDisplayName }}" diff --git a/server/i18n/hu.json b/server/i18n/hu.json index 11194850f6..e66cb9ae7f 100644 --- a/server/i18n/hu.json +++ b/server/i18n/hu.json @@ -9019,50 +9019,6 @@ "id": "app.member_count", "translation": "hiba a tagok számának lekérdezésében" }, - { - "id": "Boards", - "translation": "Táblák" - }, - { - "id": "Channels", - "translation": "Csatornák" - }, - { - "id": "Playbooks", - "translation": "Forgatókönyvek" - }, - { - "id": "api.templates.server_inactivity_button", - "translation": "Mattermost megnyitása" - }, - { - "id": "api.templates.server_inactivity_subtitle", - "translation": "Helló {{.Name}}, észrevettük, hogy a Mattermost szervered egy kicsit porosodik. Vess egy pillantást néhány funkcióra, amelyek segíthetnek könnyíteni csapatod munkaterhét." - }, - { - "id": "api.templates.server_inactivity_subject", - "translation": "Nyissa meg a Mattermostot, hogy növelje csapata termelékenységét!" - }, - { - "id": "api.templates.server_inactivity_title", - "translation": "Növelje a termelékenységet ezekkel a fantasztikus funkciókkal" - }, - { - "id": "api.templates.server_inactivity_info_bullet2", - "translation": "Feladatok kezelése a " - }, - { - "id": "api.templates.server_inactivity_info_bullet1", - "translation": "Munkafolyamatok kezelése a " - }, - { - "id": "api.templates.server_inactivity_info_bullet", - "translation": "Vendég hozzáférés a megadott " - }, - { - "id": "api.templates.server_inactivity_info", - "translation": "Jöjjön és nézze meg!" - }, { "id": "app.job.get_all_jobs_by_type_and_status.app_error", "translation": "Nem lehet lekérni az összes munkát típus és státusz szerint." @@ -9163,10 +9119,6 @@ "id": "api.elasticsearch.test_elasticsearch_settings_nil.app_error", "translation": "Az Elasticsearch beállításában nem mentett értékek vannak." }, - { - "id": "api.templates.server_inactivity_footer_disclaimer", - "translation": "Ezt az egyszeri e-mailt azért kapta, mert a Mattermost szervere több mint {{.Hours}} órán keresztül inaktív volt. Ezt az e-mailt a Mattermost szervere automatikusan generálta." - }, { "id": "api.file.cloud_upload.app_error", "translation": "Az mmctl segítségével történő feltöltés egy felhő alapú példányra nem támogatott. Kérjük, tekintse meg a dokumentációt itt: https://docs.mattermost.com/manage/cloud-data-export.html." diff --git a/server/i18n/it.json b/server/i18n/it.json index 14f201c891..0129b143eb 100644 --- a/server/i18n/it.json +++ b/server/i18n/it.json @@ -7359,14 +7359,6 @@ "id": "api.upgrade_to_enterprise.invalid-user.app_error", "translation": " " }, - { - "id": "api.templates.server_inactivity_info_bullet2", - "translation": " " - }, - { - "id": "api.templates.server_inactivity_info_bullet1", - "translation": " " - }, { "id": "api.templates.email_footer_v2", "translation": " " @@ -7479,10 +7471,6 @@ "id": "api.command_share.fetch_remote_status.error", "translation": " " }, - { - "id": "api.templates.server_inactivity_info_bullet", - "translation": " " - }, { "id": "api.templates.invite_team_and_channels_body.title", "translation": " " @@ -7527,10 +7515,6 @@ "id": "api.command_remote.remote_add_remove.help", "translation": " " }, - { - "id": "api.templates.server_inactivity_title", - "translation": " " - }, { "id": "api.templates.invite_team_and_channels_subject", "translation": " " @@ -7863,10 +7847,6 @@ "id": "api.templates.invite_body_guest.subTitle", "translation": " " }, - { - "id": "Playbooks", - "translation": " " - }, { "id": "api.command_custom_status.hint", "translation": " " @@ -8199,10 +8179,6 @@ "id": "api.system.update_notices.clear_failed", "translation": " " }, - { - "id": "api.templates.server_inactivity_info", - "translation": " " - }, { "id": "app.system.complete_onboarding_request.no_first_user", "translation": " " @@ -8211,14 +8187,6 @@ "id": "app.user.get_unread_count.app_error", "translation": " " }, - { - "id": "api.templates.server_inactivity_footer_disclaimer", - "translation": " " - }, - { - "id": "api.templates.server_inactivity_subject", - "translation": " " - }, { "id": "app.team.join_user_to_team.save_member.conflict.app_error", "translation": " " @@ -8419,18 +8387,10 @@ "id": "model.reaction.is_valid.update_at.app_error", "translation": " " }, - { - "id": "Boards", - "translation": " " - }, { "id": "app.notification.footer.info", "translation": " " }, - { - "id": "api.templates.server_inactivity_button", - "translation": " " - }, { "id": "api.command_share.remote_id_invalid.error", "translation": " " @@ -8499,10 +8459,6 @@ "id": "api.post.send_notification_and_forget.push_comment_on_crt_thread_dm", "translation": " " }, - { - "id": "api.templates.server_inactivity_subtitle", - "translation": " " - }, { "id": "import_process.worker.do_job.missing_jsonl", "translation": " " @@ -9231,10 +9187,6 @@ "id": "api.command_custom_status.clear.app_error", "translation": " " }, - { - "id": "Channels", - "translation": " " - }, { "id": "api.admin.add_certificate.parseform.app_error", "translation": " " diff --git a/server/i18n/ja.json b/server/i18n/ja.json index f7b3975155..58aa66d022 100644 --- a/server/i18n/ja.json +++ b/server/i18n/ja.json @@ -8983,18 +8983,6 @@ "id": "api.custom_groups.count_err", "translation": "グループのカウント中にエラーが発生しました" }, - { - "id": "Playbooks", - "translation": "Playbooks" - }, - { - "id": "Channels", - "translation": "チャンネル" - }, - { - "id": "Boards", - "translation": "Boards" - }, { "id": "model.emoji.system_emoji_name.app_error", "translation": "既存のシステム絵文字名と名前が競合しています。" @@ -9019,38 +9007,6 @@ "id": "app.custom_group.unique_name", "translation": "グループ名が重複しています" }, - { - "id": "api.templates.server_inactivity_title", - "translation": "これらの機能による生産性の向上をアンロックする" - }, - { - "id": "api.templates.server_inactivity_subtitle", - "translation": "{{.Name}} さん、あなたのMattermostサーバーが少し埃をかぶっていることに気づきました。チームの作業負荷を軽減するのに役立ついくつかの機能を確認してみてください。" - }, - { - "id": "api.templates.server_inactivity_subject", - "translation": "Mattermostを開いてチームの生産性を向上しましょう!" - }, - { - "id": "api.templates.server_inactivity_info_bullet2", - "translation": "タスク管理のための " - }, - { - "id": "api.templates.server_inactivity_info_bullet1", - "translation": "ワークフロー管理のための " - }, - { - "id": "api.templates.server_inactivity_info_bullet", - "translation": "ゲストアクセス可能な " - }, - { - "id": "api.templates.server_inactivity_info", - "translation": "ぜひご覧ください!" - }, - { - "id": "api.templates.server_inactivity_button", - "translation": "Mattermostを開く" - }, { "id": "api.license_error", "translation": "APIエンドポイントにはライセンスが必要です" @@ -9163,10 +9119,6 @@ "id": "api.elasticsearch.test_elasticsearch_settings_nil.app_error", "translation": "Elasticsearchの設定に未設定の値があります。" }, - { - "id": "api.templates.server_inactivity_footer_disclaimer", - "translation": "あなたの Mattermost サーバが {{.Hours}} 時間以上アクティブでなかったため、このワンタイム電子メールが送信されました。この電子メールは、Mattermost サーバによって自動的に生成されたものです。" - }, { "id": "app.recent_searches.app_error", "translation": "最近の検索履歴を取得する際にエラーが発生しました" @@ -9499,10 +9451,6 @@ "id": "model.group.name.reserved_name.app_error", "translation": "グループ名は予約語として既に登録されています" }, - { - "id": "app.plugin.product_mode.app_error", - "translation": "プラグイン {{.Name}} はプロダクトモードでは有効化できません。" - }, { "id": "app.last_accessible_file.app_error", "translation": "最後にアクセスしたファイルの取得エラー" @@ -9571,10 +9519,6 @@ "id": "app.post_prority.get_for_post.app_error", "translation": "投稿に対する優先度を取得できませんでした" }, - { - "id": "app.draft.update.app_error", - "translation": "下書きを更新できませんでした。" - }, { "id": "app.draft.save.app_error", "translation": "下書きを保存できませんでした。" diff --git a/server/i18n/ko.json b/server/i18n/ko.json index 6e71b18d58..ddd30b9753 100644 --- a/server/i18n/ko.json +++ b/server/i18n/ko.json @@ -7859,10 +7859,6 @@ "id": "bleveengine.delete_post_files.error", "translation": "게시된 파일을 삭제하지 못했습니다." }, - { - "id": "Boards", - "translation": "보드" - }, { "id": "api.elasticsearch.test_elasticsearch_settings_nil.app_error", "translation": "Elasticsearch 설정에 설정되지 않은 값이 있습니다." @@ -7887,14 +7883,6 @@ "id": "api.cloud.notify_admin_to_upgrade_error.already_notified", "translation": "이미 관리자에게 통지됨" }, - { - "id": "Playbooks", - "translation": "플레이북" - }, - { - "id": "Channels", - "translation": "채널" - }, { "id": "api.custom_groups.feature_disabled", "translation": "사용자 정의 그룹 기능은 비활성화되어 있습니다" @@ -8106,5 +8094,337 @@ { "id": "api.config.update_config.translations.app_error", "translation": "서버 번역 업데이트가 실패하였습니다." + }, + { + "id": "api.user.create_user.bad_token_email_data.app_error", + "translation": "토큰의 전자우편 주소가 사용자 데이터의 전자우편 주소와 일치하지 않습니다." + }, + { + "id": "api.user.authorize_oauth_user.saml_response_too_long.app_error", + "translation": "SAML 응답이 너무 깁니다" + }, + { + "id": "api.user.add_user_to_group_syncables.not_ldap_user.app_error", + "translation": "LDAP 사용자가 아님" + }, + { + "id": "api.upload.upload_data.multipart_error", + "translation": "멀티파트 데이터를 처리하지 못했습니다." + }, + { + "id": "api.upload.upload_data.invalid_content_type", + "translation": "멀티파트 업로드에 대한 Content-Type이 잘못되었습니다." + }, + { + "id": "api.upload.upload_data.invalid_content_length", + "translation": "유효하지 않은 Content-Length입니다." + }, + { + "id": "api.upload.get_upload.forbidden.app_error", + "translation": "업로드에 실패했습니다." + }, + { + "id": "api.upload.create.upload_too_large.app_error", + "translation": "파일을 업로드할 수 없습니다. 파일이 너무 큽니다." + }, + { + "id": "api.unable_to_read_file_from_backend", + "translation": "백엔드에서 파일 읽기 오류" + }, + { + "id": "api.templates.welcome_body.subTitle2", + "translation": "아래를 클릭하여 전자우편 주소를 인증하세요." + }, + { + "id": "api.templates.welcome_body.info1", + "translation": "수신자가 당신이 아닌 경우, 이 전자우편은 무시해도 됩니다." + }, + { + "id": "api.templates.verify_body.subTitle2", + "translation": "아래를 클릭하여 전자우편 주소를 인증하세요." + }, + { + "id": "api.templates.verify_body.subTitle1", + "translation": "참여해 주셔서 감사합니다 " + }, + { + "id": "api.templates.verify_body.serverURL", + "translation": "{{ .ServerURL }}." + }, + { + "id": "api.templates.verify_body.info1", + "translation": "수신자가 당신이 아닌 경우, 이 전자우편은 무시해도 됩니다." + }, + { + "id": "api.templates.reset_body.subTitle", + "translation": "비밀번호를 재설정하려면 아래 버튼을 클릭하세요. 요청하지 않은 경우 이 전자우편은 무시해도 됩니다." + }, + { + "id": "api.templates.reset_body.info", + "translation": "비밀번호 재설정 링크는 24시간 후에 만료됩니다." + }, + { + "id": "api.templates.questions_footer.title", + "translation": "질문이 있으신가요?" + }, + { + "id": "api.templates.questions_footer.info", + "translation": "도움이 필요하거나 질문이 있으신가요? 다음 주소로 전자우편을 보내주세요 " + }, + { + "id": "api.templates.payment_failed_no_card.title", + "translation": "Mattermost Cloud 청구서 마감일" + }, + { + "id": "api.templates.payment_failed_no_card.subject", + "translation": "Mattermost Cloud 구독에 대한 결제가 완료되었습니다" + }, + { + "id": "api.templates.payment_failed_no_card.info3", + "translation": "청구서를 검토하고 결제 방법을 추가하려면 지금 결제를 선택합니다." + }, + { + "id": "api.templates.payment_failed_no_card.info1", + "translation": "가장 최근 청구 기간에 대한 Mattermost Cloud 청구서가 처리되었습니다. 하지만 결제 세부 정보가 등록되어 있지 않습니다." + }, + { + "id": "api.templates.payment_failed_no_card.button", + "translation": "지금 결제하기" + }, + { + "id": "api.templates.payment_failed.title", + "translation": "결제가 성공하지 못했습니다" + }, + { + "id": "api.templates.payment_failed.info2", + "translation": "그들은 다음과 같은 이유를 제시했습니다:" + }, + { + "id": "api.templates.license_up_for_renewal_title", + "translation": "Mattermost 구독이 갱신될 예정입니다" + }, + { + "id": "api.templates.license_up_for_renewal_subtitle_two", + "translation": "갱신하려면 고객 계정으로 로그인하세요" + }, + { + "id": "api.templates.license_up_for_renewal_subject", + "translation": "라이선스 갱신 기간 만료" + }, + { + "id": "api.templates.license_up_for_renewal_contact_sales", + "translation": "영업팀에 문의" + }, + { + "id": "api.templates.invite_body_guest.subTitle", + "translation": "팀과의 공동 작업을 위해 게스트로 초대되었습니다" + }, + { + "id": "api.templates.invite_body_footer.info", + "translation": "Mattermost는 안전한 팀 협업을 지원하는 유연한 오픈소스 메시징 플랫폼입니다." + }, + { + "id": "api.templates.email_us_anytime_at", + "translation": "언제든지 다음 주소로 전자우편을 보내주세요 " + }, + { + "id": "api.templates.delinquency_90.title", + "translation": "Mattermost 워크스페이스가 다운그레이드되었습니다" + }, + { + "id": "api.templates.delinquency_90.subtitle2", + "translation": "또한 Cloud Free 제한으로 인해 데이터가 보관 처리되었을 수도 있습니다." + }, + { + "id": "api.templates.delinquency_90.subtitle3", + "translation": "데이터 보관을 해제하고 유료 기능을 계속 사용하려면 결제 정보를 업데이트하세요." + }, + { + "id": "api.templates.delinquency_90.subtitle1", + "translation": "중요한 비즈니스 운영에 Cloud Professional 또는 Enterprise 기능을 사용하는 경우 이러한 기능을 더 이상 사용할 수 없으며 성능이 저하됩니다." + }, + { + "id": "api.templates.delinquency_90.subject", + "translation": "Mattermost Cloud 워크스페이스가 다운그레이드되었습니다" + }, + { + "id": "api.templates.delinquency_90.secondary_action_button", + "translation": "플랜과 가격 보기" + }, + { + "id": "api.templates.delinquency_75.subtitle3", + "translation": "지금 결제 정보를 업데이트하거나 Cloud Free로 다운그레이드하세요." + }, + { + "id": "api.templates.delinquency_75.title", + "translation": "워크스페이스가 15일 후에 다운그레이드됩니다" + }, + { + "id": "api.templates.delinquency_90.button", + "translation": "결제 갱신" + }, + { + "id": "api.templates.delinquency_75.subject", + "translation": "Mattermost {{.Plan}} 플랜이 15일 후에 다운그레이드됩니다" + }, + { + "id": "api.templates.delinquency_75.downgrade_to_free", + "translation": "Cloud Free로 다운그레이드" + }, + { + "id": "api.templates.delinquency_75.button", + "translation": "결제 갱신" + }, + { + "id": "api.templates.delinquency_7.title", + "translation": "결제가 완료되지 않았습니다" + }, + { + "id": "api.templates.delinquency_7.subtitle1", + "translation": "가장 최근 결제를 처리하지 못했습니다." + }, + { + "id": "api.templates.delinquency_7.button", + "translation": "결제 갱신" + }, + { + "id": "api.templates.delinquency_60.title", + "translation": "Mattermost 워크스페이스가 30일 후에 다운그레이드됩니다" + }, + { + "id": "api.templates.delinquency_30.limits_documentation", + "translation": "모든 제한 문서 보기." + }, + { + "id": "api.templates.delinquency_30.button", + "translation": "결제 갱신" + }, + { + "id": "api.templates.delinquency_30.bullet.message_history", + "translation": "메시지 역사" + }, + { + "id": "api.templates.delinquency_30.bullet.files", + "translation": "파일" + }, + { + "id": "api.server.warn_metric.number_of_posts_2M.notification_title", + "translation": "성능 향상" + }, + { + "id": "api.server.warn_metric.number_of_teams_5.notification_title", + "translation": "고급 권한 사용 중" + }, + { + "id": "api.server.warn_metric.number_of_teams_5.start_trial_notification_success.message", + "translation": "엔터프라이즈 체험판이 활성화되었습니다. **시스템 콘솔 > 사용자 관리 > 권한** 에서 고급 권한 설정을 활성화하세요." + }, + { + "id": "api.system.logs.invalidFilter", + "translation": "유효하지 않은 로그 필터" + }, + { + "id": "api.team.add_team_member.invalid_body.app_error", + "translation": "요청 본문을 구문 분석할 수 없습니다." + }, + { + "id": "api.team.import_team.unknown_import_from.app_error", + "translation": "알 수 없는 들여오기 원본입니다." + }, + { + "id": "api.team.invite_guests_to_channels.disabled.error", + "translation": "게스트 계정이 비활성화되었습니다" + }, + { + "id": "api.team.invite_guests_to_channels.invalid_body.app_error", + "translation": "요청 본문이 유효하지 않거나 누락되었습니다." + }, + { + "id": "api.team.invite_guests_to_channels.license.error", + "translation": "게스트 계정을 지원하지 않는 라이선스입니다" + }, + { + "id": "api.team.invite_members.unable_to_send_email.app_error", + "translation": "전자우편을 보내는 중에 오류가 발생했습니다" + }, + { + "id": "api.team.invite_members.unable_to_send_email_with_defaults.app_error", + "translation": "시스템 콘솔에서 SMTP가 설정되지 않았습니다" + }, + { + "id": "api.team.invite_members_to_team_and_channels.invalid_body.app_error", + "translation": "유효하지 않은 요청 본문입니다." + }, + { + "id": "api.team.invite_members_to_team_and_channels.invalid_body_parsing.app_error", + "translation": "본문 데이터를 구문 분석하는 동안 오류가 발생했습니다." + }, + { + "id": "api.team.set_team_icon.check_image_limits.app_error", + "translation": "이미지 제한 확인에 실패했습니다. 해상도가 너무 높습니다." + }, + { + "id": "api.templates.cloud_upgrade_confirmation.subject", + "translation": "Mattermots 업그레이드 확인" + }, + { + "id": "api.templates.cloud_upgrade_confirmation.title", + "translation": "업그레이드 되었습니다!" + }, + { + "id": "api.templates.cloud_upgrade_confirmation_monthly.subtitle", + "translation": "{{.WorkspaceName}} 워크스페이스가 업그레이드 되었습니다. {{.Data}}에 결제가 됩니다" + }, + { + "id": "api.templates.cloud_upgrade_confirmation_yearly.subtitle", + "translation": "{{.WorkspaceName}} 워크스페이스가 업그레이드 되었습니다." + }, + { + "id": "api.templates.cloud_welcome_email.add_apps_info", + "translation": "워크스페이스에 앱들을 추가합니다" + }, + { + "id": "api.templates.cloud_welcome_email.app_market_place", + "translation": "앱 마켓플레이스." + }, + { + "id": "api.templates.cloud_welcome_email.button", + "translation": "매터모스트 열기" + }, + { + "id": "api.license.request-trial.can-start-trial.error", + "translation": "평가판을 시작할 수 있는지 확인할 수 없습니다" + }, + { + "id": "api.file.test_connection_s3_settings_nil.app_error", + "translation": "파일 저장소 설정에 설정되지 않은 값이 있습니다." + }, + { + "id": "api.error_set_first_admin_visit_marketplace_status", + "translation": "스토어에 초기 관리자 마켓플레이스 방문 상태를 저장하는 동안 오류가 발생했습니다." + }, + { + "id": "api.command_templates.unsupported.app_error", + "translation": "이 장치에서는 템플릿 명령이 지원되지 않습니다." + }, + { + "id": "api.command_templates.name", + "translation": "템플릿" + }, + { + "id": "api.command_templates.desc", + "translation": "템플릿에서 만들기 창을 엽니다" + }, + { + "id": "api.acknowledgement.save.archived_channel.app_error", + "translation": "보관된 채널에서는 승인할 수 없습니다." + }, + { + "id": "api.acknowledgement.delete.deadline.app_error", + "translation": "5분이 경과한 후에는 확인을 삭제할 수 없습니다." + }, + { + "id": "api.acknowledgement.delete.archived_channel.app_error", + "translation": "보관된 채널에서는 승인 내용을 삭제할 수 없습니다." } ] diff --git a/server/i18n/nl.json b/server/i18n/nl.json index 85fa7448a6..95097c7095 100644 --- a/server/i18n/nl.json +++ b/server/i18n/nl.json @@ -9026,50 +9026,6 @@ "id": "app.job.get_all_jobs_by_type_and_status.app_error", "translation": "Het lukt niet om alle jobs op type en status op te halen." }, - { - "id": "api.templates.server_inactivity_title", - "translation": "Verhoog jouw productiviteit met deze geweldige functies" - }, - { - "id": "api.templates.server_inactivity_subtitle", - "translation": "Hey {{.Naam}}, we hebben gemerkt dat jouw Mattermost server een beetje stof aan het verzamelen is. Kijk eens naar enkele functies die kunnen helpen om de werklast van jouw team te verlichten." - }, - { - "id": "api.templates.server_inactivity_subject", - "translation": "Open Mattermost om de productiviteit van je team te verhogen!" - }, - { - "id": "api.templates.server_inactivity_info_bullet2", - "translation": "Beheer taken met " - }, - { - "id": "api.templates.server_inactivity_info_bullet1", - "translation": "Workflowbeheer met " - }, - { - "id": "api.templates.server_inactivity_info_bullet", - "translation": "Gast-toegang tot gespecificeerd(e) " - }, - { - "id": "api.templates.server_inactivity_info", - "translation": "Kom maar eens kijken!" - }, - { - "id": "api.templates.server_inactivity_button", - "translation": "Mattermost openen" - }, - { - "id": "Playbooks", - "translation": "Playbooks" - }, - { - "id": "Channels", - "translation": "Kanalen" - }, - { - "id": "Boards", - "translation": "Boards" - }, { "id": "api.team.invite_members.unable_to_send_email_with_defaults.app_error", "translation": "SMTP is niet geconfigureerd in Systeem Console" @@ -9162,10 +9118,6 @@ "id": "api.elasticsearch.test_elasticsearch_settings_nil.app_error", "translation": "De instellingen van Elasticsearch bevat niet-ingestelde waarden." }, - { - "id": "api.templates.server_inactivity_footer_disclaimer", - "translation": "Je hebt deze eenmalige email ontvangen omdat jouw Mattermost server inactief was voor meer dan {{.Hours}} uur. Deze e-mail werd automatisch aangemaakt door jouw Mattermost server." - }, { "id": "api.templates.cloud_upgrade_confirmation.title", "translation": "Je bent nu geüpgraded!" @@ -9502,10 +9454,6 @@ "id": "model.group.name.reserved_name.app_error", "translation": "groepsnaam bestaat al als een gereserveerde naam" }, - { - "id": "app.plugin.product_mode.app_error", - "translation": "Plugin {{.Name}} kan niet worden ingeschakeld in de productmodus." - }, { "id": "app.last_accessible_file.app_error", "translation": "Fout bij het ophalen van het laatst toegankelijke bestand" @@ -9614,10 +9562,6 @@ "id": "app.post_prority.get_for_post.app_error", "translation": "Kon geen berichtprioriteit ophalen voor bericht" }, - { - "id": "app.draft.update.app_error", - "translation": "Kan het concept niet bijwerken." - }, { "id": "app.draft.save.app_error", "translation": "Kan het concept niet opslaan." diff --git a/server/i18n/pl.json b/server/i18n/pl.json index b123990e90..339c00b48b 100644 --- a/server/i18n/pl.json +++ b/server/i18n/pl.json @@ -9023,50 +9023,6 @@ "id": "api.custom_groups.count_err", "translation": "błąd liczenia grup" }, - { - "id": "api.templates.server_inactivity_title", - "translation": "Odblokuj zwiększoną produktywność dzięki tym wspaniałym funkcjom" - }, - { - "id": "api.templates.server_inactivity_subtitle", - "translation": "Hej {{.Name}}, zauważyliśmy, że twój serwer Mattermost zbiera trochę kurzu. Zapoznaj się z kilkoma funkcjami, które pomogą odciążyć Twój zespół." - }, - { - "id": "api.templates.server_inactivity_subject", - "translation": "Otwórz Mattermost, aby zwiększyć produktywność swojego zespołu!" - }, - { - "id": "api.templates.server_inactivity_info_bullet2", - "translation": "Zarządzaj zadaniami za pomocą " - }, - { - "id": "api.templates.server_inactivity_info_bullet1", - "translation": "Zarządzanie przepływem pracy z " - }, - { - "id": "api.templates.server_inactivity_info_bullet", - "translation": "Dostęp gości do określenia " - }, - { - "id": "api.templates.server_inactivity_info", - "translation": "Przyjdź i sprawdź to!" - }, - { - "id": "api.templates.server_inactivity_button", - "translation": "Otwórz Mattermost" - }, - { - "id": "Playbooks", - "translation": "Playbooki" - }, - { - "id": "Channels", - "translation": "Kanały" - }, - { - "id": "Boards", - "translation": "Tablice" - }, { "id": "model.oauth.is_valid.mattermost_app_id.app_error", "translation": "Maksymalna długość identyfikatora MattermostAppID wynosi 32 znaki." @@ -9171,10 +9127,6 @@ "id": "api.elasticsearch.test_elasticsearch_settings_nil.app_error", "translation": "Ustawienia Elasticsearch mają nieustawione wartości." }, - { - "id": "api.templates.server_inactivity_footer_disclaimer", - "translation": "Otrzymałeś tę jednorazową wiadomość e-mail, ponieważ Twój serwer Mattermost był nieaktywny przez ponad {{.Hours}} godzin. Ta wiadomość e-mail została automatycznie wygenerowana przez serwer Mattermost." - }, { "id": "api.file.cloud_upload.app_error", "translation": "Przesyłanie danych do instancji Chmury za pomocą mmctl nie jest obsługiwane. Proszę sprawdzić dokumentację tutaj: https://docs.mattermost.com/manage/cloud-data-export.html." @@ -9511,10 +9463,6 @@ "id": "model.group.name.reserved_name.app_error", "translation": "nazwa grupy już istnieje jako nazwa zastrzeżona" }, - { - "id": "app.plugin.product_mode.app_error", - "translation": "Wtyczka {{.Name}} nie może być włączona w trybie produktu." - }, { "id": "api.team.invite_guests_to_channels.license.error", "translation": "Twoja licencja nie wspiera kont gości" @@ -9591,10 +9539,6 @@ "id": "app.post_prority.get_for_post.app_error", "translation": "Nie można uzyskać priorytetu dla posta" }, - { - "id": "app.draft.update.app_error", - "translation": "Nie można zaktualizować szkicu." - }, { "id": "app.draft.save.app_error", "translation": "Nie można zapisać Szkicu." @@ -9914,5 +9858,197 @@ { "id": "app.command.execute.error", "translation": "Nie można wykonać polecenia." + }, + { + "id": "app.user.digest.runs_in_progress.zero_in_progress", + "translation": "Obecnie masz 0 uruchomień w trakcie." + }, + { + "id": "app.user.digest.tasks.all_tasks_command", + "translation": "Użyj `/playbook todo`, aby zobaczyć wszystkie swoje zadania." + }, + { + "id": "app.user.digest.tasks.heading", + "translation": "Twoje przydzielone zadania" + }, + { + "id": "app.user.digest.tasks.zero_assigned", + "translation": "Masz 0 przydzielonych zadań." + }, + { + "id": "app.user.new_run.run_name", + "translation": "Nazwa uruchomienia" + }, + { + "id": "app.user.new_run.title", + "translation": "Uruchom playbook" + }, + { + "id": "app.user.run.add_checklist_item.description", + "translation": "Opis" + }, + { + "id": "app.user.run.add_checklist_item.name", + "translation": "Nazwa" + }, + { + "id": "app.user.run.add_checklist_item.submit_label", + "translation": "Dodaj zadanie" + }, + { + "id": "app.user.run.add_checklist_item.title", + "translation": "Dodaj nowe zadanie" + }, + { + "id": "app.user.run.add_to_timeline.playbook_run", + "translation": "Uruchomienie Playbooka" + }, + { + "id": "app.user.run.add_to_timeline.submit_label", + "translation": "Dodaj do osi czasu uruchomienia" + }, + { + "id": "app.user.run.add_to_timeline.summary", + "translation": "Podsumowanie" + }, + { + "id": "app.user.run.add_to_timeline.summary.help", + "translation": "Maksymalnie 64 znaki" + }, + { + "id": "app.user.run.add_to_timeline.summary.placeholder", + "translation": "Krótkie podsumowanie widoczne na osi czasu" + }, + { + "id": "app.user.run.update_status.finish_run", + "translation": "Zakończ uruchomienie" + }, + { + "id": "app.user.run.update_status.finish_run.placeholder", + "translation": "Oznacz również uruchomienie jako zakończone" + }, + { + "id": "app.user.run.update_status.reminder_for_next_update", + "translation": "Przypomnienie o następnej aktualizacji" + }, + { + "id": "app.user.run.update_status.submit_label", + "translation": "Status aktualizacji" + }, + { + "id": "app.user.digest.runs_in_progress.num_in_progress", + "translation": { + "few": "Masz {{.Count}} uruchomienia w toku:", + "many": "Masz {{.Count}} uruchomień w toku:", + "one": "Masz {{.Count}} uruchomienie w toku:" + } + }, + { + "id": "app.user.digest.tasks.due_in_x_days", + "translation": { + "few": "Termin płatności za {{.Count}} dni", + "many": "Termin płatności za {{.Count}} dni", + "one": "Termin płatności za {{.Count}} dzień" + } + }, + { + "id": "app.user.digest.tasks.due_after_today", + "translation": { + "few": "Masz **{{.Count}} przydzielone zadania, których termin wykonania wypada po dzisiejszym dniu**.", + "many": "Masz **{{.Count}} przydzielonych zadań, których termin wykonania wypada po dzisiejszym dniu**.", + "one": "Masz **{{.Count}} przydzielone zadanie, którego termin wykonania wypada po dzisiejszym dniu**." + } + }, + { + "id": "app.user.digest.tasks.due_today", + "translation": "Do zapłaty dzisiaj" + }, + { + "id": "app.user.digest.tasks.due_x_days_ago", + "translation": "Termin {{.Count}} dni temu" + }, + { + "id": "app.user.digest.tasks.num_assigned", + "translation": { + "few": "Masz {{.Count}} przydzielone zadania:", + "many": "Masz {{.Count}} przydzielonych zadań:", + "one": "Masz {{.Count}} przydzielone zadanie:" + } + }, + { + "id": "app.user.digest.tasks.num_assigned_due_until_today", + "translation": { + "few": "Masz {{.Count}} przydzielone zadania, których termin wykonania właśnie upływa:", + "many": "Masz {{.Count}} przydzielonych zadań, których termin wykonania właśnie upływa:", + "one": "Masz {{.Count}} przydzielone zadanie, którego termin wykonania właśnie upływa:" + } + }, + { + "id": "app.user.new_run.intro", + "translation": "**Właściciel** {{.Username}}" + }, + { + "id": "app.user.new_run.playbook", + "translation": "Playbook" + }, + { + "id": "app.user.new_run.submit_label", + "translation": "Rozpocznij uruchomienie" + }, + { + "id": "app.user.run.confirm_finish.num_outstanding", + "translation": { + "few": "Są **{.Count}} zaległe zadania**. Czy na pewno chcesz zakończyć uruchomienie *{.RunName}}* dla wszystkich uczestników?", + "many": "Jest **{.Count}} zaległych zadań**. Czy na pewno chcesz zakończyć uruchomienie *{.RunName}}* dla wszystkich uczestników?", + "one": "Jest **{.Count}} zaległe zadanie**. Czy na pewno chcesz zakończyć uruchomienie *{.RunName}}* dla wszystkich uczestników?" + } + }, + { + "id": "app.user.run.update_status.num_channel", + "translation": { + "few": "Przedstaw aktualizację dla interesariuszy. Ten post będzie transmitowany na {{.Count}} kanałach.", + "many": "Przedstaw aktualizację dla interesariuszy. Ten post będzie transmitowany na {{.Count}} kanałach.", + "one": "Przedstaw aktualizację dla interesariuszy. Ten post będzie transmitowany na {{.Count}} kanale." + } + }, + { + "id": "app.user.run.update_status.title", + "translation": "Aktualizacje statusu" + }, + { + "id": "app.user.digest.tasks.due_yesterday", + "translation": "Termin na wczoraj" + }, + { + "id": "app.user.run.status_disable", + "translation": "@{.Username}} wyłączył aktualizacje statusu dla [{{.RunName}}]({{.RunURL}})" + }, + { + "id": "app.user.run.status_enable", + "translation": "@{.Username}} włączył aktualizacje statusu dla [{{.RunName}}]({{.RunURL}})" + }, + { + "id": "app.user.run.update_status.change_since_last_update", + "translation": "Zmiana od ostatniej aktualizacji" + }, + { + "id": "app.user.run.confirm_finish.submit_label", + "translation": "Zakończ uruchomienie" + }, + { + "id": "app.user.run.confirm_finish.title", + "translation": "Potwierdź zakończenie uruchomienia" + }, + { + "id": "app.user.run.request_join_channel", + "translation": "@{{.Name}} jest uczestnikiem uruchomienia i chce dołączyć do tego kanału. Każdy członek kanału może go zaprosić.\n" + }, + { + "id": "app.user.run.request_update", + "translation": "@here - @{.Name}} zażądał aktualizacji statusu dla [{{.RunName}}]({{.RunURL}}). \n" + }, + { + "id": "app.user.run.add_to_timeline.title", + "translation": "Dodaj do osi czasu uruchomienia" } ] diff --git a/server/i18n/pt-BR.json b/server/i18n/pt-BR.json index 63dcbcc1ce..48f679a2be 100644 --- a/server/i18n/pt-BR.json +++ b/server/i18n/pt-BR.json @@ -8619,18 +8619,6 @@ "id": "api.cloud.notify_admin_to_upgrade_error.already_notified", "translation": "Administrador já notificado" }, - { - "id": "Channels", - "translation": "Canais" - }, - { - "id": "Boards", - "translation": "Quadros" - }, - { - "id": "Playbooks", - "translation": "Playbooks" - }, { "id": "api.cloud.delinquency_email.missing_email_to_trigger", "translation": "Campos faltando para envio de email." diff --git a/server/i18n/ru.json b/server/i18n/ru.json index bc21184315..452811806c 100644 --- a/server/i18n/ru.json +++ b/server/i18n/ru.json @@ -8967,18 +8967,6 @@ "id": "api.custom_groups.feature_disabled", "translation": "функция пользовательских групп отключена" }, - { - "id": "Playbooks", - "translation": "Сценарии" - }, - { - "id": "Channels", - "translation": "Каналы" - }, - { - "id": "Boards", - "translation": "Доски" - }, { "id": "api.error_get_first_admin_complete_setup", "translation": "Ошибка при попытке получить первую завершенную настройку администратора из магазина." @@ -9035,34 +9023,6 @@ "id": "api.user.authorize_oauth_user.saml_response_too_long.app_error", "translation": "Ответ SAML слишком длинный" }, - { - "id": "api.templates.server_inactivity_title", - "translation": "Откройте для себя повышенную производительность с помощью этих замечательных функций" - }, - { - "id": "api.templates.server_inactivity_subtitle", - "translation": "Привет, {{.Name}}, мы заметили, что Ваш сервер Mattermost собирает немного пыли. Взгляните на некоторые функции, которые могут облегчить рабочую нагрузку в Вашей команде." - }, - { - "id": "api.templates.server_inactivity_subject", - "translation": "Откройте все возможности Mattermost, чтобы повысить производительность вашей команды!" - }, - { - "id": "api.templates.server_inactivity_info_bullet2", - "translation": "Управляйте задачами с помощью " - }, - { - "id": "api.templates.server_inactivity_info_bullet1", - "translation": "Управление рабочим процессом с " - }, - { - "id": "api.templates.server_inactivity_info", - "translation": "Приходите и проверьте это!" - }, - { - "id": "api.templates.server_inactivity_button", - "translation": "Открыть Mattermost" - }, { "id": "api.team.invite_members.unable_to_send_email_with_defaults.app_error", "translation": "SMTP не настроен в Системной Консоли" @@ -9079,10 +9039,6 @@ "id": "api.error_set_first_admin_complete_setup", "translation": "Ошибка при попытке сохранить первую полную настройку администратора в магазине." }, - { - "id": "api.templates.server_inactivity_info_bullet", - "translation": "Гостевой доступ к указанному " - }, { "id": "model.oauth.is_valid.mattermost_app_id.app_error", "translation": "Максимальная длина MattermostAppID — 32 символа." @@ -9307,10 +9263,6 @@ "id": "app.post.analytics_teams_count.app_error", "translation": "Не удалось получить сведения об использовании команд" }, - { - "id": "app.plugin.product_mode.app_error", - "translation": "Плагин {{.Name}} не может быть включен в продуктовом режиме." - }, { "id": "app.notify_admin.send_notification_post.app_error", "translation": "Невозможно отправить сообщение с уведомлением." @@ -9371,10 +9323,6 @@ "id": "app.cloud.get_cloud_products.app_error", "translation": "Не удалось получить облачные продукты" }, - { - "id": "api.templates.server_inactivity_footer_disclaimer", - "translation": "Вы получили это одноразовое письмо, потому что ваш сервер Mattermost был неактивен более {{.Hours}} часов. Это письмо было автоматически сгенерировано вашим сервером Mattermost." - }, { "id": "api.templates.delinquency_90.title", "translation": "Ваше рабочее пространство Mattermost было понижено в статусе" @@ -9591,10 +9539,6 @@ "id": "app.post_prority.get_for_post.app_error", "translation": "Невозможно получить приоритет для сообщения" }, - { - "id": "app.draft.update.app_error", - "translation": "Невозможно обновить черновик." - }, { "id": "app.draft.save.app_error", "translation": "Невозможно сохранить черновик." diff --git a/server/i18n/sv.json b/server/i18n/sv.json index 2b8f6eced0..f63afba27a 100644 --- a/server/i18n/sv.json +++ b/server/i18n/sv.json @@ -8970,18 +8970,6 @@ "id": "app.system.complete_onboarding_request.app_error", "translation": "Misslyckades att tolka onboarding-anropet." }, - { - "id": "Playbooks", - "translation": "Playbooks" - }, - { - "id": "Channels", - "translation": "Kanaler" - }, - { - "id": "Boards", - "translation": "Boards" - }, { "id": "api.custom_groups.no_remote_id", "translation": "remote_id måste vara tomt för anpassade grupper" @@ -9070,42 +9058,6 @@ "id": "app.channel.get_file_count.app_error", "translation": "Det går inte att få fram antalet filer i kanalen" }, - { - "id": "api.templates.server_inactivity_title", - "translation": "Öka produktiviteten med dessa fantastiska funktioner" - }, - { - "id": "api.templates.server_inactivity_subtitle", - "translation": "Hej {{.Name}}, vi ser att din Mattermost-server samlat lite damm. Ta en titt på några funktioner som kan hjälpa till att lätta på arbetsbördan för ditt team." - }, - { - "id": "api.templates.server_inactivity_subject", - "translation": "Öppna Mattermost för att öka teamets produktivitet!" - }, - { - "id": "api.templates.server_inactivity_info_bullet2", - "translation": "Hantera uppgifter med hjälp av " - }, - { - "id": "api.templates.server_inactivity_info_bullet1", - "translation": "Hantera arbetsflöden med " - }, - { - "id": "api.templates.server_inactivity_info_bullet", - "translation": "Gäståtkomst till specificerade " - }, - { - "id": "api.templates.server_inactivity_info", - "translation": "Kom och kolla in det!" - }, - { - "id": "api.templates.server_inactivity_footer_disclaimer", - "translation": "Du fick det här engångsmejlet eftersom din Mattermost-server har varit inaktiv i mer än {{.Hours}} timmar. Det här e-postmeddelandet genererades automatiskt av din Mattermost-server." - }, - { - "id": "api.templates.server_inactivity_button", - "translation": "Öppna Mattermost" - }, { "id": "api.templates.invite_team_and_channels_subject", "translation": "[{{ .SiteName }}] {{ .SenderName }} bjöd in dig att ansluta till {{ .ChannelsLen }} kanaler i teamet {{ .TeamDisplayName }}" @@ -9522,10 +9474,6 @@ "id": "model.insights.get_start_of_day_for_time_range.time_range.app_error", "translation": "Ogiltigt tidsintervall." }, - { - "id": "app.plugin.product_mode.app_error", - "translation": "Plugin {{.Name}} kan inte aktiveras i produktionsläge." - }, { "id": "app.collection.add_collection.exists.app_error", "translation": "Samlingstypen finns redan." @@ -9638,10 +9586,6 @@ "id": "app.post_prority.get_for_post.app_error", "translation": "Det går inte att få fram inläggets prioritet" }, - { - "id": "app.draft.update.app_error", - "translation": "Kunde inte uppdatera utkastet." - }, { "id": "app.draft.save.app_error", "translation": "Kunde inte spara utkastet." @@ -9889,5 +9833,213 @@ { "id": "app.oauth.remove_auth_data_by_client_id.app_error", "translation": "Kunde inte rensa oauth-information." + }, + { + "id": "app.user.run.update_status.title", + "translation": "Statusuppdatering" + }, + { + "id": "app.user.run.update_status.submit_label", + "translation": "Uppdatera status" + }, + { + "id": "app.user.run.update_status.reminder_for_next_update", + "translation": "Påminnelse om nästa uppdatering" + }, + { + "id": "app.user.run.update_status.num_channel", + "translation": { + "one": "Ge en uppdatering till intressenterna. Detta inlägg kommer att publiceras i {{.Count}} kanal.", + "other": "Ge en uppdatering till intressenterna. Detta inlägg kommer att publiceras i {{.Count}} kanaler." + } + }, + { + "id": "app.user.run.update_status.finish_run.placeholder", + "translation": "Markera även körningen som avslutad" + }, + { + "id": "app.user.run.update_status.finish_run", + "translation": "Slutför körningen" + }, + { + "id": "app.user.run.update_status.change_since_last_update", + "translation": "Förändring sedan den senaste uppdateringen" + }, + { + "id": "app.user.run.status_enable", + "translation": "@{{.Username}} aktiverade statusuppdateringar för [{{.RunName}}]({{.RunURL}})" + }, + { + "id": "app.user.run.status_disable", + "translation": "@{{.Username}} inaktiverade statusuppdateringarna för [{{.RunName}}]({{.RunURL}})" + }, + { + "id": "app.user.run.request_update", + "translation": "@here - @{{.Name}} begärde en statusuppdatering för [{{.RunName}}]({{.RunURL}}). \n" + }, + { + "id": "app.user.run.request_join_channel", + "translation": "@{{.Name}} är en deltagare i en körning och vill gå med i den här kanalen. Alla medlemmar i kanalen kan bjuda in dem.\n" + }, + { + "id": "app.user.run.confirm_finish.title", + "translation": "Bekräfta att avsluta körningen" + }, + { + "id": "app.user.run.confirm_finish.submit_label", + "translation": "Slutför körningen" + }, + { + "id": "app.user.run.confirm_finish.num_outstanding", + "translation": { + "one": "Det finns **{{.Count}} utestående uppgift**. Är du säker på att du vill avsluta körningen *{{.RunName}}* för alla deltagare?", + "other": "Det finns **{{.Count}} utestående uppgifter**. Är du säker på att du vill avsluta körningen *{{.RunName}}* för alla deltagare?" + } + }, + { + "id": "app.user.run.add_to_timeline.title", + "translation": "Lägg till i tidslinjen för körning" + }, + { + "id": "app.user.run.add_to_timeline.summary.placeholder", + "translation": "Kort sammanfattning som visas i tidslinjen" + }, + { + "id": "app.user.run.add_to_timeline.summary.help", + "translation": "Max 64 tecken" + }, + { + "id": "app.user.run.add_to_timeline.summary", + "translation": "Sammanfattning" + }, + { + "id": "app.user.run.add_to_timeline.submit_label", + "translation": "Lägg till i tidslinjen för körning" + }, + { + "id": "app.user.run.add_to_timeline.playbook_run", + "translation": "Kör Playbook" + }, + { + "id": "app.user.run.add_checklist_item.title", + "translation": "Lägg till en ny uppgift" + }, + { + "id": "app.user.run.add_checklist_item.submit_label", + "translation": "Lägg till en uppgift" + }, + { + "id": "app.user.run.add_checklist_item.name", + "translation": "Namn" + }, + { + "id": "app.user.run.add_checklist_item.description", + "translation": "Beskrivning" + }, + { + "id": "app.user.new_run.title", + "translation": "Kör playbook" + }, + { + "id": "app.user.new_run.submit_label", + "translation": "Starta körning" + }, + { + "id": "app.user.new_run.run_name", + "translation": "Namn på körning" + }, + { + "id": "app.user.new_run.playbook", + "translation": "Playbook" + }, + { + "id": "app.user.new_run.intro", + "translation": "**Ägare** {{.Username}}" + }, + { + "id": "app.user.digest.tasks.zero_assigned", + "translation": "Du har 0 tilldelade uppgifter." + }, + { + "id": "app.user.digest.tasks.num_assigned_due_until_today", + "translation": { + "one": "Du har {{.Count}} tilldelad uppgift som nu är förfallen:", + "other": "Du har {{.Count}} tilldelade uppgifter som nu är förfallna:" + } + }, + { + "id": "app.user.digest.tasks.num_assigned", + "translation": { + "one": "Du har {{.Count}} tilldelad uppgift:", + "other": "Du har {{.Count}} tilldelade uppgifter:" + } + }, + { + "id": "app.user.digest.tasks.heading", + "translation": "Dina tilldelade uppgifter" + }, + { + "id": "app.user.digest.tasks.due_yesterday", + "translation": "Skulle utförts igår" + }, + { + "id": "app.user.digest.tasks.due_x_days_ago", + "translation": "Försenad {{.Count}} dagar" + }, + { + "id": "app.user.digest.tasks.due_in_x_days", + "translation": { + "one": "Ska utföras inom {{.Count}} dag", + "other": "Ska utföras inom {{.Count}} dagar" + } + }, + { + "id": "app.user.digest.tasks.due_today", + "translation": "Ska utföras idag" + }, + { + "id": "app.user.digest.tasks.due_after_today", + "translation": { + "one": "Du har **{{.Count}} tilldelad uppgift som ska utföras idag**.", + "other": "Du har **{{.Count}} tilldelade uppgifter som ska utföras idag**." + } + }, + { + "id": "app.user.digest.tasks.all_tasks_command", + "translation": "Använd `/playbook todo` för att se alla dina uppgifter." + }, + { + "id": "app.user.digest.runs_in_progress.zero_in_progress", + "translation": "Du har 0 pågående körningar." + }, + { + "id": "app.user.digest.runs_in_progress.num_in_progress", + "translation": { + "one": "Du har {{.Count}} körning som för närvarande pågår:", + "other": "Du har {{.Count}} körningar som för närvarande pågår:" + } + }, + { + "id": "app.user.digest.runs_in_progress.heading", + "translation": "Körningar som pågår" + }, + { + "id": "app.user.digest.overdue_status_updates.zero_overdue", + "translation": "Du har 0 försenade körningar." + }, + { + "id": "app.user.digest.overdue_status_updates.num_overdue", + "translation": { + "one": "Du har {{.Count}} körning som borde ha fått en statusuppdatering:", + "other": "Du har {{.Count}} körningar som borde ha fått en statusuppdatering:" + } + }, + { + "id": "app.user.digest.overdue_status_updates.heading", + "translation": "Status om förseningar" + }, + { + "id": "app.command.execute.error", + "translation": "Kunde inte utföra kommandot." } ] diff --git a/server/i18n/tr.json b/server/i18n/tr.json index 1c9448505c..218846ffc4 100644 --- a/server/i18n/tr.json +++ b/server/i18n/tr.json @@ -9022,50 +9022,6 @@ "id": "api.custom_groups.count_err", "translation": "gruplar sayılırken sorun çıktı" }, - { - "id": "api.templates.server_inactivity_title", - "translation": "Bu harika özelliklerle üretkenliğinizi artırın" - }, - { - "id": "api.templates.server_inactivity_subject", - "translation": "Ekibinizin üretkenliğini arttırmak için Mattermost açın!" - }, - { - "id": "api.templates.server_inactivity_subtitle", - "translation": "Merhaba {{.Name}}, Mattermost sunucunuzun biraz toz tuttuğunu fark ettik. Ekibinizin iş yükünü hafifletmeye yardımcı olabilecek bazı özelliklere göz atın." - }, - { - "id": "api.templates.server_inactivity_info_bullet", - "translation": "Belirtilen konuk erişimi " - }, - { - "id": "api.templates.server_inactivity_info_bullet2", - "translation": "Görev yönetimi " - }, - { - "id": "api.templates.server_inactivity_info_bullet1", - "translation": "İş akışı yönetimi " - }, - { - "id": "api.templates.server_inactivity_info", - "translation": "Gelin ve inceleyin!" - }, - { - "id": "api.templates.server_inactivity_button", - "translation": "Mattermost uygulamasını aç" - }, - { - "id": "Playbooks", - "translation": "Senaryolar" - }, - { - "id": "Channels", - "translation": "Kanallar" - }, - { - "id": "Boards", - "translation": "Panolar" - }, { "id": "app.job.get_all_jobs_by_type_and_status.app_error", "translation": "Türe ve duruma göre tüm görevler alınamadı." @@ -9166,10 +9122,6 @@ "id": "api.templates.cloud_upgrade_confirmation.subject", "translation": "Mattermost üst tarifeye geçme onayı" }, - { - "id": "api.templates.server_inactivity_footer_disclaimer", - "translation": "Bu bir kerelik e-posta Mattermost sunucunuz {{.Hours}} saatten uzun süredir etkin olmadığı için gönderildi. Bu e-posta Mattermost sunucunuz tarafından otomatik olarak oluşturuldu." - }, { "id": "api.elasticsearch.test_elasticsearch_settings_nil.app_error", "translation": "Elasticsearch yapılandırmasında ayarlanmamış değerler var." @@ -9510,10 +9462,6 @@ "id": "model.group.name.reserved_name.app_error", "translation": "aynı adlı bir grup zaten sistem kullanımına ayrılmış bir ad olarak var" }, - { - "id": "app.plugin.product_mode.app_error", - "translation": "{{.Name}} uygulama eki ürün kipinde etkinleştirilemez." - }, { "id": "api.team.invite_guests_to_channels.license.error", "translation": "Lisansınız konuk hesaplarının kullanılmasını desteklemiyor" @@ -9618,10 +9566,6 @@ "id": "app.post_prority.get_for_post.app_error", "translation": "İletinin önceliği alınamadı" }, - { - "id": "app.draft.update.app_error", - "translation": "Taslak güncellenemedi." - }, { "id": "app.draft.get_drafts.app_error", "translation": "Kullanıcının taslakları alınamadı." diff --git a/server/i18n/uk.json b/server/i18n/uk.json index bce734f746..b63779f26f 100644 --- a/server/i18n/uk.json +++ b/server/i18n/uk.json @@ -6847,10 +6847,6 @@ "id": "api.back_to_app", "translation": "Повернутися до {{.SiteName}}" }, - { - "id": "Channels", - "translation": "Канали" - }, { "id": "api.cloud.cws_webhook_event_missing_error", "translation": "Подія Webhook не оброблена. Або вона відсутня, або недійсна." @@ -6879,14 +6875,6 @@ "id": "api.acknowledgement.delete.archived_channel.app_error", "translation": "Ви не можете видалити підтвердження в заархівованому каналі." }, - { - "id": "Playbooks", - "translation": "Сценарії" - }, - { - "id": "Boards", - "translation": "Дошки" - }, { "id": "api.command_remote.invite.help", "translation": "Запросіть безпечне з'єднання" diff --git a/server/i18n/zh-CN.json b/server/i18n/zh-CN.json index 11f9a677c4..bbf95c2889 100644 --- a/server/i18n/zh-CN.json +++ b/server/i18n/zh-CN.json @@ -8999,38 +8999,6 @@ "id": "api.user.authorize_oauth_user.saml_response_too_long.app_error", "translation": "SAML的请求信息太长" }, - { - "id": "api.templates.server_inactivity_title", - "translation": "解锁这些令人敬佩的、提高生产力的功能" - }, - { - "id": "api.templates.server_inactivity_subtitle", - "translation": "嗨, {{.Name}}, 我们注意到您的 Mattermost 服务器集了一些“灰尘”(有一段时间没有维护了),快来看看可以帮助减轻团队工作量的一些功能." - }, - { - "id": "api.templates.server_inactivity_subject", - "translation": "快来打开 Mattermost 以提高您团队的生产力!" - }, - { - "id": "api.templates.server_inactivity_info_bullet2", - "translation": "管理任务由 " - }, - { - "id": "api.templates.server_inactivity_info_bullet1", - "translation": "工作流管理于 " - }, - { - "id": "api.templates.server_inactivity_info_bullet", - "translation": "指定客户专访 " - }, - { - "id": "api.templates.server_inactivity_info", - "translation": "快来看看吧!" - }, - { - "id": "api.templates.server_inactivity_button", - "translation": "打开 Mattermost" - }, { "id": "api.templates.invite_team_and_channels_subject", "translation": "[{{ .SiteName }}] {{ .SenderName }} 邀请您加入 {{ .TeamDisplayName }} 团队的 {{ .ChannelsLen }} 频道组" @@ -9127,18 +9095,6 @@ "id": "api.custom_groups.count_err", "translation": "统计“组”时出现错误" }, - { - "id": "Playbooks", - "translation": "规划书" - }, - { - "id": "Channels", - "translation": "频道" - }, - { - "id": "Boards", - "translation": "面板" - }, { "id": "model.oauth.is_valid.mattermost_app_id.app_error", "translation": "MattermostAppID 的最大长度为 32 个字符。" @@ -9171,10 +9127,6 @@ "id": "app.insights.feature_disabled", "translation": "Insights 功能已禁用。" }, - { - "id": "api.templates.server_inactivity_footer_disclaimer", - "translation": "您收到这封一次性电子邮件是因为您的 Mattermost 服务器处于非活动状态超过 {{.Hours}} 小时。 本电子邮件是由您的 Mattermost 服务器自动生成。" - }, { "id": "api.file.cloud_upload.app_error", "translation": "不支持通过 mmctl 上传到 Cloud 实例。 请在此处查看文档:https://docs.mattermost.com/manage/cloud-data-export.html。" @@ -9505,7 +9457,7 @@ }, { "id": "worktemplate.category.product_teams", - "translation": "产品团队" + "translation": "产品" }, { "id": "worktemplate.category.leadership", @@ -9687,10 +9639,6 @@ "id": "app.post_prority.get_for_post.app_error", "translation": "无法取得消息的优先级" }, - { - "id": "app.plugin.product_mode.app_error", - "translation": "在生产模式中无法启用 {{.Name}} 插件。" - }, { "id": "app.notify_admin.send_notification_post.app_error", "translation": "无法发送通知消息。" @@ -9719,10 +9667,6 @@ "id": "app.file.cloud.get.app_error", "translation": "由于云订阅的限制,无法取得文件。" }, - { - "id": "app.draft.update.app_error", - "translation": "无法更新草稿。" - }, { "id": "app.draft.save.app_error", "translation": "无法保存草稿。" @@ -10082,5 +10026,69 @@ { "id": "api.license.true_up_review.create_error", "translation": "无法创建真实的状态记录" + }, + { + "id": "api.license.request-trial.bad-request.business-email", + "translation": "无效的商务试用邮箱" + }, + { + "id": "worktemplate.product_teams.feature_release.description.integration", + "translation": "通过集成大多您使用过的工具(比如 GitHub)在您的频道里提高生产力,实现你的功能发布。这些工具将为你下载。" + }, + { + "id": "worktemplate.product_teams.feature_release.description.channel", + "translation": "在一个频道与你的团队讨论任何的发布障碍和变动,并很容易的与你的面板,Playbook或者其他的集成功能连接。" + }, + { + "id": "worktemplate.product_teams.feature_release.description.board", + "translation": "使用会议安排面板保持会议一起正常。使用项目任务面板管理你的工作量。" + }, + { + "id": "worktemplate.product_teams.bug_bash.integration", + "translation": "通过集成大多您使用过的工具(比如Jira)在您的频道里提高生产力,跟踪你的Bug解决过程。这些工具将为你下载。" + }, + { + "id": "worktemplate.leadership.goals_and_okrs.integration", + "translation": "通过集成大多您使用过的工具(比如 Zoom)在您的频道里提高生产力,实行更容易的协作。这些工具将为你下载。" + }, + { + "id": "worktemplate.leadership.goals_and_okrs.channel", + "translation": "和您的团队讨论目标和进度,以异步或者同步的方式,在同一个频道中保持跟上任何发生的变化。" + }, + { + "id": "worktemplate.leadership.goals_and_okrs.board", + "translation": "使用目标和OKR面板跟踪您的团队进度向组织目标推进。使用会议安排面板保持会议一起正常。" + }, + { + "id": "worktemplate.devops.incident_resolution.description.channel", + "translation": "在一单独频道与您的团队讨论优先级、添加利益相关者,提供更新,向解决的方向努力。" + }, + { + "id": "worktemplate.devops.incident_resolution.description.board", + "translation": "使用事故解决面板来实现重复性的流程和分配跨越整个团队的任务。" + }, + { + "id": "worktemplate.companywide.goals_and_okrs.integration", + "translation": "通过集成大多您使用过的工具(比如 Zoom)在您的频道里提高生产力,实行更容易的协作。这些工具将为你下载。" + }, + { + "id": "worktemplate.companywide.goals_and_okrs.channel", + "translation": "和您的团队讨论目标和进度,以异步或者同步的方式,在同一个频道中保持跟上任何发生的变化。" + }, + { + "id": "worktemplate.companywide.goals_and_okrs.board", + "translation": "使用目标和OKR面板跟踪您的团队进度向组织目标推进。使用会议安排面板保持会议一起正常。" + }, + { + "id": "worktemplate.companywide.create_project.integration", + "translation": "通过集成大多您使用过的工具在您的频道里提高生产力。这些工具将为你下载。" + }, + { + "id": "worktemplate.companywide.create_project.channel", + "translation": "与您的团队在一个协作频道里讨论新的项目并且决定如何你们将如此组织。" + }, + { + "id": "worktemplate.companywide.create_project.board", + "translation": "使用看板面板定义和跟踪您的项目任务列表和进度。" } ] diff --git a/server/i18n/zh-TW.json b/server/i18n/zh-TW.json index 780ee87973..e677cec709 100644 --- a/server/i18n/zh-TW.json +++ b/server/i18n/zh-TW.json @@ -7247,10 +7247,6 @@ "id": "api.back_to_app", "translation": "返回至 {{.SiteName}}" }, - { - "id": "Channels", - "translation": "頻道" - }, { "id": "api.cloud.app_error", "translation": "雲端 API 請求時發生內部錯誤。" diff --git a/server/playbooks/server/main_test.go b/server/playbooks/server/main_test.go index c63f5e8b00..3392c2cb1e 100644 --- a/server/playbooks/server/main_test.go +++ b/server/playbooks/server/main_test.go @@ -6,6 +6,7 @@ package main import ( "context" "encoding/json" + "fmt" "os" "os/exec" "path/filepath" @@ -117,7 +118,7 @@ func Setup(t *testing.T) *TestEnvironment { config := configStore.Get() // Force plugins to be disabled since we are in product mode config.PluginSettings.Enable = model.NewBool(false) - config.ServiceSettings.ListenAddress = model.NewString("localhost:9056") + config.ServiceSettings.ListenAddress = model.NewString("localhost:0") config.TeamSettings.MaxUsersPerTeam = model.NewInt(10000) config.LocalizationSettings.SetDefaults() config.SqlSettings = *sqlSettings @@ -218,7 +219,7 @@ func (e *TestEnvironment) CreateClients() { require.Nil(e.T, appErr) e.RegularUserNotInTeam = notInTeam - siteURL := "http://localhost:9056" + siteURL := fmt.Sprintf("http://localhost:%v", e.A.Srv().ListenAddr.Port) serverAdminClient := model.NewAPIv4Client(siteURL) _, _, err := serverAdminClient.Login(admin.Email, userPassword) diff --git a/webapp/channels/.eslintignore b/webapp/channels/.eslintignore index 9c3001349d..468bb89127 100644 --- a/webapp/channels/.eslintignore +++ b/webapp/channels/.eslintignore @@ -1,4 +1,3 @@ -e2e/results node_modules dist lib diff --git a/webapp/channels/.eslintrc.json b/webapp/channels/.eslintrc.json index 048bb4b85e..7a3865a6db 100644 --- a/webapp/channels/.eslintrc.json +++ b/webapp/channels/.eslintrc.json @@ -2,22 +2,19 @@ "root": true, "extends": [ "plugin:mattermost/react", - "plugin:cypress/recommended", "plugin:react-hooks/recommended" ], "plugins": [ "@babel/eslint-plugin", "mattermost", "import", - "cypress", "no-only-tests", "@typescript-eslint", "formatjs" ], "parser": "@typescript-eslint/parser", "env": { - "jest": true, - "cypress/globals": true + "jest": true }, "settings": { "import/resolver": "webpack", @@ -166,17 +163,6 @@ "no-process-env": 0, "prefer-arrow-callback": 0 } - }, - { - "files": ["e2e/**"], - "rules": { - "@babel/no-unused-expressions": 0, - "func-names": 0, - "import/no-unresolved": 0, - "max-nested-callbacks": 0, - "no-process-env": 0, - "no-unused-expressions": 0 - } } ] } diff --git a/webapp/channels/.gobom.json b/webapp/channels/.gobom.json index 3279174525..4ec47b22c6 100644 --- a/webapp/channels/.gobom.json +++ b/webapp/channels/.gobom.json @@ -3,7 +3,6 @@ "properties": { "GomodMainOnly": true, "GomodExcludes": "_tests/|[-_]generators?$|^plugin/checker$", - "NpmExcludes": "/e2e$", "GradlePath": "./gradlew:../gradlew" }, "filters": [ diff --git a/webapp/channels/Makefile b/webapp/channels/Makefile index 889907e624..aa20ad2ca2 100644 --- a/webapp/channels/Makefile +++ b/webapp/channels/Makefile @@ -6,58 +6,6 @@ export NODE_OPTIONS=--max-old-space-size=4096 i18n-extract: ## Extract strings for translation from the source code npm run i18n-extract -e2e/playwright/node_modules: - @echo Install Playwright and its dependencies - cd e2e/playwright && npm install - -.PHONY: e2e-test -e2e-test: - @echo E2E: Running mattermost-mysql-e2e - @if [ $(shell docker ps -a | grep -ci mattermost-mysql-e2e) -eq 0 ]; then \ - echo starting mattermost-mysql-e2e; \ - docker run --name mattermost-mysql-e2e -p 35476:3306 -e MYSQL_ROOT_PASSWORD=mostest \ - -e MYSQL_USER=mmuser -e MYSQL_PASSWORD=mostest -e MYSQL_DATABASE=mattermost_test -d mysql:5.7 > /dev/null; \ - elif [ $(shell docker ps | grep -ci mattermost-mysql-e2e) -eq 0 ]; then \ - echo restarting mattermost-mysql-e2e; \ - docker start mattermost-mysql-e2e > /dev/null; \ - fi - - cd $(BUILD_SERVER_DIR) && [[ -f config/config.json ]] && \ - cp config/config.json config/config-backup.json && make config-reset || \ - echo "config.json not found" && make config-reset - - @echo E2E: Starting the server - cd $(BUILD_SERVER_DIR) && $(MAKE) run - - @echo E2E: Generating test data - cd $(BUILD_SERVER_DIR) && $(MAKE) test-data - - @echo E2E: Running end-to-end testing - cd e2e && npm install && npm run cypress:run - - @echo E2E: Stoppping the server - cd $(BUILD_SERVER_DIR) && $(MAKE) stop - - @echo E2E: stopping mattermost-mysql-e2e - docker stop mattermost-mysql-e2e > /dev/null - - cd $(BUILD_SERVER_DIR) && [[ -f config/config-backup.json ]] && \ - cp config/config-backup.json config/config.json && echo "revert local config.json" || \ - echo "config-backup.json not found" && sed -i'' -e 's|"DataSource": ".*"|"DataSource": "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8\u0026readTimeout=30s\u0026writeTimeout=30s"|g' config/config.json - - @echo E2E: Tests completed - -.PHONY: clean-e2e -clean-e2e: - @if [ $(shell docker ps -a | grep -ci mattermost-mysql-e2e) -eq 1 ]; then \ - echo stopping mattermost-mysql-e2e; \ - docker stop mattermost-mysql-e2e > /dev/null; \ - fi - - cd $(BUILD_SERVER_DIR) && [[ -f config/config-backup.json ]] && \ - cp config/config-backup.json config/config.json && echo "revert local config.json" || \ - echo "config-backup.json not found" && sed -i'' -e 's|"DataSource": ".*"|"DataSource": "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8\u0026readTimeout=30s\u0026writeTimeout=30s"|g' config/config.json - .PHONY: emojis emojis: ## Creates emoji JSON, JSX and Go files and extracts emoji images from the system font SERVER_DIR=$(BUILD_SERVER_DIR) npm run make-emojis diff --git a/webapp/channels/jest.config.js b/webapp/channels/jest.config.js index 123f6d7e57..01d80ffe8d 100644 --- a/webapp/channels/jest.config.js +++ b/webapp/channels/jest.config.js @@ -5,7 +5,7 @@ const config = { snapshotSerializers: ['enzyme-to-json/serializer'], - testPathIgnorePatterns: ['/node_modules/', '/e2e/'], + testPathIgnorePatterns: ['/node_modules/'], clearMocks: true, collectCoverageFrom: [ 'actions/src/**/*.{js,jsx,ts,tsx}', @@ -17,7 +17,6 @@ const config = { 'selectors/src/**/*.{js,jsx,ts,tsx}', 'stores/src/**/*.{js,jsx,ts,tsx}', 'utils/src/**/*.{js,jsx,ts,tsx}', - '!e2e/**', ], coverageReporters: ['lcov', 'text-summary'], moduleNameMapper: { diff --git a/webapp/channels/package.json b/webapp/channels/package.json index 7f33c6b05f..830cbfae08 100644 --- a/webapp/channels/package.json +++ b/webapp/channels/package.json @@ -169,7 +169,6 @@ "enzyme-to-json": "3.6.2", "eslint": "7.32.0", "eslint-import-resolver-webpack": "0.13.2", - "eslint-plugin-cypress": "2.11.3", "eslint-plugin-formatjs": "4.3.4", "eslint-plugin-header": "3.1.1", "eslint-plugin-import": "2.23.4", diff --git a/webapp/channels/src/actions/command.ts b/webapp/channels/src/actions/command.ts index 2b89fae193..62195ac6dc 100644 --- a/webapp/channels/src/actions/command.ts +++ b/webapp/channels/src/actions/command.ts @@ -35,7 +35,7 @@ import KeyboardShortcutsModal from 'components/keyboard_shortcuts/keyboard_short import {GlobalState} from 'types/store'; import {t} from 'utils/i18n'; -import MarketplaceModal from 'components/plugin_marketplace'; +import MarketplaceModal from 'components/plugin_marketplace/marketplace_modal'; import WorkTemplateModal from 'components/work_templates'; import {haveICurrentTeamPermission} from 'mattermost-redux/selectors/entities/roles'; import {Permissions} from 'mattermost-redux/constants'; diff --git a/webapp/channels/src/actions/views/rhs.ts b/webapp/channels/src/actions/views/rhs.ts index 67fba87ea8..908d08a2be 100644 --- a/webapp/channels/src/actions/views/rhs.ts +++ b/webapp/channels/src/actions/views/rhs.ts @@ -14,13 +14,12 @@ import { searchFilesWithParams, } from 'mattermost-redux/actions/search'; import * as PostActions from 'mattermost-redux/actions/posts'; -import {getCurrentUserId, getCurrentUserMentionKeys} from 'mattermost-redux/selectors/entities/users'; +import {getCurrentUserMentionKeys} from 'mattermost-redux/selectors/entities/users'; import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {getCurrentChannelId, getCurrentChannelNameForSearchShortcut, getChannel as getChannelSelector} from 'mattermost-redux/selectors/entities/channels'; import {getPost} from 'mattermost-redux/selectors/entities/posts'; -import {makeGetUserTimezone} from 'mattermost-redux/selectors/entities/timezone'; -import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils'; +import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone'; import {Action, ActionResult, DispatchFunc, GenericAction, GetStateFunc} from 'mattermost-redux/types/actions'; import {Post} from '@mattermost/types/posts'; @@ -174,9 +173,7 @@ export function performSearch(terms: string, isMentionSearch?: boolean) { } // timezone offset in seconds - const userId = getCurrentUserId(getState()); - const userTimezone = makeGetUserTimezone()(getState(), userId); - const userCurrentTimezone = getUserCurrentTimezone(userTimezone); + const userCurrentTimezone = getCurrentTimezone(getState()); const timezoneOffset = ((userCurrentTimezone && (userCurrentTimezone.length > 0)) ? getUtcOffsetForTimeZone(userCurrentTimezone) : getBrowserUtcOffset()) * 60; const messagesPromise = dispatch(searchPostsWithParams(isMentionSearch ? '' : teamId, {terms, is_or_search: Boolean(isMentionSearch), include_deleted_channels: viewArchivedChannels, time_zone_offset: timezoneOffset, page: 0, per_page: 20})); const filesPromise = dispatch(searchFilesWithParams(teamId, {terms: termsWithExtensionsFilters, is_or_search: Boolean(isMentionSearch), include_deleted_channels: viewArchivedChannels, time_zone_offset: timezoneOffset, page: 0, per_page: 20})); diff --git a/webapp/channels/src/components/__snapshots__/generic_modal.test.tsx.snap b/webapp/channels/src/components/__snapshots__/generic_modal.test.tsx.snap index 95b80cb2da..cf1462ee4a 100644 --- a/webapp/channels/src/components/__snapshots__/generic_modal.test.tsx.snap +++ b/webapp/channels/src/components/__snapshots__/generic_modal.test.tsx.snap @@ -59,7 +59,7 @@ exports[`components/GenericModal should match snapshot for base case 1`] = `
@@ -126,11 +126,12 @@ exports[`components/GenericModal should match snapshot with both buttons 1`] = `
{ }, appBindings: [], pluginMenuItems: [], - handleOpenTip: jest.fn(), - handleNextTip: jest.fn(), - handleDismissTip: jest.fn(), - showPulsatingDot: false, - showTutorialTip: false, appsEnabled: false, isSysAdmin: true, canOpenMarketplace: false, diff --git a/webapp/channels/src/components/actions_menu/actions_menu_mobile.test.tsx b/webapp/channels/src/components/actions_menu/actions_menu_mobile.test.tsx index 3f4f3c3534..cdd71a8390 100644 --- a/webapp/channels/src/components/actions_menu/actions_menu_mobile.test.tsx +++ b/webapp/channels/src/components/actions_menu/actions_menu_mobile.test.tsx @@ -37,11 +37,6 @@ describe('components/actions_menu/ActionsMenu on mobile view', () => { }, appBindings: [], pluginMenuItems: [], - handleOpenTip: jest.fn(), - handleNextTip: jest.fn(), - handleDismissTip: jest.fn(), - showPulsatingDot: false, - showTutorialTip: false, appsEnabled: false, isSysAdmin: true, canOpenMarketplace: false, diff --git a/webapp/channels/src/components/activity_and_insights/insights/top_channels/top_channels.tsx b/webapp/channels/src/components/activity_and_insights/insights/top_channels/top_channels.tsx index 86bb49fbc6..8d2dbb23e1 100644 --- a/webapp/channels/src/components/activity_and_insights/insights/top_channels/top_channels.tsx +++ b/webapp/channels/src/components/activity_and_insights/insights/top_channels/top_channels.tsx @@ -11,14 +11,13 @@ import {TopChannel, TopChannelGraphData} from '@mattermost/types/insights'; import {CircleSkeletonLoader, RectangleSkeletonLoader} from '@mattermost/components'; import {getCurrentRelativeTeamUrl, getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams'; +import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone'; import {getMyTopChannels, getTopChannelsForTeam} from 'mattermost-redux/actions/insights'; import Constants, {InsightsScopes} from 'utils/constants'; import {trackEvent} from 'actions/telemetry_actions'; -import {getCurrentUserTimezone} from 'selectors/general'; - import OverlayTrigger from 'components/overlay_trigger'; import Tooltip from 'components/tooltip'; @@ -38,7 +37,7 @@ const TopChannels = (props: WidgetHocProps) => { const currentTeamId = useSelector(getCurrentTeamId); const currentTeamUrl = useSelector(getCurrentRelativeTeamUrl); - const timeZone = useSelector(getCurrentUserTimezone); + const timeZone = useSelector(getCurrentTimezone); const getTopTeamChannels = useCallback(async () => { if (props.filterType === InsightsScopes.TEAM) { diff --git a/webapp/channels/src/components/admin_console/billing/billing_summary/billing_summary.tsx b/webapp/channels/src/components/admin_console/billing/billing_summary/billing_summary.tsx index 14c9f85791..5a3c40872a 100644 --- a/webapp/channels/src/components/admin_console/billing/billing_summary/billing_summary.tsx +++ b/webapp/channels/src/components/admin_console/billing/billing_summary/billing_summary.tsx @@ -226,9 +226,9 @@ export const InvoiceInfo = ({invoice, product, fullCharges, partialCharges, hasM currency='USD' /> )} @@ -309,9 +309,9 @@ export const InvoiceInfo = ({invoice, product, fullCharges, partialCharges, hasM >
diff --git a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_cta.tsx b/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_cta.tsx index ef8ec25dad..ccdf364756 100644 --- a/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_cta.tsx +++ b/webapp/channels/src/components/admin_console/billing/delete_workspace/delete_workspace_cta.tsx @@ -15,8 +15,6 @@ import {isCloudLicense} from 'utils/license_utils'; import {getCloudSubscription, getSubscriptionProduct} from 'mattermost-redux/selectors/entities/cloud'; -import ExternalLink from 'components/external_link'; - import DeleteWorkspaceModal from './delete_workspace_modal'; export default function DeleteWorkspaceCTA() { @@ -80,17 +78,15 @@ export default function DeleteWorkspaceCTA() { }} />
- - +
); diff --git a/webapp/channels/src/components/admin_console/billing/invoice_user_count.test.tsx b/webapp/channels/src/components/admin_console/billing/invoice_user_count.test.tsx index b65bc918e6..e4f6e098f4 100644 --- a/webapp/channels/src/components/admin_console/billing/invoice_user_count.test.tsx +++ b/webapp/channels/src/components/admin_console/billing/invoice_user_count.test.tsx @@ -51,28 +51,28 @@ describe('InvoiceUserCount', () => { [1, InvoiceLineItemType.Full], [1, InvoiceLineItemType.Partial], ), - expected: '1 metered users, 1 users at full rate, 1 users with partial charges', + expected: '1 metered seats, 1 seats at full rate, 1 seats with partial charges', }, { name: 'Supports cloud invoices with only metered line items', invoice: makeInvoice( [12.34, InvoiceLineItemType.Metered], ), - expected: '12.34 users', + expected: '12.34 seats', }, { name: 'Shows minimum decimal necessary', invoice: makeInvoice( [12.499, InvoiceLineItemType.Metered], ), - expected: '12.5 users', + expected: '12.5 seats', }, { name: 'hides insignificant decimals', invoice: makeInvoice( [12.002, InvoiceLineItemType.Metered], ), - expected: '12 users', + expected: '12 seats', }, { name: 'Supports cloud invoices with only non-metered line items', @@ -80,7 +80,7 @@ describe('InvoiceUserCount', () => { [1, InvoiceLineItemType.Full], [249, InvoiceLineItemType.Partial], ), - expected: '1 users at full rate, 249 users with partial charges', + expected: '1 seats at full rate, 249 seats with partial charges', }, { name: 'Shows default of 0 full users, 0 partial users when there are no users', @@ -89,19 +89,19 @@ describe('InvoiceUserCount', () => { [0, InvoiceLineItemType.Full], [0, InvoiceLineItemType.Partial], ), - expected: '0 users at full rate, 0 users with partial charges', + expected: '0 seats at full rate, 0 seats with partial charges', }, { name: 'Shows default of 0 full users, 0 partial users when there are no line items in invoice', invoice: makeInvoice(), - expected: '0 users at full rate, 0 users with partial charges', + expected: '0 seats at full rate, 0 seats with partial charges', }, { name: 'Shows 3 full userswhen there are on prem users', invoice: makeInvoice( [3, InvoiceLineItemType.OnPremise], ), - expected: '3 users', + expected: '3 seats', }, ]; diff --git a/webapp/channels/src/components/admin_console/billing/invoice_user_count.tsx b/webapp/channels/src/components/admin_console/billing/invoice_user_count.tsx index 896b7f0b63..c93be8af8e 100644 --- a/webapp/channels/src/components/admin_console/billing/invoice_user_count.tsx +++ b/webapp/channels/src/components/admin_console/billing/invoice_user_count.tsx @@ -18,8 +18,8 @@ export default function InvoiceUserCount({invoice}: {invoice: Invoice}): JSX.Ele if (onPremUsers) { return ( ); @@ -45,10 +45,10 @@ export default function InvoiceUserCount({invoice}: {invoice: Invoice}): JSX.Ele return ( ); @@ -56,11 +56,11 @@ export default function InvoiceUserCount({invoice}: {invoice: Invoice}): JSX.Ele return ( ); diff --git a/webapp/channels/src/components/admin_console/license_settings/enterprise_edition/enterprise_edition_left_panel.test.tsx b/webapp/channels/src/components/admin_console/license_settings/enterprise_edition/enterprise_edition_left_panel.test.tsx index ef6a3d387f..603aa6807b 100644 --- a/webapp/channels/src/components/admin_console/license_settings/enterprise_edition/enterprise_edition_left_panel.test.tsx +++ b/webapp/channels/src/components/admin_console/license_settings/enterprise_edition/enterprise_edition_left_panel.test.tsx @@ -82,7 +82,7 @@ describe('components/admin_console/license_settings/enterprise_edition/enterpris return n.children().length === 2 && n.childAt(0).type() === 'span' && !n.childAt(0).text().includes('ACTIVE') && - n.childAt(0).text().includes('USERS'); + n.childAt(0).text().includes('LICENSED SEATS'); }); expect(item.text()).toContain('1,000,000'); diff --git a/webapp/channels/src/components/admin_console/license_settings/enterprise_edition/enterprise_edition_left_panel.tsx b/webapp/channels/src/components/admin_console/license_settings/enterprise_edition/enterprise_edition_left_panel.tsx index c3f37bfd93..790272b3a5 100644 --- a/webapp/channels/src/components/admin_console/license_settings/enterprise_edition/enterprise_edition_left_panel.tsx +++ b/webapp/channels/src/components/admin_console/license_settings/enterprise_edition/enterprise_edition_left_panel.tsx @@ -160,7 +160,7 @@ const EnterpriseEditionLeftPanel = ({ ); }; -type LegendValues = 'START DATE:' | 'EXPIRES:' | 'USERS:' | 'ACTIVE USERS:' | 'EDITION:' | 'LICENSE ISSUED:' | 'NAME:' | 'COMPANY / ORG:' +type LegendValues = 'START DATE:' | 'EXPIRES:' | 'LICENSED SEATS:' | 'ACTIVE USERS:' | 'EDITION:' | 'LICENSE ISSUED:' | 'NAME:' | 'COMPANY / ORG:' const renderLicenseValues = (activeUsers: number, seatsPurchased: number) => ({legend, value}: {legend: LegendValues; value: string | JSX.Element | null}, index: number): React.ReactNode => { if (legend === 'ACTIVE USERS:') { @@ -236,7 +236,7 @@ const renderLicenseContent = ( }> = [ {legend: 'START DATE:', value: startsAt}, {legend: 'EXPIRES:', value: expiresAt}, - {legend: 'USERS:', value: users}, + {legend: 'LICENSED SEATS:', value: users}, {legend: 'ACTIVE USERS:', value: activeUsers}, {legend: 'EDITION:', value: sku}, {legend: 'LICENSE ISSUED:', value: issued}, diff --git a/webapp/channels/src/components/admin_console/license_settings/modals/upload_license_modal.tsx b/webapp/channels/src/components/admin_console/license_settings/modals/upload_license_modal.tsx index 7d16824535..634b030724 100644 --- a/webapp/channels/src/components/admin_console/license_settings/modals/upload_license_modal.tsx +++ b/webapp/channels/src/components/admin_console/license_settings/modals/upload_license_modal.tsx @@ -260,7 +260,7 @@ const UploadLicenseModal = (props: Props): JSX.Element | null => {
{ + onBlur?.(); + setKeepEditorInFocus(false); + }, [onBlur]); + + const handleFocus = useCallback(() => { + setKeepEditorInFocus(true); + }, []); + let serverErrorJsx = null; if (serverError) { serverErrorJsx = ( @@ -422,6 +432,7 @@ const AdvanceTextEditor = ({ /> )} slot2={null} + shouldScrollIntoView={keepEditorInFocus} /> ); @@ -479,6 +490,7 @@ const AdvanceTextEditor = ({ handlePostError={handlePostError} value={messageValue} onBlur={handleBlur} + onFocus={handleFocus} emojiEnabled={enableEmojiPicker} createMessage={createMessage} channelId={channelId} diff --git a/webapp/channels/src/components/analytics/activated_users_card/index.tsx b/webapp/channels/src/components/analytics/activated_users_card/index.tsx index 1f4d6c68a3..cce7ac420c 100644 --- a/webapp/channels/src/components/analytics/activated_users_card/index.tsx +++ b/webapp/channels/src/components/analytics/activated_users_card/index.tsx @@ -56,7 +56,7 @@ export const ActivatedUserCard = ({activatedUsers, seatsPurchased, isCloud}: Act /> {(text) => {text}} diff --git a/webapp/channels/src/components/analytics/system_analytics/system_analytics.tsx b/webapp/channels/src/components/analytics/system_analytics/system_analytics.tsx index b760279651..151941ee2a 100644 --- a/webapp/channels/src/components/analytics/system_analytics/system_analytics.tsx +++ b/webapp/channels/src/components/analytics/system_analytics/system_analytics.tsx @@ -348,7 +348,7 @@ export default class SystemAnalytics extends React.PureComponent { title={ } icon='fa-users' diff --git a/webapp/channels/src/components/app_bar/app_bar_marketplace.tsx b/webapp/channels/src/components/app_bar/app_bar_marketplace.tsx index 11b379bd75..b50ec6832f 100644 --- a/webapp/channels/src/components/app_bar/app_bar_marketplace.tsx +++ b/webapp/channels/src/components/app_bar/app_bar_marketplace.tsx @@ -10,7 +10,7 @@ import Icon from '@mattermost/compass-components/foundations/icon'; import {openModal} from 'actions/views/modals'; -import MarketplaceModal from 'components/plugin_marketplace'; +import MarketplaceModal from 'components/plugin_marketplace/marketplace_modal'; import OverlayTrigger from 'components/overlay_trigger'; import {Constants, ModalIdentifiers} from 'utils/constants'; diff --git a/webapp/channels/src/components/channel_select/channel_select.tsx b/webapp/channels/src/components/channel_select/channel_select.tsx index d6b3a8e882..b7c32449d9 100644 --- a/webapp/channels/src/components/channel_select/channel_select.tsx +++ b/webapp/channels/src/components/channel_select/channel_select.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React from 'react'; +import React, {ChangeEventHandler} from 'react'; import {Channel} from '@mattermost/types/channels'; @@ -10,7 +10,7 @@ import * as Utils from 'utils/utils'; type Props = { channels: Channel[]; - onChange?: () => void; + onChange?: ChangeEventHandler; value?: string; selectOpen: boolean; selectPrivate: boolean; diff --git a/webapp/channels/src/components/choose_different_shipping/choose_different_shipping.scss b/webapp/channels/src/components/choose_different_shipping/choose_different_shipping.scss index 322997fc03..ab86a14982 100644 --- a/webapp/channels/src/components/choose_different_shipping/choose_different_shipping.scss +++ b/webapp/channels/src/components/choose_different_shipping/choose_different_shipping.scss @@ -1,10 +1,13 @@ +@import '../payment_form/mixins'; + .shipping-address-section { display: flex; - align-content: flex-start; + align-items: center; padding-bottom: 24px; font-weight: normal; button.no-style { + height: auto; padding-left: 0; border: none; background: transparent; @@ -26,7 +29,6 @@ padding-left: 12px; cursor: default; font-family: 'Open Sans', sans-serif; - vertical-align: middle; } .billing_address_btn_text { @@ -35,3 +37,7 @@ font-weight: bold; } } + +.PurchaseModal .shipping-address-section { + @include payment-form-padding; +} diff --git a/webapp/channels/src/components/custom_open_plugin_install_post_renderer/index.tsx b/webapp/channels/src/components/custom_open_plugin_install_post_renderer/index.tsx index d16f073cf1..3d55a4cdc1 100644 --- a/webapp/channels/src/components/custom_open_plugin_install_post_renderer/index.tsx +++ b/webapp/channels/src/components/custom_open_plugin_install_post_renderer/index.tsx @@ -10,7 +10,7 @@ import {uniqWith} from 'lodash'; import {Post} from '@mattermost/types/posts'; import {MarketplacePlugin} from '@mattermost/types/marketplace'; -import MarketplaceModal from 'components/plugin_marketplace'; +import MarketplaceModal from 'components/plugin_marketplace/marketplace_modal'; import Markdown from 'components/markdown'; import {getUsers} from 'mattermost-redux/selectors/entities/users'; diff --git a/webapp/channels/src/components/custom_status/custom_status_emoji.test.tsx b/webapp/channels/src/components/custom_status/custom_status_emoji.test.tsx index 64c32488d1..7111f0c0fe 100644 --- a/webapp/channels/src/components/custom_status/custom_status_emoji.test.tsx +++ b/webapp/channels/src/components/custom_status/custom_status_emoji.test.tsx @@ -12,8 +12,8 @@ import mockStore from 'tests/test_store'; import CustomStatusEmoji from './custom_status_emoji'; +jest.mock('mattermost-redux/selectors/entities/timezone'); jest.mock('selectors/views/custom_status'); -jest.mock('selectors/general'); describe('components/custom_status/custom_status_emoji', () => { const store = mockStore({}); diff --git a/webapp/channels/src/components/custom_status/custom_status_emoji.tsx b/webapp/channels/src/components/custom_status/custom_status_emoji.tsx index 43ad92d45a..f4359fae1f 100644 --- a/webapp/channels/src/components/custom_status/custom_status_emoji.tsx +++ b/webapp/channels/src/components/custom_status/custom_status_emoji.tsx @@ -6,8 +6,9 @@ import {useSelector} from 'react-redux'; import {CustomStatusDuration} from '@mattermost/types/users'; +import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone'; + import {GlobalState} from 'types/store'; -import {getCurrentUserTimezone} from 'selectors/general'; import {makeGetCustomStatus, isCustomStatusEnabled, isCustomStatusExpired} from 'selectors/views/custom_status'; import Constants from 'utils/constants'; @@ -41,7 +42,7 @@ function CustomStatusEmoji({ const getCustomStatus = useMemo(makeGetCustomStatus, []); const customStatus = useSelector((state: GlobalState) => getCustomStatus(state, userID)); - const timezone = useSelector(getCurrentUserTimezone); + const timezone = useSelector(getCurrentTimezone); const customStatusExpired = useSelector((state: GlobalState) => isCustomStatusExpired(state, customStatus)); const customStatusEnabled = useSelector(isCustomStatusEnabled); diff --git a/webapp/channels/src/components/custom_status/custom_status_modal.tsx b/webapp/channels/src/components/custom_status/custom_status_modal.tsx index 35844581f4..28f2efd9aa 100644 --- a/webapp/channels/src/components/custom_status/custom_status_modal.tsx +++ b/webapp/channels/src/components/custom_status/custom_status_modal.tsx @@ -11,6 +11,7 @@ import {useRouteMatch} from 'react-router-dom'; import {setCustomStatus, unsetCustomStatus, removeRecentCustomStatus} from 'mattermost-redux/actions/users'; import {setCustomStatusInitialisationState} from 'mattermost-redux/actions/preferences'; import {Preferences} from 'mattermost-redux/constants'; +import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone'; import {loadCustomEmojisIfNeeded} from 'actions/emoji_actions'; import {closeModal} from 'actions/views/modals'; @@ -20,7 +21,6 @@ import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay'; import RenderEmoji from 'components/emoji/render_emoji'; import QuickInput, {MaxLengthInput} from 'components/quick_input'; import {makeGetCustomStatus, getRecentCustomStatuses, showStatusDropdownPulsatingDot, isCustomStatusExpired} from 'selectors/views/custom_status'; -import {getCurrentUserTimezone} from 'selectors/general'; import {GlobalState} from 'types/store'; import {getCurrentMomentForTimezone} from 'utils/timezone'; import {A11yCustomEventTypes, A11yFocusEventDetail, Constants, ModalIdentifiers} from 'utils/constants'; @@ -115,7 +115,7 @@ const CustomStatusModal: React.FC = (props: Props) => { const [duration, setDuration] = useState(initialDuration === undefined ? defaultDuration : initialDuration); const isStatusSet = Boolean(emoji || text); const firstTimeModalOpened = useSelector(showStatusDropdownPulsatingDot); - const timezone = useSelector(getCurrentUserTimezone); + const timezone = useSelector(getCurrentTimezone); const inCustomEmojiPath = useRouteMatch('/:team/emoji'); const currentTime = getCurrentMomentForTimezone(timezone); diff --git a/webapp/channels/src/components/dot_menu/__snapshots__/dot_menu.test.tsx.snap b/webapp/channels/src/components/dot_menu/__snapshots__/dot_menu.test.tsx.snap index 9970088867..490e16e2da 100644 --- a/webapp/channels/src/components/dot_menu/__snapshots__/dot_menu.test.tsx.snap +++ b/webapp/channels/src/components/dot_menu/__snapshots__/dot_menu.test.tsx.snap @@ -107,113 +107,6 @@ Object { } `; -exports[`components/dot_menu/DotMenu should match snapshot, hide "New" badge on forward post 1`] = ` -Object { - "asFragment": [Function], - "baseElement": -
- -
- , - "container":
- -
, - "debug": [Function], - "findAllByAltText": [Function], - "findAllByDisplayValue": [Function], - "findAllByLabelText": [Function], - "findAllByPlaceholderText": [Function], - "findAllByRole": [Function], - "findAllByTestId": [Function], - "findAllByText": [Function], - "findAllByTitle": [Function], - "findByAltText": [Function], - "findByDisplayValue": [Function], - "findByLabelText": [Function], - "findByPlaceholderText": [Function], - "findByRole": [Function], - "findByTestId": [Function], - "findByText": [Function], - "findByTitle": [Function], - "getAllByAltText": [Function], - "getAllByDisplayValue": [Function], - "getAllByLabelText": [Function], - "getAllByPlaceholderText": [Function], - "getAllByRole": [Function], - "getAllByTestId": [Function], - "getAllByText": [Function], - "getAllByTitle": [Function], - "getByAltText": [Function], - "getByDisplayValue": [Function], - "getByLabelText": [Function], - "getByPlaceholderText": [Function], - "getByRole": [Function], - "getByTestId": [Function], - "getByText": [Function], - "getByTitle": [Function], - "queryAllByAltText": [Function], - "queryAllByDisplayValue": [Function], - "queryAllByLabelText": [Function], - "queryAllByPlaceholderText": [Function], - "queryAllByRole": [Function], - "queryAllByTestId": [Function], - "queryAllByText": [Function], - "queryAllByTitle": [Function], - "queryByAltText": [Function], - "queryByDisplayValue": [Function], - "queryByLabelText": [Function], - "queryByPlaceholderText": [Function], - "queryByRole": [Function], - "queryByTestId": [Function], - "queryByText": [Function], - "queryByTitle": [Function], - "rerender": [Function], - "unmount": [Function], -} -`; - exports[`components/dot_menu/DotMenu should match snapshot, on Center 1`] = ` `; - -exports[`components/dot_menu/DotMenu should match snapshot, show "New" badge on forward post 1`] = ` -Object { - "asFragment": [Function], - "baseElement": -
- -
- , - "container":
- -
, - "debug": [Function], - "findAllByAltText": [Function], - "findAllByDisplayValue": [Function], - "findAllByLabelText": [Function], - "findAllByPlaceholderText": [Function], - "findAllByRole": [Function], - "findAllByTestId": [Function], - "findAllByText": [Function], - "findAllByTitle": [Function], - "findByAltText": [Function], - "findByDisplayValue": [Function], - "findByLabelText": [Function], - "findByPlaceholderText": [Function], - "findByRole": [Function], - "findByTestId": [Function], - "findByText": [Function], - "findByTitle": [Function], - "getAllByAltText": [Function], - "getAllByDisplayValue": [Function], - "getAllByLabelText": [Function], - "getAllByPlaceholderText": [Function], - "getAllByRole": [Function], - "getAllByTestId": [Function], - "getAllByText": [Function], - "getAllByTitle": [Function], - "getByAltText": [Function], - "getByDisplayValue": [Function], - "getByLabelText": [Function], - "getByPlaceholderText": [Function], - "getByRole": [Function], - "getByTestId": [Function], - "getByText": [Function], - "getByTitle": [Function], - "queryAllByAltText": [Function], - "queryAllByDisplayValue": [Function], - "queryAllByLabelText": [Function], - "queryAllByPlaceholderText": [Function], - "queryAllByRole": [Function], - "queryAllByTestId": [Function], - "queryAllByText": [Function], - "queryAllByTitle": [Function], - "queryByAltText": [Function], - "queryByDisplayValue": [Function], - "queryByLabelText": [Function], - "queryByPlaceholderText": [Function], - "queryByRole": [Function], - "queryByTestId": [Function], - "queryByText": [Function], - "queryByTitle": [Function], - "rerender": [Function], - "unmount": [Function], -} -`; diff --git a/webapp/channels/src/components/dot_menu/dot_menu.test.tsx b/webapp/channels/src/components/dot_menu/dot_menu.test.tsx index e807940ead..402cf954b1 100644 --- a/webapp/channels/src/components/dot_menu/dot_menu.test.tsx +++ b/webapp/channels/src/components/dot_menu/dot_menu.test.tsx @@ -146,7 +146,6 @@ describe('components/dot_menu/DotMenu', () => { threadReplyCount: 0, userId: 'user_id_1', isMilitaryTime: false, - showForwardPostNewLabel: false, }; test('should match snapshot, on Center', () => { @@ -181,32 +180,6 @@ describe('components/dot_menu/DotMenu', () => { expect(wrapper).toMatchSnapshot(); }); - test('should match snapshot, show "New" badge on forward post', () => { - const props = { - ...baseProps, - showForwardPostNewLabel: true, - }; - const wrapper = renderWithIntlAndStore( - , - initialState, - ); - - expect(wrapper).toMatchSnapshot(); - }); - - test('should match snapshot, hide "New" badge on forward post', () => { - const props = { - ...baseProps, - showForwardPostNewLabel: false, - }; - const wrapper = renderWithIntlAndStore( - , - initialState, - ); - - expect(wrapper).toMatchSnapshot(); - }); - test('should show mark as unread when channel is not archived', () => { const props = { ...baseProps, diff --git a/webapp/channels/src/components/dot_menu/dot_menu.tsx b/webapp/channels/src/components/dot_menu/dot_menu.tsx index 135fb04593..df164cf18f 100644 --- a/webapp/channels/src/components/dot_menu/dot_menu.tsx +++ b/webapp/channels/src/components/dot_menu/dot_menu.tsx @@ -25,7 +25,7 @@ import { import Permissions from 'mattermost-redux/constants/permissions'; -import {Locations, ModalIdentifiers, Constants, TELEMETRY_LABELS, Preferences} from 'utils/constants'; +import {Locations, ModalIdentifiers, Constants, TELEMETRY_LABELS} from 'utils/constants'; import DeletePostModal from 'components/delete_post_modal'; import DelayedAction from 'utils/delayed_action'; import * as PostUtils from 'utils/post_utils'; @@ -33,12 +33,10 @@ import * as Menu from 'components/menu'; import * as Utils from 'utils/utils'; import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate'; import {ModalData} from 'types/actions'; -import {PluginComponent} from 'types/store/plugins'; import {UserThread} from '@mattermost/types/threads'; import {Post} from '@mattermost/types/posts'; import ForwardPostModal from '../forward_post_modal'; -import Tag from '../widgets/tag/tag'; import {ChangeEvent, trackDotMenuEvent} from './utils'; @@ -70,20 +68,11 @@ type Props = { postEditTimeLimit?: string; // TechDebt: Made non-mandatory while converting to typescript enableEmojiPicker?: boolean; // TechDebt: Made non-mandatory while converting to typescript channelIsArchived?: boolean; // TechDebt: Made non-mandatory while converting to typescript - currentTeamUrl?: string; // TechDebt: Made non-mandatory while converting to typescript teamUrl?: string; // TechDebt: Made non-mandatory while converting to typescript isMobileView: boolean; - showForwardPostNewLabel: boolean; timezone?: string; isMilitaryTime: boolean; - /** - * Components for overriding provided by plugins - */ - components: { - [componentName: string]: PluginComponent[]; - }; - actions: { /** @@ -292,9 +281,6 @@ export class DotMenuClass extends React.PureComponent { }, }; - if (this.props.showForwardPostNewLabel) { - this.props.actions.setGlobalItem(Preferences.FORWARD_POST_VIEWED, false); - } this.props.actions.openModal(forwardPostModalData); } @@ -441,15 +427,6 @@ export class DotMenuClass extends React.PureComponent { id='forward_post_button.label' defaultMessage='Forward' /> - {this.props.showForwardPostNewLabel && ( - - )} ); @@ -519,12 +496,12 @@ export class DotMenuClass extends React.PureComponent { class: classNames('post-menu__item', { 'post-menu__item--active': this.props.isMenuOpen, }), - 'aria-label': this.props.intl.formatMessage({id: 'post_info.dot_menu.tooltip.more_actions', defaultMessage: 'Actions'}), + 'aria-label': formatMessage({id: 'post_info.dot_menu.tooltip.more_actions', defaultMessage: 'Actions'}), children: , }} menu={{ id: `${this.props.location}_dropdown_${this.props.post.id}`, - 'aria-label': this.props.intl.formatMessage({id: 'post_info.menuAriaLabel', defaultMessage: 'Post extra options'}), + 'aria-label': formatMessage({id: 'post_info.menuAriaLabel', defaultMessage: 'Post extra options'}), onKeyDown: this.onShortcutKeyDown, width: '264px', onToggle: this.handleMenuToggle, @@ -532,7 +509,7 @@ export class DotMenuClass extends React.PureComponent { }} menuButtonTooltip={{ id: `PostDotMenu-ButtonTooltip-${this.props.post.id}`, - text: this.props.intl.formatMessage({id: 'post_info.dot_menu.tooltip.more_actions', defaultMessage: 'More'}), + text: formatMessage({id: 'post_info.dot_menu.tooltip.more_actions', defaultMessage: 'More'}), class: 'hidden-xs', }} > diff --git a/webapp/channels/src/components/dot_menu/dot_menu_empty.test.tsx b/webapp/channels/src/components/dot_menu/dot_menu_empty.test.tsx index dd9b38594b..c5b2e454d1 100644 --- a/webapp/channels/src/components/dot_menu/dot_menu_empty.test.tsx +++ b/webapp/channels/src/components/dot_menu/dot_menu_empty.test.tsx @@ -59,7 +59,6 @@ describe('components/dot_menu/DotMenu returning empty ("")', () => { threadId: 'post_id_1', userId: 'user_id_1', isMilitaryTime: false, - showForwardPostNewLabel: false, }; const wrapper = shallow( diff --git a/webapp/channels/src/components/dot_menu/dot_menu_mobile.test.tsx b/webapp/channels/src/components/dot_menu/dot_menu_mobile.test.tsx index a6ecb1761c..cd3f1d23a8 100644 --- a/webapp/channels/src/components/dot_menu/dot_menu_mobile.test.tsx +++ b/webapp/channels/src/components/dot_menu/dot_menu_mobile.test.tsx @@ -59,7 +59,6 @@ describe('components/dot_menu/DotMenu on mobile view', () => { threadId: 'post_id_1', userId: 'user_id_1', isMilitaryTime: false, - showForwardPostNewLabel: false, }; const wrapper = shallow( diff --git a/webapp/channels/src/components/dot_menu/index.ts b/webapp/channels/src/components/dot_menu/index.ts index fa1623fa2c..1aea3d476c 100644 --- a/webapp/channels/src/components/dot_menu/index.ts +++ b/webapp/channels/src/components/dot_menu/index.ts @@ -12,7 +12,7 @@ import {getCurrentTeamId, getCurrentTeam, getTeam} from 'mattermost-redux/select import {makeGetThreadOrSynthetic} from 'mattermost-redux/selectors/entities/threads'; import {getPost} from 'mattermost-redux/selectors/entities/posts'; import {getBool, isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences'; -import {getCurrentUserTimezone} from 'selectors/general'; +import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone'; import {isSystemMessage} from 'mattermost-redux/utils/post_utils'; import {GenericAction} from 'mattermost-redux/types/actions'; import {setThreadFollow} from 'mattermost-redux/actions/threads'; @@ -44,7 +44,6 @@ import {matchUserMentionTriggersWithMessageMentions} from 'utils/post_utils'; import {Post} from '@mattermost/types/posts'; import {setGlobalItem} from '../../actions/storage'; -import {getGlobalItem} from '../../selectors/storage'; import DotMenu from './dot_menu'; @@ -110,8 +109,6 @@ function makeMapStateToProps() { } } - const showForwardPostNewLabel = getGlobalItem(state, Preferences.FORWARD_POST_VIEWED, true); - return { channelIsArchived: isArchivedChannel(channel), components: state.plugins.components, @@ -129,8 +126,7 @@ function makeMapStateToProps() { isCollapsedThreadsEnabled: collapsedThreads, threadReplyCount, isMobileView: getIsMobileView(state), - showForwardPostNewLabel, - timezone: getCurrentUserTimezone(state), + timezone: getCurrentTimezone(state), isMilitaryTime, }; }; diff --git a/webapp/channels/src/components/drafts/draft_actions/__snapshots__/delete_draft_modal.test.tsx.snap b/webapp/channels/src/components/drafts/draft_actions/__snapshots__/delete_draft_modal.test.tsx.snap index 092e043646..bcffa0da8f 100644 --- a/webapp/channels/src/components/drafts/draft_actions/__snapshots__/delete_draft_modal.test.tsx.snap +++ b/webapp/channels/src/components/drafts/draft_actions/__snapshots__/delete_draft_modal.test.tsx.snap @@ -4,6 +4,7 @@ exports[`components/drafts/draft_actions/delete_draft_modal should have called o { - const team = {name: 'team_name'}; + const team: Team = {id: 'team_id', + create_at: 0, + update_at: 0, + delete_at: 0, + display_name: 'team_name', + name: 'team_name', + description: 'team_description', + email: 'team_email', + type: 'I', + company_name: 'team_company_name', + allowed_domains: 'team_allowed_domains', + invite_id: 'team_invite_id', + allow_open_invite: false, + scheme_id: 'team_scheme_id', + group_constrained: false, + }; const header = {id: 'header_id', defaultMessage: 'Header'}; const footer = {id: 'footer_id', defaultMessage: 'Footer'}; const loading = {id: 'loading_id', defaultMessage: 'Loading'}; @@ -16,6 +34,15 @@ describe('components/integrations/AbstractIncomingWebhook', () => { display_name: 'testIncomingWebhook', channel_id: '88cxd9wpzpbpfp8pad78xj75pr', description: 'testing', + id: 'test_id', + team_id: 'test_team_id', + create_at: 0, + update_at: 0, + delete_at: 0, + user_id: 'test_user_id', + username: '', + icon_url: '', + channel_locked: false, }; const enablePostUsernameOverride = true; const enablePostIconOverride = true; @@ -104,7 +131,7 @@ describe('components/integrations/AbstractIncomingWebhook', () => { }; const wrapper = shallow(); - wrapper.find('#channelId').simulate('change', evt); + wrapper.find(ChannelSelect).simulate('change', evt); expect(wrapper.state('channelId')).toBe(newChannelId); }); diff --git a/webapp/channels/src/components/integrations/abstract_incoming_webhook.jsx b/webapp/channels/src/components/integrations/abstract_incoming_webhook.tsx similarity index 79% rename from webapp/channels/src/components/integrations/abstract_incoming_webhook.jsx rename to webapp/channels/src/components/integrations/abstract_incoming_webhook.tsx index 498d811c99..af43262239 100644 --- a/webapp/channels/src/components/integrations/abstract_incoming_webhook.jsx +++ b/webapp/channels/src/components/integrations/abstract_incoming_webhook.tsx @@ -1,87 +1,100 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import PropTypes from 'prop-types'; -import React from 'react'; -import {FormattedMessage} from 'react-intl'; +import React, {ChangeEventHandler, FormEvent, MouseEvent, PureComponent} from 'react'; +import {FormattedMessage, MessageDescriptor} from 'react-intl'; import {Link} from 'react-router-dom'; import BackstageHeader from 'components/backstage/components/backstage_header'; import ChannelSelect from 'components/channel_select'; import FormError from 'components/form_error'; import SpinnerButton from 'components/spinner_button'; +import {Team} from '@mattermost/types/teams'; import {localizeMessage} from 'utils/utils'; +import {IncomingWebhook} from '@mattermost/types/integrations'; -export default class AbstractIncomingWebhook extends React.PureComponent { - static propTypes = { +interface State { + displayName: string; + description: string; + channelId: string; + channelLocked: boolean; + username: string; + iconURL: string; + saving: boolean; + serverError: string; + clientError: JSX.Element | null; +} - /** - * The current team - */ - team: PropTypes.object.isRequired, +interface Props { - /** - * The header text to render, has id and defaultMessage - */ - header: PropTypes.object.isRequired, + /** + * The current team + */ + team: Team; - /** - * The footer text to render, has id and defaultMessage - */ - footer: PropTypes.object.isRequired, + /** + * The header text to render, has id and defaultMessage + */ + header: MessageDescriptor; - /** - * The spinner loading text to render, has id and defaultMessage - */ - loading: PropTypes.object.isRequired, + /** + * The footer text to render, has id and defaultMessage + */ + footer: MessageDescriptor; - /** - * The server error text after a failed action - */ - serverError: PropTypes.string.isRequired, + /** + * The spinner loading text to render, has id and defaultMessage + */ + loading: MessageDescriptor; - /** - * The hook used to set the initial state - */ - initialHook: PropTypes.object, + /** + * The server error text after a failed action + */ + serverError: string; - /** - * Whether to allow configuration of the default post username. - */ - enablePostUsernameOverride: PropTypes.bool.isRequired, + /** + * The hook used to set the initial state + */ + initialHook?: IncomingWebhook | Record; - /** - * Whether to allow configuration of the default post icon. - */ - enablePostIconOverride: PropTypes.bool.isRequired, + /** + * Whether to allow configuration of the default post username. + */ + enablePostUsernameOverride: boolean; - /** - * The async function to run when the action button is pressed - */ - action: PropTypes.func.isRequired, - } + /** + * Whether to allow configuration of the default post icon. + */ + enablePostIconOverride: boolean; - constructor(props) { + /** + * The async function to run when the action button is pressed + */ + action: (hook: IncomingWebhook) => Promise; +} + +export default class AbstractIncomingWebhook extends PureComponent { + constructor(props: Props | Readonly) { super(props); this.state = this.getStateFromHook(this.props.initialHook || {}); } - getStateFromHook = (hook) => { + getStateFromHook = (hook: IncomingWebhook | Record) => { return { - displayName: hook.display_name || '', - description: hook.description || '', - channelId: hook.channel_id || '', - channelLocked: hook.channel_locked || false, - username: hook.username || '', - iconURL: hook.icon_url || '', + displayName: hook?.display_name || '', + description: hook?.description || '', + channelId: hook?.channel_id || '', + channelLocked: hook?.channel_locked || false, + username: hook?.username || '', + iconURL: hook?.icon_url || '', saving: false, serverError: '', clientError: null, }; } - handleSubmit = (e) => { + handleSubmit = (e: MouseEvent | FormEvent) => { e.preventDefault(); if (this.state.saving) { @@ -91,7 +104,7 @@ export default class AbstractIncomingWebhook extends React.PureComponent { this.setState({ saving: true, serverError: '', - clientError: '', + clientError: null, }); if (!this.state.channelId) { @@ -115,50 +128,56 @@ export default class AbstractIncomingWebhook extends React.PureComponent { description: this.state.description, username: this.state.username, icon_url: this.state.iconURL, + id: this.props.initialHook?.id || '', + create_at: this.props.initialHook?.create_at || 0, + update_at: this.props.initialHook?.update_at || 0, + delete_at: this.props.initialHook?.delete_at || 0, + team_id: this.props.initialHook?.team_id || '', + user_id: this.props.initialHook?.user_id || '', }; this.props.action(hook).then(() => this.setState({saving: false})); } - updateDisplayName = (e) => { + updateDisplayName: ChangeEventHandler = (e) => { this.setState({ displayName: e.target.value, }); } - updateDescription = (e) => { + updateDescription: ChangeEventHandler = (e) => { this.setState({ description: e.target.value, }); } - updateChannelId = (e) => { + updateChannelId: ChangeEventHandler = (e) => { this.setState({ channelId: e.target.value, }); } - updateChannelLocked = (e) => { + updateChannelLocked: ChangeEventHandler = (e) => { this.setState({ channelLocked: e.target.checked, }); } - updateUsername = (e) => { + updateUsername: ChangeEventHandler = (e) => { this.setState({ username: e.target.value, }); } - updateIconURL = (e) => { + updateIconURL: ChangeEventHandler = (e) => { this.setState({ iconURL: e.target.value, }); } render() { - var headerToRender = this.props.header; - var footerToRender = this.props.footer; + const headerToRender = this.props.header; + const footerToRender = this.props.footer; return (
@@ -177,7 +196,7 @@ export default class AbstractIncomingWebhook extends React.PureComponent {
this.handleSubmit(e)} >
- - + + `; -exports[`components/marketplace/ MarketplaceModal should render with plugins installed 1`] = ` - - + + + + +
- - + + +`; + +exports[`components/marketplace/ should render with plugins available 1`] = ` + + +
+ +
+

+ App Marketplace +

+
+ + } + inputSize="large" + name="searchMarketplaceTextbox" + onChange={[Function]} + onClear={[Function]} + placeholder="Search marketplace" + type="text" + useLegend={false} + value="" + /> +
+ +
+ + + + + + + + +
+
+ + + +
+
+
+`; + +exports[`components/marketplace/ should render with plugins installed 1`] = ` + + +
+ +
+

+ App Marketplace +

+
+ + } + inputSize="large" + name="searchMarketplaceTextbox" + onChange={[Function]} + onClear={[Function]} + placeholder="Search marketplace" + type="text" + useLegend={false} + value="" + /> +
+ +
+ + + + + + + + +
+
+ + + +
+
+
`; diff --git a/webapp/channels/src/components/plugin_marketplace/index.ts b/webapp/channels/src/components/plugin_marketplace/index.ts deleted file mode 100644 index b004efae32..0000000000 --- a/webapp/channels/src/components/plugin_marketplace/index.ts +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {connect} from 'react-redux'; -import {bindActionCreators, Dispatch, ActionCreatorsMapObject} from 'redux'; - -import {GenericAction, ActionFunc} from 'mattermost-redux/types/actions'; - -import {GlobalState} from 'types/store'; -import {getListing, getInstalledListing} from 'selectors/views/marketplace'; -import {setFirstAdminVisitMarketplaceStatus} from 'mattermost-redux/actions/general'; -import {getPluginStatuses} from 'mattermost-redux/actions/admin'; -import {getFirstAdminVisitMarketplaceStatus} from 'mattermost-redux/selectors/entities/general'; - -import {makeAsyncComponent} from 'components/async_load'; - -import {isModalOpen} from 'selectors/views/modals'; -import {ModalIdentifiers} from 'utils/constants'; -import {getSiteURL} from 'utils/url'; - -import {closeModal} from 'actions/views/modals'; -import {fetchListing, filterListing} from 'actions/marketplace'; - -const MarketplaceModal = makeAsyncComponent('MarketplaceModal', React.lazy(() => import('./marketplace_modal'))); - -function mapStateToProps(state: GlobalState) { - return { - show: isModalOpen(state, ModalIdentifiers.PLUGIN_MARKETPLACE), - listing: getListing(state), - installedListing: getInstalledListing(state), - siteURL: getSiteURL(), - pluginStatuses: state.entities.admin.pluginStatuses, - firstAdminVisitMarketplaceStatus: getFirstAdminVisitMarketplaceStatus(state), - }; -} - -type Actions = { - closeModal(): void; - fetchListing(localOnly?: boolean): Promise<{error?: Error}>; - filterListing(filter: string): Promise<{error?: Error}>; - setFirstAdminVisitMarketplaceStatus(): Promise; - getPluginStatuses(): Promise; -} - -function mapDispatchToProps(dispatch: Dispatch) { - return { - actions: bindActionCreators, Actions>({ - closeModal: () => closeModal(ModalIdentifiers.PLUGIN_MARKETPLACE), - fetchListing, - filterListing, - setFirstAdminVisitMarketplaceStatus, - getPluginStatuses, - }, dispatch), - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(MarketplaceModal); diff --git a/webapp/channels/src/components/plugin_marketplace/marketplace_item/marketplace_item_app/__snapshots__/marketplace_item_app.test.tsx.snap b/webapp/channels/src/components/plugin_marketplace/marketplace_item/marketplace_item_app/__snapshots__/marketplace_item_app.test.tsx.snap index 1ec6de8134..7fb956f716 100644 --- a/webapp/channels/src/components/plugin_marketplace/marketplace_item/marketplace_item_app/__snapshots__/marketplace_item_app.test.tsx.snap +++ b/webapp/channels/src/components/plugin_marketplace/marketplace_item/marketplace_item_app/__snapshots__/marketplace_item_app.test.tsx.snap @@ -11,7 +11,7 @@ exports[`components/MarketplaceItemApp MarketplaceItem should render 1`] = ` } button={
`; diff --git a/webapp/channels/src/components/plugin_marketplace/marketplace_list/marketplace_list.test.tsx b/webapp/channels/src/components/plugin_marketplace/marketplace_list/marketplace_list.test.tsx index 641b2bec20..4925c36056 100644 --- a/webapp/channels/src/components/plugin_marketplace/marketplace_list/marketplace_list.test.tsx +++ b/webapp/channels/src/components/plugin_marketplace/marketplace_list/marketplace_list.test.tsx @@ -8,8 +8,7 @@ import {AuthorType, MarketplacePlugin, ReleaseStage} from '@mattermost/types/mar import MarketplaceItem from '../marketplace_item/marketplace_item_plugin'; -import MarketplaceList from './marketplace_list'; -import NavigationRow from './navigation_row'; +import MarketplaceList, {ITEMS_PER_PAGE} from './marketplace_list'; describe('components/marketplace/marketplace_list', () => { const samplePlugin: MarketplacePlugin = { @@ -28,8 +27,20 @@ describe('components/marketplace/marketplace_list', () => { installed_version: '', }; - it('should render with multiple plugins', () => { - const wrapper = shallow( + it('should render default', () => { + const wrapper = shallow( + , + ); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should render page with ITEMS_PER_PAGE plugins', () => { + const wrapper = shallow( { samplePlugin, samplePlugin, samplePlugin, samplePlugin, samplePlugin, samplePlugin, samplePlugin, ]} + page={0} + noResultsMessage='' />, ); - expect(wrapper).toMatchSnapshot(); - expect(wrapper.state().page).toEqual(0); - expect(wrapper.find(MarketplaceItem)).toHaveLength(15); - expect(wrapper.find(NavigationRow)).toHaveLength(1); - expect(wrapper.find(NavigationRow).props().page).toEqual(0); - expect(wrapper.find(NavigationRow).props().total).toEqual(17); - expect(wrapper.find(NavigationRow).props().maximumPerPage).toEqual(15); + expect(wrapper.find(MarketplaceItem)).toHaveLength(ITEMS_PER_PAGE); }); - it('should set page to 0 when list of plugins changed', () => { - const wrapper = shallow( + it('should render no results', () => { + const wrapper = shallow( , ); - wrapper.setState({page: 10}); - wrapper.setProps({listing: [samplePlugin]}); - - expect(wrapper.state().page).toEqual(0); + expect(wrapper.find('.icon__plugin').length).toEqual(1); + expect(wrapper.find('.no_plugins__message').length).toEqual(1); + expect(wrapper.find('.no_plugins__action').length).toEqual(1); }); }); diff --git a/webapp/channels/src/components/plugin_marketplace/marketplace_list/marketplace_list.tsx b/webapp/channels/src/components/plugin_marketplace/marketplace_list/marketplace_list.tsx index 1da22da6a3..8c002501ca 100644 --- a/webapp/channels/src/components/plugin_marketplace/marketplace_list/marketplace_list.tsx +++ b/webapp/channels/src/components/plugin_marketplace/marketplace_list/marketplace_list.tsx @@ -1,107 +1,116 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React from 'react'; +import React, {useCallback, useMemo} from 'react'; +import {useIntl} from 'react-intl'; import type {MarketplaceApp, MarketplacePlugin} from '@mattermost/types/marketplace'; import {isPlugin, getName} from 'mattermost-redux/utils/marketplace'; +import PluginIcon from 'components/widgets/icons/plugin_icon'; + import MarketplaceItemPlugin from '../marketplace_item/marketplace_item_plugin'; import MarketplaceItemApp from '../marketplace_item/marketplace_item_app'; -import NavigationRow from './navigation_row'; - -const ITEMS_PER_PAGE = 15; +export const ITEMS_PER_PAGE = 15; type MarketplaceListProps = { listing: Array; -}; - -type MarketplaceListState = { page: number; + noResultsMessage: string; + noResultsAction?: { + label: string; + onClick: () => void; + }; + filter?: string; + listRef?: React.RefObject; }; -export default class MarketplaceList extends React.PureComponent { - static getDerivedStateFromProps(props: MarketplaceListProps, state: MarketplaceListState): MarketplaceListState | null { - if (state.page > 0 && props.listing.length < ITEMS_PER_PAGE) { - return {page: 0}; +const MarketplaceList = ({ + listing, + page, + noResultsMessage, + noResultsAction, + filter, + listRef, +}: MarketplaceListProps) => { + const {formatMessage} = useIntl(); + + const pageItems = useMemo(() => { + if (listing.length === 0) { + return []; } - return null; - } - - constructor(props: MarketplaceListProps) { - super(props); - - this.state = { - page: 0, - }; - } - - nextPage = (): void => { - this.setState((state) => ({ - page: state.page + 1, - })); - }; - - previousPage = (): void => { - this.setState((state) => ({ - page: state.page - 1, - })); - }; - - render(): JSX.Element { - const pageStart = this.state.page * ITEMS_PER_PAGE; + const pageStart = page * ITEMS_PER_PAGE; const pageEnd = pageStart + ITEMS_PER_PAGE; - this.props.listing.sort((a, b) => { - return getName(a).localeCompare(getName(b)); - }); + return [...listing]. + sort((a, b) => getName(a).localeCompare(getName(b))). + slice(pageStart, pageEnd). + map((i) => ( + isPlugin(i) ? ( + + ) : ( + + ) + )); + }, [listing, page]); - const itemsToDisplay = this.props.listing.slice(pageStart, pageEnd); + const getNoResultsMessage = useCallback(() => ( + filter ? ( + formatMessage( + {id: 'marketplace_modal_list.no_plugins_filter', defaultMessage: 'No results for "{filter}"'}, + {filter}, + ) + ) : ( + noResultsMessage + ) + ), [filter, noResultsMessage]); - return ( -
- {itemsToDisplay.map((i) => { - if (isPlugin(i)) { - return ( - - ); - } - - return ( - - ); - }) - } - + return (listing.length === 0 ? ( +
+ +
+ {getNoResultsMessage()}
- ); - } -} + {noResultsAction && ( + + )} +
+ ) : ( +
+ {pageItems} +
+ )); +}; + +export default MarketplaceList; diff --git a/webapp/channels/src/components/plugin_marketplace/marketplace_list/navigation_row/__snapshots__/navigation_row.test.tsx.snap b/webapp/channels/src/components/plugin_marketplace/marketplace_list/navigation_row/__snapshots__/navigation_row.test.tsx.snap deleted file mode 100644 index f08718159d..0000000000 --- a/webapp/channels/src/components/plugin_marketplace/marketplace_list/navigation_row/__snapshots__/navigation_row.test.tsx.snap +++ /dev/null @@ -1,157 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`components/marketplace/navigation_row should not render any buttons 1`] = ` -
-
-
- -
-
-
-`; - -exports[`components/marketplace/navigation_row should render next and previous buttons 1`] = ` -
-
- -
-
- -
-
- -
-
-`; - -exports[`components/marketplace/navigation_row should render only next button 1`] = ` -
-
-
- -
-
- -
-
-`; - -exports[`components/marketplace/navigation_row should render only previous button 1`] = ` -
-
- -
-
- -
-
-
-`; diff --git a/webapp/channels/src/components/plugin_marketplace/marketplace_list/navigation_row/index.ts b/webapp/channels/src/components/plugin_marketplace/marketplace_list/navigation_row/index.ts deleted file mode 100644 index 320c07dca9..0000000000 --- a/webapp/channels/src/components/plugin_marketplace/marketplace_list/navigation_row/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {connect} from 'react-redux'; - -import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {GlobalState} from '@mattermost/types/store'; - -import NavigationRow from './navigation_row'; - -function mapStateToProps(state: GlobalState) { - return { - theme: getTheme(state), - }; -} - -export default connect(mapStateToProps)(NavigationRow); diff --git a/webapp/channels/src/components/plugin_marketplace/marketplace_list/navigation_row/navigation_button.tsx b/webapp/channels/src/components/plugin_marketplace/marketplace_list/navigation_row/navigation_button.tsx deleted file mode 100644 index 8433739030..0000000000 --- a/webapp/channels/src/components/plugin_marketplace/marketplace_list/navigation_row/navigation_button.tsx +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {FormattedMessage} from 'react-intl'; - -type NavigationButtonProps = { - onClick: (event: React.MouseEvent) => void; - messageId: string; - defaultMessage: string; -}; - -export default class NavigationButton extends React.PureComponent { - onClick = (event: React.MouseEvent): void => { - event.preventDefault(); - this.props.onClick(event); - }; - - render(): JSX.Element { - const {onClick, messageId, defaultMessage} = this.props; - return ( - - ); - } -} diff --git a/webapp/channels/src/components/plugin_marketplace/marketplace_list/navigation_row/navigation_row.test.tsx b/webapp/channels/src/components/plugin_marketplace/marketplace_list/navigation_row/navigation_row.test.tsx deleted file mode 100644 index 4633263c55..0000000000 --- a/webapp/channels/src/components/plugin_marketplace/marketplace_list/navigation_row/navigation_row.test.tsx +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {shallow} from 'enzyme'; - -import {Theme} from 'mattermost-redux/selectors/entities/preferences'; - -import NavigationRow, {NavigationRowProps} from './navigation_row'; - -describe('components/marketplace/navigation_row', () => { - const baseProps: NavigationRowProps = { - page: 0, - total: 32, - maximumPerPage: 15, - onNextPageButtonClick: jest.fn(), - onPreviousPageButtonClick: jest.fn(), - theme: {centerChannelColor: '#fff'} as Theme, - }; - - it('should render only next button', () => { - const wrapper = shallow( - , - ); - - expect(wrapper).toMatchSnapshot(); - expect(wrapper.find('NavigationButton')).toHaveLength(1); - - wrapper.find('NavigationButton').simulate('click', {preventDefault: jest.fn}); - - expect(wrapper.instance().props.onNextPageButtonClick).toHaveBeenCalledTimes(1); - expect(wrapper.instance().props.onPreviousPageButtonClick).toHaveBeenCalledTimes(0); - }); - - it('should render next and previous buttons', () => { - const props = {...baseProps, page: 1}; - const wrapper = shallow( - , - ); - - expect(wrapper).toMatchSnapshot(); - expect(wrapper.find('NavigationButton')).toHaveLength(2); - - wrapper.find('NavigationButton').at(0).simulate('click', {preventDefault: jest.fn}); - wrapper.find('NavigationButton').at(1).simulate('click', {preventDefault: jest.fn}); - - expect(wrapper.instance().props.onNextPageButtonClick).toHaveBeenCalledTimes(1); - expect(wrapper.instance().props.onPreviousPageButtonClick).toHaveBeenCalledTimes(1); - }); - - it('should render only previous button', () => { - const props = {...baseProps, page: 2}; - const wrapper = shallow( - , - ); - expect(wrapper).toMatchSnapshot(); - expect(wrapper.find('NavigationButton')).toHaveLength(1); - - wrapper.find('NavigationButton').simulate('click', {preventDefault: jest.fn}); - - expect(wrapper.instance().props.onNextPageButtonClick).toHaveBeenCalledTimes(0); - expect(wrapper.instance().props.onPreviousPageButtonClick).toHaveBeenCalledTimes(1); - }); - - it('should not render any buttons', () => { - const props = {...baseProps, page: 0, total: 15}; - const wrapper = shallow( - , - ); - expect(wrapper).toMatchSnapshot(); - expect(wrapper.find('NavigationButton')).toHaveLength(0); - }); -}); diff --git a/webapp/channels/src/components/plugin_marketplace/marketplace_list/navigation_row/navigation_row.tsx b/webapp/channels/src/components/plugin_marketplace/marketplace_list/navigation_row/navigation_row.tsx deleted file mode 100644 index 98cff683d0..0000000000 --- a/webapp/channels/src/components/plugin_marketplace/marketplace_list/navigation_row/navigation_row.tsx +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import {FormattedMessage} from 'react-intl'; - -import {Theme} from 'mattermost-redux/selectors/entities/preferences'; -import {changeOpacity, makeStyleFromTheme} from 'mattermost-redux/utils/theme_utils'; - -import NavigationButton from './navigation_button'; - -export type NavigationRowProps = { - page: number; - total: number; - maximumPerPage: number; - onNextPageButtonClick: (event: React.MouseEvent) => void; - onPreviousPageButtonClick: (event: React.MouseEvent) => void; - theme: Theme; -}; - -export default class NavigationRow extends React.PureComponent { - canShowNextButton = (): boolean => { - const {page, maximumPerPage, total} = this.props; - const totalPages = Math.trunc((total - 1) / maximumPerPage); - - return totalPages > page; - }; - - renderCount = (): JSX.Element => { - const {page, total, maximumPerPage} = this.props; - const startCount = page * maximumPerPage; - const endCount = Math.min(startCount + maximumPerPage, total); - - return ( - - ); - }; - - render(): JSX.Element { - const style = getStyle(this.props.theme); - - return ( -
-
- {(this.props.page > 0) && ( - - )} -
-
- {this.renderCount()} -
-
- {this.canShowNextButton() && ( - - )} -
-
- ); - } -} - -const getStyle = makeStyleFromTheme((theme) => { - return { - count: { - color: changeOpacity(theme.centerChannelColor, 0.6), - }, - }; -}); diff --git a/webapp/channels/src/components/plugin_marketplace/marketplace_modal.scss b/webapp/channels/src/components/plugin_marketplace/marketplace_modal.scss index f2f3dec629..5a0495c0fc 100644 --- a/webapp/channels/src/components/plugin_marketplace/marketplace_modal.scss +++ b/webapp/channels/src/components/plugin_marketplace/marketplace_modal.scss @@ -1,21 +1,11 @@ @import 'utils/variables'; +@import 'utils/mixins'; -.modal-marketplace { - display: flex; - width: 100%; - height: 100%; - flex-direction: column; - align-items: center; - padding-top: 50px; - color: var(--center-channel-color); - font-size: 16px; - - @media (max-width: 768px) { - padding-right: 15px; - padding-left: 15px; - } +.marketplace-modal { + width: 800px; div.navigation-row { + overflow: unset; margin-top: 10px; div { @@ -29,7 +19,7 @@ } div.count { - padding-top: 8px; + padding: 9px 15px; } } @@ -45,33 +35,47 @@ } .nav-tabs { - font-size: 14px; + padding: 0 32px; + margin: 0 0 8px; - > li { - > a { - padding: 10px 16px; + li { + margin-right: 0; + + a { + padding: 13px 12px; + border: none; background: transparent; + color: rgba(var(--center-channel-color-rgb), 0.64); + font-size: 14px; + font-weight: 600; + line-height: 20px; transition: all 0.15s ease; &:hover, &:active, - &:focus { + &:focus, + &:focus-within { + border: none; background: transparent; + border-radius: none; color: var(--center-channel-color); } } - &.active > a { - color: var(--center-channel-color); + &.active { + border-bottom: 2px solid var(--denim-button-bg); + + a { + color: var(--denim-button-bg); + } + } + + &:not(:first-child) { + margin-left: 8px; } } } - h1 { - margin: 8px 0 24px; - font-size: 28px; - } - h2 { font-weight: 300; @@ -81,8 +85,14 @@ } .more-modal__list { + height: 390px; + margin: 0 6px 8px 12px; + overflow-y: scroll; + .more-modal__row { - align-items: normal; + min-height: 80px; + padding: 16px 20px; + border-bottom: none; .marketplace__tag { margin-left: 6px; @@ -94,25 +104,91 @@ margin: 10px 10px 0 0; font-size: 0.9em; } + + .more-modal__details { + padding-left: 16px; + + .more-modal__row--link { + color: var(--center-channel-color); + font-size: 16px; + font-weight: 600; + line-height: 24px; + } + + .more-modal__description { + margin: 2px 0 0; + color: rgba(var(--center-channel-color-rgb), 0.64); + font-size: 14px; + font-weight: 400; + line-height: 20px; + } + } + + .more-modal__actions { + padding-left: 16px; + margin: 0; + + .plugin-configure, + .app-installed { + @include secondary-button; + @include button-medium; + } + + .plugin-install, + .app-install { + @include primary-button; + @include button-medium; + } + + a { + &:hover, + &:focus { + text-decoration: none; + } + } + } + + &:hover, + &:focus, + &:focus-within { + background-color: rgba(var(--center-channel-color-rgb), 0.08); + } } - .more-modal__description { - margin: 2px 0 0; - font-size: 0.9em; - } + .icon__plugin { + display: flex; + height: 48px; + flex: 0 0 48px; + align-items: center; + justify-content: center; + background-color: $white; + border-radius: 50%; - padding-bottom: 80px; + svg, + img { + width: 48px; + height: 48px; + } + + svg { + fill: var(--button-bg); + } + + img { + border-radius: 4px; + } + } } - .search_input { - width: 720px; - height: 40px; - flex: 1; - margin-top: 28px; - margin-right: 16px; - margin-bottom: 20px; - margin-left: 16px; - box-shadow: none; + .marketplace-modal-search { + padding: 24px 0 0; + + .search_input { + width: 100%; + border: 0 !important; + border-radius: 0 !important; + box-shadow: none; + } } .btn { @@ -130,8 +206,9 @@ .tabs { display: flex; - width: 720px; + width: 100%; flex-direction: column; + margin-top: 12px; } .subtitle { @@ -160,67 +237,41 @@ } } - .icon__plugin { + .no_plugins { display: flex; - height: 42px; - flex: 0 0 42px; + height: 390px; + flex-flow: column; align-items: center; justify-content: center; - margin-right: 4px; - border-radius: 50%; + margin-bottom: 8px; - svg { - width: 32px; - height: 32px; - fill: var(--button-bg); + &__message { + margin-top: 20px; + color: var(--center-channel-color); + font-size: 20px; + font-weight: 600; + line-height: 28px; } - } - .icon__plugin--background { - padding: 6px; - background-color: $white; + &__action { + @include primary-button; + @include button-medium; - svg { - width: 24px; - height: 24px; + margin-top: 30px; } - } - .no_plugins_div { - text-align: center; + .icon__plugin { + svg { + fill: var(--button-bg); + } + } } .item_error { - background-color: rgba(var(--error-text-rgb), 0.08); + background-color: rgba(var(--error-text-color-rgb), 0.08); } - .error_text { - color: var(--error-text); - opacity: 1; - } -} - -.error-bar { - position: fixed; - z-index: 8; - top: 0; - overflow: hidden; - width: 100%; - min-height: $announcement-bar-height; - max-height: $announcement-bar-height; - padding: 5px 30px; - background-color: var(--center-channel-bg); - color: var(--error-text); - - .error-bar__content { - position: absolute; - top: 0; - left: 0; - display: flex; - width: 100%; - height: 100%; - align-items: center; - justify-content: center; - background-color: rgba(var(--error-text-rgb), 0.12); + .loading { + height: 390px; } } diff --git a/webapp/channels/src/components/plugin_marketplace/marketplace_modal.test.tsx b/webapp/channels/src/components/plugin_marketplace/marketplace_modal.test.tsx index 3e2938603c..c4c2181182 100644 --- a/webapp/channels/src/components/plugin_marketplace/marketplace_modal.test.tsx +++ b/webapp/channels/src/components/plugin_marketplace/marketplace_modal.test.tsx @@ -5,19 +5,21 @@ import React from 'react'; import {shallow} from 'enzyme'; import {AuthorType, MarketplacePlugin, ReleaseStage} from '@mattermost/types/marketplace'; -import type {PluginStatusRedux} from '@mattermost/types/plugins'; -import {trackEvent} from 'actions/telemetry_actions.jsx'; +import {ActionFunc} from 'mattermost-redux/types/actions'; -import MarketplaceModal, {AllListing, InstalledListing, MarketplaceModalProps} from './marketplace_modal'; +import {GlobalState} from 'types/store'; +import {ModalIdentifiers} from 'utils/constants'; -jest.mock('actions/telemetry_actions.jsx', () => { - const original = jest.requireActual('actions/telemetry_actions.jsx'); - return { - ...original, - trackEvent: jest.fn(), - }; -}); +import MarketplaceModal, {OpenedFromType} from './marketplace_modal'; + +let mockState: GlobalState; + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux') as typeof import('react-redux'), + useSelector: (selector: (state: typeof mockState) => unknown) => selector(mockState), + useDispatch: jest.fn(() => (action: ActionFunc) => action), +})); describe('components/marketplace/', () => { const samplePlugin: MarketplacePlugin = { @@ -52,161 +54,106 @@ describe('components/marketplace/', () => { installed_version: '1.0.3', }; - describe('AllListing', () => { - it('should render with no plugins', () => { - const wrapper = shallow( - , - ); - expect(wrapper).toMatchSnapshot(); - }); + const defaultProps = { + openedFrom: 'actions_menu' as OpenedFromType, + }; - it('should render with one plugin', () => { - const wrapper = shallow( - , - ); - expect(wrapper).toMatchSnapshot(); - }); - - it('should render with plugins', () => { - const wrapper = shallow( - , - ); - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('InstalledPlugins', () => { - const baseProps = { - changeTab: jest.fn(), - }; - - it('should render with no plugins', () => { - const wrapper = shallow( - , - ); - expect(wrapper).toMatchSnapshot(); - }); - - it('should render with one plugin', () => { - const wrapper = shallow( - , - ); - expect(wrapper).toMatchSnapshot(); - }); - - it('should render with multiple plugins', () => { - const wrapper = shallow( - , - ); - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('MarketplaceModal', () => { - const baseProps: MarketplaceModalProps = { - show: true, - listing: [samplePlugin], - installedListing: [], - pluginStatuses: {}, - siteURL: 'http://example.com', - firstAdminVisitMarketplaceStatus: false, - openedFrom: 'actions_menu', - actions: { - closeModal: jest.fn(), - fetchListing: jest.fn(() => { - return Promise.resolve({}); - }), - filterListing: jest.fn(() => { - return Promise.resolve({}); - }), - setFirstAdminVisitMarketplaceStatus: jest.fn(), - getPluginStatuses: jest.fn(), + beforeEach(() => { + mockState = { + views: { + modals: { + modalState: { + [ModalIdentifiers.PLUGIN_MARKETPLACE]: { + open: true, + }, + }, + }, + marketplace: { + plugins: [], + apps: [], + }, }, - }; + entities: { + general: { + firstAdminCompleteSetup: false, + }, + admin: { + pluginStatuses: {}, + }, + }, + } as unknown as GlobalState; + }); - test('should render with no plugins installed', () => { - const wrapper = shallow( - , - ); - expect(wrapper).toMatchSnapshot(); - }); + test('should render default', () => { + const wrapper = shallow( + , + ); - test('should render with plugins installed', () => { - const props = { - ...baseProps, - plugins: [ - ...baseProps.listing, - sampleInstalledPlugin, - ], - installedListing: [ - sampleInstalledPlugin, - ], - }; + expect(wrapper.shallow()).toMatchSnapshot(); + }); - const wrapper = shallow( - , - ); + test('should render with no plugins available', () => { + const setState = jest.fn(); + const useStateSpy = jest.spyOn(React, 'useState'); + useStateSpy.mockImplementationOnce(() => [false, setState]); - expect(wrapper).toMatchSnapshot(); - }); + const wrapper = shallow( + , + ); - test('should fetch plugins when plugin status is changed', () => { - const fetchListing = baseProps.actions.fetchListing; - const wrapper = shallow(); + wrapper.update(); - expect(fetchListing).toBeCalledTimes(1); - wrapper.setProps({...baseProps}); - expect(fetchListing).toBeCalledTimes(1); + expect(wrapper.shallow()).toMatchSnapshot(); + }); - const status = { - id: 'test', - } as PluginStatusRedux; - wrapper.setProps({...baseProps, pluginStatuses: {test: status}}); - expect(fetchListing).toBeCalledTimes(2); - }); + test('should render with plugins available', () => { + const setState = jest.fn(); + const useStateSpy = jest.spyOn(React, 'useState'); + useStateSpy.mockImplementationOnce(() => [false, setState]); - test('should render with error banner', () => { - const wrapper = shallow( - , - ); + mockState.views.marketplace.plugins = [ + samplePlugin, + ]; - wrapper.setState({serverError: {name: 'some.error', message: 'Error test'}}); + const wrapper = shallow( + , + ); - expect(wrapper).toMatchSnapshot(); - }); + wrapper.update(); - test('Should call for track event when searching', () => { - const wrapper = shallow( - , - ); + expect(wrapper.shallow()).toMatchSnapshot(); + }); - wrapper.setState({filter: 'nps'}); - wrapper.instance().doSearch(); + test('should render with plugins installed', () => { + const setState = jest.fn(); + const useStateSpy = jest.spyOn(React, 'useState'); + useStateSpy.mockImplementationOnce(() => [false, setState]); - expect(trackEvent).toHaveBeenCalledWith('plugins', 'ui_marketplace_opened', {from: 'actions_menu'}); - expect(trackEvent).toHaveBeenCalledWith('plugins', 'ui_marketplace_search', {filter: 'nps'}); - }); + mockState.views.marketplace.plugins = [ + samplePlugin, + sampleInstalledPlugin, + ]; - test('Should call for opened track event on mount', () => { - const openedFrom = 'actions_menu'; + const wrapper = shallow( + , + ); - shallow( - , - ); + wrapper.update(); - expect(trackEvent).toHaveBeenCalledWith('plugins', 'ui_marketplace_opened', {from: openedFrom}); - }); + expect(wrapper.shallow()).toMatchSnapshot(); + }); + + test('should render with error banner', () => { + const setState = jest.fn(); + const useStateSpy = jest.spyOn(React, 'useState'); + useStateSpy.mockImplementation(() => [true, setState]); + + const wrapper = shallow( + , + ); + + wrapper.update(); + + expect(wrapper.shallow()).toMatchSnapshot(); }); }); diff --git a/webapp/channels/src/components/plugin_marketplace/marketplace_modal.tsx b/webapp/channels/src/components/plugin_marketplace/marketplace_modal.tsx index 59ac7510ba..062727f9e1 100644 --- a/webapp/channels/src/components/plugin_marketplace/marketplace_modal.tsx +++ b/webapp/channels/src/components/plugin_marketplace/marketplace_modal.tsx @@ -1,283 +1,269 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React from 'react'; -import {FormattedMessage} from 'react-intl'; -import debounce from 'lodash/debounce'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import {Tabs, Tab, SelectCallback} from 'react-bootstrap'; +import {useIntl} from 'react-intl'; +import {useDispatch, useSelector} from 'react-redux'; +import {Link} from 'react-router-dom'; +import debounce from 'lodash/debounce'; -import {PluginStatusRedux} from '@mattermost/types/plugins'; -import type {MarketplaceApp, MarketplacePlugin} from '@mattermost/types/marketplace'; +import {MagnifyIcon} from '@mattermost/compass-icons/components'; -import FullScreenModal from 'components/widgets/modals/full_screen_modal'; -import RootPortal from 'components/root_portal'; -import QuickInput from 'components/quick_input'; -import LocalizedInput from 'components/localized_input/localized_input'; -import PluginIcon from 'components/widgets/icons/plugin_icon'; -import LoadingScreen from 'components/loading_screen'; -import FormattedMarkdownMessage from 'components/formatted_markdown_message'; +import {FooterPagination} from '@mattermost/components'; +import {getPluginStatuses} from 'mattermost-redux/actions/admin'; +import {setFirstAdminVisitMarketplaceStatus} from 'mattermost-redux/actions/general'; +import {getFirstAdminVisitMarketplaceStatus} from 'mattermost-redux/selectors/entities/general'; +import {ActionResult} from 'mattermost-redux/types/actions'; +import {fetchListing, filterListing} from 'actions/marketplace'; import {trackEvent} from 'actions/telemetry_actions.jsx'; -import {t} from 'utils/i18n'; -import {localizeMessage} from 'utils/utils'; +import {closeModal} from 'actions/views/modals'; + +import GenericModal from 'components/generic_modal'; +import LoadingScreen from 'components/loading_screen'; +import Input, {SIZE} from 'components/widgets/inputs/input/input'; + +import {getListing, getInstalledListing} from 'selectors/views/marketplace'; +import {isModalOpen} from 'selectors/views/modals'; +import {GlobalState} from 'types/store'; +import {ModalIdentifiers} from 'utils/constants'; import './marketplace_modal.scss'; -import MarketplaceList from './marketplace_list/marketplace_list'; + +import MarketplaceList, {ITEMS_PER_PAGE} from './marketplace_list/marketplace_list'; const MarketplaceTabs = { - ALL_LISTING: 'allListing', + ALL_LISTING: 'all', INSTALLED_LISTING: 'installed', }; const SEARCH_TIMEOUT_MILLISECONDS = 200; +const linkConsole = (msg: string) => ( + + {msg} + +); + export type OpenedFromType = 'actions_menu' | 'app_bar' | 'channel_header' | 'command' | 'open_plugin_install_post' | 'product_menu'; -type AllListingProps = { - listing: Array; -}; - -// AllListing renders the contents of the all listing tab. -export const AllListing = ({listing}: AllListingProps): JSX.Element => { - if (listing.length === 0) { - return ( -
-
- -
- -
-
- ); - } - - return ; -}; - -type InstalledListingProps = { - installedItems: Array; - changeTab: SelectCallback; -}; - -// InstalledListing renders the contents of the installed listing tab. -export const InstalledListing = ({installedItems, changeTab}: InstalledListingProps): JSX.Element => { - if (installedItems.length === 0) { - return ( -
-
- -
- -
- -
- ); - } - - return ; -}; - -export type MarketplaceModalProps = { - show: boolean; - listing: Array; - installedListing: Array; - siteURL: string; - pluginStatuses?: Record; - firstAdminVisitMarketplaceStatus: boolean; +type MarketplaceModalProps = { openedFrom: OpenedFromType; - actions: { - closeModal: () => void; - fetchListing(localOnly?: boolean): Promise<{error?: Error}>; - filterListing(filter: string): Promise<{error?: Error}>; - setFirstAdminVisitMarketplaceStatus(): Promise; - getPluginStatuses(): Promise; - }; -}; - -type MarketplaceModalState = { - tabKey: unknown; - loading: boolean; - serverError?: Error; - filter: string; -}; - -// MarketplaceModal is the marketplace modal. -export default class MarketplaceModal extends React.PureComponent { - private filterRef: React.RefObject; - - constructor(props: MarketplaceModalProps) { - super(props); - - this.state = { - tabKey: MarketplaceTabs.ALL_LISTING, - loading: true, - serverError: undefined, - filter: '', - }; - - this.filterRef = React.createRef(); - } - - componentDidMount(): void { - trackEvent('plugins', 'ui_marketplace_opened', {from: this.props.openedFrom}); - - this.fetchListing(); - this.props.actions.getPluginStatuses(); - if (!this.props.firstAdminVisitMarketplaceStatus) { - trackEvent('plugins', 'ui_first_admin_visit_marketplace_status'); - - this.props.actions.setFirstAdminVisitMarketplaceStatus(); - } - - this.filterRef.current?.focus(); - } - - componentDidUpdate(prevProps: MarketplaceModalProps): void { - // Automatically refresh the component when a plugin is installed or uninstalled. - if (this.props.pluginStatuses !== prevProps.pluginStatuses) { - this.fetchListing(); - } - } - - fetchListing = async (): Promise => { - const {error} = await this.props.actions.fetchListing(); - this.setState({loading: false, serverError: error}); - } - - close = (): void => { - trackEvent('plugins', 'ui_marketplace_closed'); - this.props.actions.closeModal(); - } - - changeTab: SelectCallback = (tabKey: any): void => { - this.setState({tabKey}); - } - - onInput = (): void => { - if (this.filterRef.current) { - this.setState({filter: this.filterRef.current.value}); - - this.debouncedSearch(); - } - } - - handleClearSearch = (): void => { - if (this.filterRef.current) { - this.filterRef.current.value = ''; - this.setState({filter: this.filterRef.current.value}, this.doSearch); - } - } - - doSearch = async (): Promise => { - trackEvent('plugins', 'ui_marketplace_search', {filter: this.state.filter}); - - const {error} = await this.props.actions.filterListing(this.state.filter); - - this.setState({serverError: error}); - } - - debouncedSearch = debounce(this.doSearch, SEARCH_TIMEOUT_MILLISECONDS); - - render(): JSX.Element { - const input = ( -
-
- -
-
- ); - - let errorBanner = null; - if (this.state.serverError) { - errorBanner = ( -
-
- -
-
- ); - } - - return ( - - - {errorBanner} - - - - ); - } } + +const MarketplaceModal = ({ + openedFrom, +}: MarketplaceModalProps) => { + const dispatch = useDispatch(); + const {formatMessage} = useIntl(); + const listRef = useRef(null); + + const show = useSelector((state: GlobalState) => isModalOpen(state, ModalIdentifiers.PLUGIN_MARKETPLACE)); + const listing = useSelector(getListing); + const installedListing = useSelector(getInstalledListing); + const pluginStatuses = useSelector((state: GlobalState) => state.entities.admin.pluginStatuses); + const hasFirstAdminVisitedMarketplace = useSelector(getFirstAdminVisitMarketplaceStatus); + + const [tabKey, setTabKey] = useState(MarketplaceTabs.ALL_LISTING); + const [filter, setFilter] = useState(''); + const [page, setPage] = useState(0); + const [hasLoaded, setHasLoaded] = useState(false); + const [loading, setLoading] = React.useState(true); + const [serverError, setServerError] = React.useState(false); + + const doFetchListing = useCallback(async () => { + const {error} = await dispatch(fetchListing()) as ActionResult; + + if (error) { + setServerError(true); + } + + setLoading(false); + }, []); + + const doSearch = useCallback(async () => { + trackEvent('plugins', 'ui_marketplace_search', {filter}); + + const {error} = await dispatch(filterListing(filter)) as ActionResult; + + if (error) { + setServerError(true); + } + }, [filter]); + + const debouncedSearch = debounce(doSearch, SEARCH_TIMEOUT_MILLISECONDS); + + useEffect(() => { + async function doFetch() { + await dispatch(getPluginStatuses()); + await doFetchListing(); + setHasLoaded(true); + } + + trackEvent('plugins', 'ui_marketplace_opened', {from: openedFrom}); + + if (!hasFirstAdminVisitedMarketplace) { + trackEvent('plugins', 'ui_first_admin_visit_marketplace_status'); + dispatch(setFirstAdminVisitMarketplaceStatus()); + } + + doFetch(); + }, []); + + useEffect(() => { + if (hasLoaded) { + doFetchListing(); + } + }, [pluginStatuses]); + + useEffect(() => { + if (hasLoaded) { + debouncedSearch(); + setPage(0); + } + }, [filter]); + + const scrollListToTop = useCallback(() => { + if (listRef.current) { + listRef.current.scrollTop = 0; + } + }, []); + + const handleOnClose = () => { + trackEvent('plugins', 'ui_marketplace_closed'); + dispatch(closeModal(ModalIdentifiers.PLUGIN_MARKETPLACE)); + }; + + const handleChangeTab: SelectCallback = useCallback((tabKey) => { + setTabKey(tabKey); + setPage(0); + scrollListToTop(); + }, [scrollListToTop]); + + const handleOnChange = useCallback((event: React.ChangeEvent) => { + setFilter(event.target.value); + }, []); + + const handleOnClear = useCallback(() => { + setFilter(''); + }, []); + + const handleOnNextPage = useCallback(() => { + setPage(page + 1); + scrollListToTop(); + }, [page, scrollListToTop]); + + const handleOnPreviousPage = useCallback(() => { + setPage(page - 1); + scrollListToTop(); + }, [page, scrollListToTop]); + + const handleNoResultsButtonClick = useCallback(() => { + handleChangeTab(MarketplaceTabs.ALL_LISTING); + }, [handleChangeTab]); + + const getHeaderInput = useCallback(() => ( + } + placeholder={formatMessage({id: 'marketplace_modal.search', defaultMessage: 'Search marketplace'})} + useLegend={false} + autoFocus={true} + clearable={true} + value={filter} + onChange={handleOnChange} + onClear={handleOnClear} + /> + ), [filter, handleOnChange, handleOnClear]); + + const getFooterContent = useCallback(() => ( + + ), [installedListing.length, listing.length, page, handleOnNextPage, handleOnPreviousPage, tabKey]); + + return ( + System Console.', + }, + {linkConsole}, + ) + ) : undefined} + show={show} + compassDesign={true} + bodyPadding={false} + footerDivider={true} + onExited={handleOnClose} + footerContent={getFooterContent()} + headerInput={getHeaderInput()} + > + + + {loading ? ( + + ) : ( + + )} + + + + + + + ); +}; + +export default MarketplaceModal; diff --git a/webapp/channels/src/components/post/index.tsx b/webapp/channels/src/components/post/index.tsx index 2bd3324d9f..39c664736f 100644 --- a/webapp/channels/src/components/post/index.tsx +++ b/webapp/channels/src/components/post/index.tsx @@ -4,7 +4,6 @@ import {connect, ConnectedProps} from 'react-redux'; import {AnyAction, bindActionCreators, Dispatch} from 'redux'; -import {showActionsDropdownPulsatingDot} from 'selectors/actions_menu'; import {setActionsMenuInitialisationState} from 'mattermost-redux/actions/preferences'; import {getConfig} from 'mattermost-redux/selectors/entities/general'; import {getPost, makeGetCommentCountForPost, makeIsPostCommentMention, isPostAcknowledgementsEnabled, isPostPriorityEnabled, UserActivityPost} from 'mattermost-redux/selectors/entities/posts'; @@ -13,7 +12,6 @@ import { get, getBool, isCollapsedThreadsEnabled, - onboardingTourTipsEnabled, } from 'mattermost-redux/selectors/entities/preferences'; import {getCurrentTeam, getCurrentTeamId, getTeam, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams'; import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users'; @@ -129,8 +127,7 @@ function makeMapStateToProps() { const user = getUser(state, post.user_id); const isBot = Boolean(user && user.is_bot); const highlightedPostId = getHighlightedPostId(state); - const showActionsMenuPulsatingDot = showActionsDropdownPulsatingDot(state); - const tourTipsEnabled = onboardingTourTipsEnabled(state); + const selectedCard = getSelectedPostCard(state); let emojis: Emoji[] = []; @@ -203,8 +200,7 @@ function makeMapStateToProps() { compactDisplay: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.MESSAGE_DISPLAY, Preferences.MESSAGE_DISPLAY_DEFAULT) === Preferences.MESSAGE_DISPLAY_COMPACT, colorizeUsernames: get(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.COLORIZE_USERNAMES, Preferences.COLORIZE_USERNAMES_DEFAULT) === 'true', shouldShowActionsMenu: shouldShowActionsMenu(state, post), - showActionsMenuPulsatingDot, - tourTipsEnabled, + shortcutReactToLastPostEmittedFrom, isBot, collapsedThreadsEnabled: isCollapsedThreadsEnabled(state), diff --git a/webapp/channels/src/components/post/post_component.tsx b/webapp/channels/src/components/post/post_component.tsx index 2559cc56e1..8a549c272e 100644 --- a/webapp/channels/src/components/post/post_component.tsx +++ b/webapp/channels/src/components/post/post_component.tsx @@ -114,8 +114,6 @@ export type Props = { isPostAcknowledgementsEnabled: boolean; isPostPriorityEnabled: boolean; isCardOpen?: boolean; - shouldShowDotMenu: boolean; - tourTipsEnabled: boolean; }; const PostComponent = (props: Props): JSX.Element => { @@ -148,6 +146,7 @@ const PostComponent = (props: Props): JSX.Element => { const handleA11yActivateEvent = () => setA11y(true); const handleA11yDeactivateEvent = () => setA11y(false); + const handleAlt = (e: KeyboardEvent) => setAlt(e.altKey); useEffect(() => { if (a11yActive) { @@ -156,13 +155,8 @@ const PostComponent = (props: Props): JSX.Element => { }, [a11yActive]); useEffect(() => { - const handleAlt = (e: KeyboardEvent) => { - setAlt(e.altKey); - }; let removeEventListener: (type: string, listener: EventListener) => void; - document.addEventListener('keydown', handleAlt); - document.addEventListener('keyup', handleAlt); if (postRef.current) { postRef.current.addEventListener(A11yCustomEventTypes.ACTIVATE, handleA11yActivateEvent); postRef.current.addEventListener(A11yCustomEventTypes.DEACTIVATE, handleA11yDeactivateEvent); @@ -170,8 +164,6 @@ const PostComponent = (props: Props): JSX.Element => { } return () => { - document.removeEventListener('keydown', handleAlt); - document.removeEventListener('keyup', handleAlt); if (removeEventListener) { removeEventListener(A11yCustomEventTypes.ACTIVATE, handleA11yActivateEvent); removeEventListener(A11yCustomEventTypes.DEACTIVATE, handleA11yDeactivateEvent); @@ -179,6 +171,18 @@ const PostComponent = (props: Props): JSX.Element => { }; }, []); + useEffect(() => { + if (hover) { + document.addEventListener('keydown', handleAlt); + document.addEventListener('keyup', handleAlt); + } + + return () => { + document.removeEventListener('keydown', handleAlt); + document.removeEventListener('keyup', handleAlt); + }; + }, [hover]); + const hasSameRoot = (props: Props) => { if (props.isFirstReply) { return false; @@ -256,16 +260,16 @@ const PostComponent = (props: Props): JSX.Element => { 'current--user': props.currentUserId === post.user_id && !isSystemMessage, 'post--system': isSystemMessage || isMeMessage, 'post--root': props.hasReplies && !(post.root_id && post.root_id.length > 0), - 'post--comment': post.root_id && post.root_id.length > 0 && !props.isCollapsedThreadsEnabled, + 'post--comment': (post.root_id && post.root_id.length > 0 && !props.isCollapsedThreadsEnabled) || (props.location === Locations.RHS_COMMENT), 'post--compact': props.compactDisplay, 'post--hovered': hovered, - 'same--user': props.isConsecutivePost && !props.compactDisplay, + 'same--user': props.isConsecutivePost && (!props.compactDisplay || props.location === Locations.RHS_COMMENT), 'cursor--pointer': alt && !props.channelIsArchived, 'post--hide-controls': post.failed || post.state === Posts.POST_DELETED, 'post--comment same--root': fromAutoResponder, 'post--pinned-or-flagged': (post.is_pinned || props.isFlagged) && props.location === Locations.CENTER, 'mention-comment': props.isCommentMention, - 'post--thread': props.location === Locations.RHS_COMMENT || Locations.RHS_ROOT, + 'post--thread': props.location === Locations.RHS_COMMENT || props.location === Locations.RHS_ROOT, }); }; @@ -380,7 +384,8 @@ const PostComponent = (props: Props): JSX.Element => { let profilePic; const hideProfilePicture = hasSameRoot(props) && (!post.root_id && !props.hasReplies) && !PostUtils.isFromBot(post); - if (!hideProfilePicture) { + const hideProfileCase = !(props.location === Locations.RHS_COMMENT && props.compactDisplay && props.isConsecutivePost); + if (!hideProfilePicture && hideProfileCase) { profilePic = ( { isSystemMessage={isSystemMessage} />
- { + {((!hideProfilePicture && props.location === Locations.CENTER) || hover || props.location !== Locations.CENTER) && void; collapsedThreadsEnabled?: boolean; shouldShowActionsMenu?: boolean; - showActionsMenuPulsatingDot?: boolean; - tourTipsEnabled: boolean; oneClickReactionsEnabled?: boolean; recentEmojis: Emoji[]; isExpanded?: boolean; @@ -51,7 +50,6 @@ type Props = { shortcutReactToLastPostEmittedFrom?: string; isPostHeaderVisible?: boolean | null; isPostBeingEdited?: boolean; - shouldShowDotMenu: boolean; actions: { emitShortcutReactToLastPostFrom: (emittedFrom: 'CENTER' | 'RHS_ROOT' | 'NO_WHERE') => void; }; @@ -63,7 +61,6 @@ const PostOptions = (props: Props): JSX.Element => { const [showEmojiPicker, setShowEmojiPicker] = useState(false); const [showDotMenu, setShowDotMenu] = useState(false); const [showActionsMenu, setShowActionsMenu] = useState(false); - const [showActionTip, setShowActionTip] = useState(false); useEffect(() => { if (props.isLastPost && @@ -80,8 +77,6 @@ const PostOptions = (props: Props): JSX.Element => { isReadOnly, post, oneClickReactionsEnabled, - showActionsMenuPulsatingDot, - tourTipsEnabled, isMobileView, } = props; @@ -102,34 +97,14 @@ const PostOptions = (props: Props): JSX.Element => { }; const handleActionsMenuOpened = (open: boolean) => { - if (tourTipsEnabled && showActionsMenuPulsatingDot) { - setShowActionTip(true); - return; - } setShowActionsMenu(open); props.handleDropdownOpened!(open); }; - const handleActionsMenuTipOpened = () => { - setShowActionTip(true); - props.handleDropdownOpened!(true); - }; - - const handleActionsMenuGotItClick = () => { - props.setActionsMenuInitialisationState?.(({[Preferences.ACTIONS_MENU_VIEWED]: true})); - setShowActionTip(false); - props.handleDropdownOpened!(false); - }; - - const handleTipDismissed = () => { - setShowActionTip(false); - props.handleDropdownOpened!(false); - }; - const getDotMenuRef = () => dotMenuRef.current; const isPostDeleted = post && post.state === Posts.POST_DELETED; - const hoverLocal = props.hover || showEmojiPicker || showDotMenu || showActionsMenu || showActionTip; + const hoverLocal = props.hover || showEmojiPicker || showDotMenu || showActionsMenu; const showCommentIcon = isFromAutoResponder || (!systemMessage && (isMobileView || hoverLocal || (!post.root_id && Boolean(props.hasReplies)) || props.isFirstReply) && props.location === Locations.CENTER); @@ -197,11 +172,6 @@ const PostOptions = (props: Props): JSX.Element => { location={props.location} handleDropdownOpened={handleActionsMenuOpened} isMenuOpen={showActionsMenu} - showPulsatingDot={tourTipsEnabled && showActionsMenuPulsatingDot} - showTutorialTip={tourTipsEnabled && showActionTip} - handleOpenTip={handleActionsMenuTipOpened} - handleNextTip={handleActionsMenuGotItClick} - handleDismissTip={handleTipDismissed} /> ); const dotMenu = ( @@ -231,7 +201,7 @@ const PostOptions = (props: Props): JSX.Element => {
); - } else if (isPostDeleted || !props.shouldShowDotMenu) { + } else if (isPostDeleted) { options = null; } else if (props.location === Locations.SEARCH) { const hasCRTFooter = props.collapsedThreadsEnabled && !post.root_id && (post.reply_count > 0 || post.is_following); @@ -266,7 +236,7 @@ const PostOptions = (props: Props): JSX.Element => {
{!collapsedThreadsEnabled && !showRecentlyUsedReactions && dotMenu} {showRecentReactions} diff --git a/webapp/channels/src/components/post_markdown/index.ts b/webapp/channels/src/components/post_markdown/index.ts index 9de0edbf37..750d91d793 100644 --- a/webapp/channels/src/components/post_markdown/index.ts +++ b/webapp/channels/src/components/post_markdown/index.ts @@ -19,12 +19,11 @@ import {getBool} from 'mattermost-redux/selectors/entities/preferences'; import {Preferences} from 'utils/constants'; import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams'; +import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone'; import {Channel} from '@mattermost/types/channels'; import {Post} from '@mattermost/types/posts'; -import {getCurrentUserTimezone} from '../../selectors/general'; - import PostMarkdown from './post_markdown'; export function makeGetMentionKeysForPost(): ( @@ -75,7 +74,7 @@ function makeMapStateToProps() { isUserCanManageMembers: channel && canManageMembers(state, channel), mentionKeys: getMentionKeysForPost(state, ownProps.post, channel), isMilitaryTime: getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false), - timezone: getCurrentUserTimezone(state), + timezone: getCurrentTimezone(state), }; }; } diff --git a/webapp/channels/src/components/post_reminder_custom_time_picker_modal/index.ts b/webapp/channels/src/components/post_reminder_custom_time_picker_modal/index.ts index 32fd6c0fbf..2235ab865c 100644 --- a/webapp/channels/src/components/post_reminder_custom_time_picker_modal/index.ts +++ b/webapp/channels/src/components/post_reminder_custom_time_picker_modal/index.ts @@ -10,17 +10,16 @@ import {Preferences} from 'mattermost-redux/constants'; import {addPostReminder} from 'mattermost-redux/actions/posts'; import {getBool} from 'mattermost-redux/selectors/entities/preferences'; +import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {GlobalState} from 'types/store'; import {makeAsyncComponent} from 'components/async_load'; -import {getCurrentUserTimezone} from 'selectors/general'; - const PostReminderCustomTimePicker = makeAsyncComponent('PostReminderCustomTimePicker', React.lazy(() => import('./post_reminder_custom_time_picker_modal'))); function mapStateToProps(state: GlobalState) { - const timezone = getCurrentUserTimezone(state); + const timezone = getCurrentTimezone(state); const userId = getCurrentUserId(state); const isMilitaryTime = getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false); diff --git a/webapp/channels/src/components/post_view/post_edited_indicator/index.ts b/webapp/channels/src/components/post_view/post_edited_indicator/index.ts index e18489f17a..a5613ece53 100644 --- a/webapp/channels/src/components/post_view/post_edited_indicator/index.ts +++ b/webapp/channels/src/components/post_view/post_edited_indicator/index.ts @@ -6,18 +6,16 @@ import {connect} from 'react-redux'; import {bindActionCreators, Dispatch} from 'redux'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/common'; -import {makeGetUserTimezone} from 'mattermost-redux/selectors/entities/timezone'; -import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils'; import {getBool} from 'mattermost-redux/selectors/entities/preferences'; import {getPost} from 'mattermost-redux/selectors/entities/posts'; import {getPostEditHistory} from 'mattermost-redux/actions/posts'; import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; import {getChannel} from 'mattermost-redux/selectors/entities/channels'; +import {getCurrentTimezone, isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone'; import {Preferences} from 'utils/constants'; import {isPostOwner, canEditPost} from 'utils/post_utils'; -import {areTimezonesEnabledAndSupported} from '../../../selectors/general'; import {GlobalState} from '../../../types/store'; import {Props as TimestampProps} from '../../timestamp/timestamp'; @@ -49,27 +47,23 @@ type DispatchProps = { export type Props = OwnProps & StateProps & DispatchProps; -function makeMapStateToProps() { - const getUserTimezone = makeGetUserTimezone(); +function mapStateToProps(state: GlobalState, ownProps: OwnProps): StateProps { + const currentUserId = getCurrentUserId(state); + const post = ownProps.postId ? getPost(state, ownProps.postId) : undefined; + const license = getLicense(state); + const config = getConfig(state); + const channel = getChannel(state, post?.channel_id || ''); - return (state: GlobalState, ownProps: OwnProps): StateProps => { - const currentUserId = getCurrentUserId(state); - const post = ownProps.postId ? getPost(state, ownProps.postId) : undefined; - const license = getLicense(state); - const config = getConfig(state); - const channel = getChannel(state, post?.channel_id || ''); + let timeZone: TimestampProps['timeZone']; - let timeZone: TimestampProps['timeZone']; + if (isTimezoneEnabled(state)) { + timeZone = getCurrentTimezone(state); + } + const postOwner = post ? isPostOwner(state, post) : undefined; - if (areTimezonesEnabledAndSupported(state)) { - timeZone = getUserCurrentTimezone(getUserTimezone(state, currentUserId)) ?? undefined; - } - const postOwner = post ? isPostOwner(state, post) : undefined; - - const isMilitaryTime = getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false); - const canEdit = post ? canEditPost(state, post, license, config, channel, currentUserId) : false; - return {isMilitaryTime, timeZone, postOwner, post, canEdit}; - }; + const isMilitaryTime = getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false); + const canEdit = post ? canEditPost(state, post, license, config, channel, currentUserId) : false; + return {isMilitaryTime, timeZone, postOwner, post, canEdit}; } function mapDispatchToProps(dispatch: Dispatch) { @@ -81,4 +75,4 @@ function mapDispatchToProps(dispatch: Dispatch) { }; } -export default connect(makeMapStateToProps, mapDispatchToProps)(PostEditedIndicator); +export default connect(mapStateToProps, mapDispatchToProps)(PostEditedIndicator); diff --git a/webapp/channels/src/components/pricing_modal/content.tsx b/webapp/channels/src/components/pricing_modal/content.tsx index e0f55999fe..b4712ec50f 100644 --- a/webapp/channels/src/components/pricing_modal/content.tsx +++ b/webapp/channels/src/components/pricing_modal/content.tsx @@ -396,7 +396,7 @@ function Content(props: ContentProps) { plan='Professional' planSummary={formatMessage({id: 'pricing_modal.planSummary.professional', defaultMessage: 'Scalable solutions for growing teams'})} price={`$${professionalPrice}`} - rate={formatMessage({id: 'pricing_modal.rate.userPerMonth', defaultMessage: 'USD per user/month {br}(billed annually)'}, { + rate={formatMessage({id: 'pricing_modal.rate.seatPerMonth', defaultMessage: 'USD per seat/month {br}(billed annually)'}, { br:
, b: (chunks: React.ReactNode | React.ReactNodeArray) => ( diff --git a/webapp/channels/src/components/pricing_modal/self_hosted_content.tsx b/webapp/channels/src/components/pricing_modal/self_hosted_content.tsx index 9edb86d824..67c1f8e69a 100644 --- a/webapp/channels/src/components/pricing_modal/self_hosted_content.tsx +++ b/webapp/channels/src/components/pricing_modal/self_hosted_content.tsx @@ -211,7 +211,7 @@ function SelfHostedContent(props: ContentProps) { plan='Professional' planSummary={formatMessage({id: 'pricing_modal.planSummary.professional', defaultMessage: 'Scalable solutions for growing teams'})} price={professionalPrice} - rate={formatMessage({id: 'pricing_modal.rate.userPerMonth', defaultMessage: 'USD per user/month {br}(billed annually)'}, { + rate={formatMessage({id: 'pricing_modal.rate.seatPerMonth', defaultMessage: 'USD per seat/month {br}(billed annually)'}, { br:
, b: (chunks: React.ReactNode | React.ReactNodeArray) => ( diff --git a/webapp/channels/src/components/product_notices_modal/__snapshots__/product_notices.test.tsx.snap b/webapp/channels/src/components/product_notices_modal/__snapshots__/product_notices.test.tsx.snap index abebd8539b..5252df431b 100644 --- a/webapp/channels/src/components/product_notices_modal/__snapshots__/product_notices.test.tsx.snap +++ b/webapp/channels/src/components/product_notices_modal/__snapshots__/product_notices.test.tsx.snap @@ -4,6 +4,7 @@ exports[`ProductNoticesModal Match snapshot for single notice 1`] = ` { this.state.selectedProduct ? this.state.selectedProduct.name : '', )} price={yearlyProductMonthlyPrice} - rate={formatMessage({id: 'pricing_modal.rate.userPerMonth', defaultMessage: 'USD per user/month {br}(billed annually)'}, { + rate={formatMessage({id: 'pricing_modal.rate.seatPerMonth', defaultMessage: 'USD per seat/month {br}(billed annually)'}, { br:
, b: (chunks: React.ReactNode | React.ReactNodeArray) => ( diff --git a/webapp/channels/src/components/root/effects.ts b/webapp/channels/src/components/root/effects.ts index 3d7d834bd3..4ff1c46520 100644 --- a/webapp/channels/src/components/root/effects.ts +++ b/webapp/channels/src/components/root/effects.ts @@ -4,10 +4,7 @@ import {Settings} from 'luxon'; import {getCurrentLocale} from 'selectors/i18n'; -import {areTimezonesEnabledAndSupported} from 'selectors/general'; -import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; -import {makeGetUserTimezone} from 'mattermost-redux/selectors/entities/timezone'; +import {getCurrentTimezone, isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone'; import {GlobalState} from 'types/store'; let prevTimezone: string | undefined; @@ -19,8 +16,8 @@ export function applyLuxonDefaults(state: GlobalState) { Settings.defaultLocale = locale; } - if (areTimezonesEnabledAndSupported(state)) { - const tz = getUserCurrentTimezone(makeGetUserTimezone()(state, getCurrentUserId(state))) ?? undefined; + if (isTimezoneEnabled(state)) { + const tz = getCurrentTimezone(state); if (tz !== prevTimezone) { prevTimezone = tz; Settings.defaultZone = tz ?? 'system'; diff --git a/webapp/channels/src/components/seats_calculator/index.tsx b/webapp/channels/src/components/seats_calculator/index.tsx index 57c3436576..fe2b9259a2 100644 --- a/webapp/channels/src/components/seats_calculator/index.tsx +++ b/webapp/channels/src/components/seats_calculator/index.tsx @@ -88,7 +88,7 @@ function validateSeats(seats: string, annualPricePerSeat: number, minSeats: numb {errorPrefix} , }} @@ -167,7 +167,7 @@ export default function SeatsCalculator(props: Props) { type='text' value={props.seats.quantity} onChange={onChange} - placeholder={intl.formatMessage({id: 'self_hosted_signup.seats', defaultMessage: 'User seats'})} + placeholder={intl.formatMessage({id: 'self_hosted_signup.seats', defaultMessage: 'Seats'})} wrapperClassName='user_seats' inputClassName='user_seats' maxLength={maxSeats.toString().length + 1} @@ -197,7 +197,7 @@ export default function SeatsCalculator(props: Props) {
{ // check title, and some of the most prominent details and secondary actions screen.getByText('Provide your payment details'); screen.getByText('Contact Sales'); - screen.getByText('USD per user/month', {exact: false}); + screen.getByText('USD per seat/month', {exact: false}); screen.getByText('billed annually', {exact: false}); screen.getByText(productName); screen.getByText('You will be billed today. Your license will be applied automatically', {exact: false}); diff --git a/webapp/channels/src/components/self_hosted_purchase_modal/self_hosted_card.tsx b/webapp/channels/src/components/self_hosted_purchase_modal/self_hosted_card.tsx index 59b9d4af0f..13b1925be2 100644 --- a/webapp/channels/src/components/self_hosted_purchase_modal/self_hosted_card.tsx +++ b/webapp/channels/src/components/self_hosted_purchase_modal/self_hosted_card.tsx @@ -80,7 +80,7 @@ export default function SelfHostedCard(props: Props) { topColor='#4A69AC' plan={props.desiredPlanName} price={`${props.desiredProduct?.price_per_seat?.toString()}`} - rate={intl.formatMessage({id: 'pricing_modal.rate.userPerMonth', defaultMessage: 'USD per user/month {br}(billed annually)'}, { + rate={intl.formatMessage({id: 'pricing_modal.rate.seatPerMonth', defaultMessage: 'USD per seat/month {br}(billed annually)'}, { br:
, b: (chunks: React.ReactNode | React.ReactNodeArray) => ( diff --git a/webapp/channels/src/components/sidebar/__snapshots__/add_channels_cta_button.test.tsx.snap b/webapp/channels/src/components/sidebar/__snapshots__/add_channels_cta_button.test.tsx.snap index 59e9f3bbdd..893aaa0ffe 100644 --- a/webapp/channels/src/components/sidebar/__snapshots__/add_channels_cta_button.test.tsx.snap +++ b/webapp/channels/src/components/sidebar/__snapshots__/add_channels_cta_button.test.tsx.snap @@ -11,6 +11,7 @@ exports[`components/new_channel_modal should match snapshot 1`] = ` aria-label="Add Channels Dropdown" className="SidebarChannelNavigator__addChannelsCtaLhsButton SidebarChannelNavigator__addChannelsCtaLhsButton--untouched" id="addChannelsCta" + onClick={[Function]} >
  • `; + +exports[`components/new_channel_modal should match snapshot when user has only join channel permissions 1`] = ` + +`; diff --git a/webapp/channels/src/components/sidebar/add_channels_cta_button.test.tsx b/webapp/channels/src/components/sidebar/add_channels_cta_button.test.tsx index 6066f4dcd3..68ab7ed614 100644 --- a/webapp/channels/src/components/sidebar/add_channels_cta_button.test.tsx +++ b/webapp/channels/src/components/sidebar/add_channels_cta_button.test.tsx @@ -80,6 +80,12 @@ describe('components/new_channel_modal', () => { system_user: { permissions: [Permissions.JOIN_PUBLIC_CHANNELS, Permissions.CREATE_PRIVATE_CHANNEL, Permissions.CREATE_PUBLIC_CHANNEL], }, + system_user_join_permissions: { + permissions: [Permissions.JOIN_PUBLIC_CHANNELS], + }, + system_user_create_public_permissions: { + permissions: [Permissions.JOIN_PUBLIC_CHANNELS, Permissions.CREATE_PUBLIC_CHANNEL], + }, }, }, }, @@ -99,6 +105,25 @@ describe('components/new_channel_modal', () => { ).toMatchSnapshot(); }); + test('should match snapshot when user has only join channel permissions', () => { + const userWithJoinChannelsPermission = { + currentUserId: 'current_user_id', + profiles: { + current_user_id: { + id: 'current_user_id', + roles: 'system_user_join_permissions', + }, + }, + } as unknown as UsersState; + mockState = {...mockState, entities: {...mockState.entities, users: userWithJoinChannelsPermission}}; + + expect( + shallow( + , + ), + ).toMatchSnapshot(); + }); + test('should find the add channels button when user has permissions', () => { const wrapper = mountWithIntl( , @@ -145,4 +170,52 @@ describe('components/new_channel_modal', () => { expect(trackEvent).toHaveBeenCalledWith('ui', 'add_channels_cta_button_clicked'); }); + + test('should not display as a Cta Dropdown when user only has permissions to join channels ', () => { + const userWithJoinChannelsPermission = { + currentUserId: 'current_user_id', + profiles: { + current_user_id: { + id: 'current_user_id', + roles: 'system_user_join_permissions', + }, + }, + } as unknown as UsersState; + mockState = {...mockState, entities: {...mockState.entities, users: userWithJoinChannelsPermission}}; + + const wrapper = mountWithIntl( + , + ); + + // do not find the menu + expect(wrapper.find('.AddChannelsCtaDropdown').exists()).toBeFalsy(); + + // only find the button + const button = wrapper.find('button#addChannelsCta'); + expect(button.exists()).toBeTruthy(); + + button.simulate('click'); + + // when clicked show the browse channels modal + expect(trackEvent).toHaveBeenCalledWith('ui', 'browse_channels_button_is_clicked'); + }); + + test('should still display as a Cta Dropdown when user has permissions to create at least one form of channel', () => { + const userWithJoinChannelsPermission = { + currentUserId: 'current_user_id', + profiles: { + current_user_id: { + id: 'current_user_id', + roles: 'system_user_create_public_permissions', + }, + }, + } as unknown as UsersState; + mockState = {...mockState, entities: {...mockState.entities, users: userWithJoinChannelsPermission}}; + + const wrapper = mountWithIntl( + , + ); + + expect(wrapper.find('.AddChannelsCtaDropdown').exists()).toBeTruthy(); + }); }); diff --git a/webapp/channels/src/components/sidebar/add_channels_cta_button.tsx b/webapp/channels/src/components/sidebar/add_channels_cta_button.tsx index 964bc81c4b..d3f1244edd 100644 --- a/webapp/channels/src/components/sidebar/add_channels_cta_button.tsx +++ b/webapp/channels/src/components/sidebar/add_channels_cta_button.tsx @@ -106,8 +106,28 @@ const AddChannelsCtaButton = (): JSX.Element | null => { ); }; - const trackOpen = (opened: boolean) => { - openAddChannelsCtaOpen(opened); + const addChannelsButton = (btnCallback?: () => void) => { + const handleClick = () => btnCallback?.(); + return ( + + ); + }; + + const storePreferencesAndTrackEvent = () => { trackEvent('ui', 'add_channels_cta_button_clicked'); if (!touchedAddChannelsCtaButton) { dispatch(savePreferences( @@ -122,26 +142,26 @@ const AddChannelsCtaButton = (): JSX.Element | null => { } }; + const trackOpen = (opened: boolean) => { + openAddChannelsCtaOpen(opened); + storePreferencesAndTrackEvent(); + }; + + if (!canCreateChannel) { + const browseChannelsAction = () => { + showMoreChannelsModal(); + storePreferencesAndTrackEvent(); + }; + return addChannelsButton(browseChannelsAction); + } + return ( - + {addChannelsButton()} +
    + + Interested in receiving Mattermost security updates via newsletter? + + + Sign up at + + https://mattermost.com/security-updates/ + + . + +
    +
    + + Interested in receiving Mattermost security updates via newsletter? + + + Sign up at + + https://mattermost.com/security-updates/ + + . + +
    ; let mockDispatch = jest.fn(); @@ -96,7 +98,7 @@ describe('components/signup/Signup', () => { beforeEach(() => { mockLocation = {pathname: '', search: '', hash: ''}; - mockLicense = {IsLicensed: 'true'}; + mockLicense = {IsLicensed: 'true', Cloud: 'false'}; mockState = { entities: { @@ -178,7 +180,7 @@ describe('components/signup/Signup', () => { }); it('should match snapshot for all signup options enabled with isLicensed disabled', () => { - mockLicense = {IsLicensed: 'false'}; + mockLicense = {IsLicensed: 'false', Cloud: 'false'}; const wrapper = shallow( , @@ -295,4 +297,45 @@ describe('components/signup/Signup', () => { expect(wrapper.find('.content-layout-column-title').text()).toEqual('This invite link is invalid'); }); }); + + it('should show newsletter check box opt-in for self-hosted non airgapped workspaces', async () => { + jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => true); + mockLicense = {IsLicensed: 'true', Cloud: 'false'}; + + const {container: signupContainer} = renderWithIntlAndStore( + + + , {}); + + screen.getByTestId('signup-body-card-form-check-newsletter'); + const checkInput = screen.getByTestId('signup-body-card-form-check-newsletter'); + expect(checkInput).toHaveAttribute('type', 'checkbox'); + + expect(signupContainer).toHaveTextContent(/I would like to receive Mattermost security updates via newsletter. Data Terms and Conditions apply/); + }); + + it('should NOT show newsletter check box opt-in for self-hosted AND airgapped workspaces', async () => { + jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => false); + mockLicense = {IsLicensed: 'true', Cloud: 'false'}; + + const {container: signupContainer} = renderWithIntlAndStore( + + + , {}); + + expect(() => screen.getByTestId('signup-body-card-form-check-newsletter')).toThrow(); + expect(signupContainer).toHaveTextContent('Interested in receiving Mattermost security updates via newsletter?Sign up at https://mattermost.com/security-updates/.'); + }); + + it('should not show any newsletter related opt-in or text for cloud', async () => { + jest.spyOn(useCWSAvailabilityCheckAll, 'default').mockImplementation(() => true); + mockLicense = {IsLicensed: 'true', Cloud: 'true'}; + + renderWithIntlAndStore( + + + , {}); + + expect(() => screen.getByTestId('signup-body-card-form-check-newsletter')).toThrow(); + }); }); diff --git a/webapp/channels/src/components/signup/signup.tsx b/webapp/channels/src/components/signup/signup.tsx index dde147597f..3f3a13d460 100644 --- a/webapp/channels/src/components/signup/signup.tsx +++ b/webapp/channels/src/components/signup/signup.tsx @@ -48,9 +48,12 @@ import LoginOpenIDIcon from 'components/widgets/icons/login_openid_icon'; import LoginOffice365Icon from 'components/widgets/icons/login_office_365_icon'; import Input, {CustomMessageInputType, SIZE} from 'components/widgets/inputs/input/input'; import PasswordInput from 'components/widgets/inputs/password_input/password_input'; +import CheckInput from 'components/widgets/inputs/check'; import SaveButton from 'components/save_button'; +import useCWSAvailabilityCheck from 'components/common/hooks/useCWSAvailabilityCheck'; +import ExternalLink from 'components/external_link'; -import {Constants, ItemStatus, ValidationErrors} from 'utils/constants'; +import {Constants, HostedCustomerLinks, ItemStatus, ValidationErrors} from 'utils/constants'; import {isValidUsername, isValidPassword, getPasswordConfig, getRoleFromTrackFlow, getMediumFromTrackFlow} from 'utils/utils'; import './signup.scss'; @@ -99,7 +102,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => { TermsOfServiceLink, PrivacyPolicyLink, } = config; - const {IsLicensed} = useSelector(getLicense); + const {IsLicensed, Cloud} = useSelector(getLicense); const loggedIn = Boolean(useSelector(getCurrentUserId)); const useCaseOnboarding = useSelector(getUseCaseOnboarding); const usedBefore = useSelector((state: GlobalState) => (!inviteId && !loggedIn && token ? getGlobalItem(state, token, null) : undefined)); @@ -110,6 +113,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => { const passwordInput = useRef(null); const isLicensed = IsLicensed === 'true'; + const isCloud = Cloud === 'true'; const enableOpenServer = EnableOpenServer === 'true'; const noAccounts = NoAccounts === 'true'; const enableSignUpWithEmail = EnableSignUpWithEmail === 'true'; @@ -136,12 +140,24 @@ const Signup = ({onCustomizeHeader}: SignupProps) => { const [teamName, setTeamName] = useState(parsedTeamName ?? ''); const [alertBanner, setAlertBanner] = useState(null); const [isMobileView, setIsMobileView] = useState(false); + const [subscribeToSecurityNewsletter, setSubscribeToSecurityNewsletter] = useState(false); + + const canReachCWS = useCWSAvailabilityCheck(); const enableExternalSignup = enableSignUpWithGitLab || enableSignUpWithOffice365 || enableSignUpWithGoogle || enableSignUpWithOpenId || enableLDAP || enableSAML; const hasError = Boolean(emailError || nameError || passwordError || serverError || alertBanner); const canSubmit = Boolean(email && name && password) && !hasError && !loading; const {error: passwordInfo} = isValidPassword('', getPasswordConfig(config), intl); + const subscribeToSecurityNewsletterFunc = () => { + try { + Client4.subscribeToNewsletter({email, subscribed_content: 'security_newsletter'}); + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + } + }; + const getExternalSignupOptions = () => { const externalLoginOptions: ExternalLoginButtonType[] = []; @@ -564,6 +580,9 @@ const Signup = ({onCustomizeHeader}: SignupProps) => { } await handleSignupSuccess(user, data as UserProfile); + if (subscribeToSecurityNewsletter) { + subscribeToSecurityNewsletterFunc(); + } } else { setIsWaiting(false); } @@ -571,6 +590,60 @@ const Signup = ({onCustomizeHeader}: SignupProps) => { const handleReturnButtonOnClick = () => history.replace('/'); + const getNewsletterCheck = () => { + if (isCloud) { + return null; + } + + if (canReachCWS) { + return ( + setSubscribeToSecurityNewsletter(!subscribeToSecurityNewsletter)} + text={ + formatMessage( + {id: 'newsletter_optin.checkmark.text', defaultMessage: 'I would like to receive Mattermost security updates via newsletter. Data Terms and Conditions apply'}, + { + a: (chunks: React.ReactNode | React.ReactNodeArray) => ( + + {chunks} + + ), + }, + )} + checked={subscribeToSecurityNewsletter} + /> + ); + } + return ( +
    + + {formatMessage({id: 'newsletter_optin.title', defaultMessage: 'Interested in receiving Mattermost security updates via newsletter?'})} + + + {formatMessage( + {id: 'newsletter_optin.desc', defaultMessage: 'Sign up at {link}.'}, + { + link: HostedCustomerLinks.SECURITY_UPDATES, + a: (chunks: React.ReactNode | React.ReactNodeArray) => ( + + {chunks} + + ), + }, + )} + +
    + ); + }; + const handleOnBlur = (e: FocusEvent, inputId: string) => { const text = e.target.value; if (!text) { @@ -736,6 +809,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => { error={passwordError} onBlur={(e) => handleOnBlur(e, 'password')} /> + {getNewsletterCheck()} ) => void; } -export default class SpinnerButton extends PureComponent> { +export default class SpinnerButton extends PureComponent> { public static defaultProps: Partial = { spinning: false, } diff --git a/webapp/channels/src/components/start_trial_form_modal/index.tsx b/webapp/channels/src/components/start_trial_form_modal/index.tsx index 04e778682b..e64ed539a1 100644 --- a/webapp/channels/src/components/start_trial_form_modal/index.tsx +++ b/webapp/channels/src/components/start_trial_form_modal/index.tsx @@ -129,7 +129,7 @@ function StartTrialFormModal(props: Props): JSX.Element | null { company_country: country, company_size: orgSize, }; - const error = await dispatch(requestTrialLicense(trialRequestBody, props.page || 'license')); + const {error, data} = await dispatch(requestTrialLicense(trialRequestBody, props.page || 'license')); if (error) { setLoadStatus(TrialLoadStatus.Failed); let title; @@ -137,7 +137,7 @@ function StartTrialFormModal(props: Props): JSX.Element | null { let buttonText; let onTryAgain = handleErrorModalTryAgain; - if (error?.data.status === 422) { + if (data.status === 422) { title = (<>); subtitle = ( { - const currentUserId = getCurrentUserId(state); - const userTimezone = getUserTimezone(state, currentUserId); - const locale = getCurrentLocale(state); + const enableTimezone = isTimezoneEnabled(state); - const enableTimezone = areTimezonesEnabledAndSupported(state); + let currentDate; + if (enableTimezone) { + currentDate = getCurrentDateForTimezone(timezone); + } - let currentDate; - if (enableTimezone) { - if (userTimezone.useAutomaticTimezone) { - currentDate = getCurrentDateForTimezone(userTimezone.automaticTimezone); - } else { - currentDate = getCurrentDateForTimezone(userTimezone.manualTimezone); - } - } - - return { - currentDate, - locale, - }; + return { + currentDate, + locale, }; } -export default connect(makeMapStateToProps)(SearchDateSuggestion); +export default connect(mapStateToProps)(SearchDateSuggestion); diff --git a/webapp/channels/src/components/textbox/textbox.tsx b/webapp/channels/src/components/textbox/textbox.tsx index dc89524e21..1ba345a75e 100644 --- a/webapp/channels/src/components/textbox/textbox.tsx +++ b/webapp/channels/src/components/textbox/textbox.tsx @@ -44,6 +44,7 @@ export type Props = { onMouseUp?: (e: React.MouseEvent) => void; onKeyUp?: (e: React.KeyboardEvent) => void; onBlur?: (e: FocusEvent) => void; + onFocus?: (e: FocusEvent) => void; supportsCommands?: boolean; handlePostError?: (message: JSX.Element | null) => void; onPaste?: (e: ClipboardEvent) => void; @@ -312,6 +313,7 @@ export default class Textbox extends React.PureComponent { onKeyUp={this.handleKeyUp} onComposition={this.props.onComposition} onBlur={this.handleBlur} + onFocus={this.props.onFocus} onHeightChange={this.props.onHeightChange} onWidthChange={this.props.onWidthChange} onPaste={this.props.onPaste} diff --git a/webapp/channels/src/components/threading/virtualized_thread_viewer/thread_viewer_row.tsx b/webapp/channels/src/components/threading/virtualized_thread_viewer/thread_viewer_row.tsx index b26051f723..4917f2fa79 100644 --- a/webapp/channels/src/components/threading/virtualized_thread_viewer/thread_viewer_row.tsx +++ b/webapp/channels/src/components/threading/virtualized_thread_viewer/thread_viewer_row.tsx @@ -27,7 +27,6 @@ type Props = { previousPostId: string; teamId: string; timestampProps?: Partial; - lastPost: Post; }; function noop() {} diff --git a/webapp/channels/src/components/threading/virtualized_thread_viewer/virtualized_thread_viewer.tsx b/webapp/channels/src/components/threading/virtualized_thread_viewer/virtualized_thread_viewer.tsx index 09fc561d00..4cafae3823 100644 --- a/webapp/channels/src/components/threading/virtualized_thread_viewer/virtualized_thread_viewer.tsx +++ b/webapp/channels/src/components/threading/virtualized_thread_viewer/virtualized_thread_viewer.tsx @@ -403,7 +403,6 @@ class ThreadViewerVirtualized extends PureComponent { previousPostId={getPreviousPostId(data, index)} teamId={this.props.teamId} timestampProps={this.props.useRelativeTimestamp ? THREADING_TIME : undefined} - lastPost={this.props.lastPost} />
  • ); diff --git a/webapp/channels/src/components/timestamp/index.test.tsx b/webapp/channels/src/components/timestamp/index.test.tsx index 182c7e4b8a..ecbaa75f5b 100644 --- a/webapp/channels/src/components/timestamp/index.test.tsx +++ b/webapp/channels/src/components/timestamp/index.test.tsx @@ -1,13 +1,13 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import mergeObjects from 'packages/mattermost-redux/test/merge_objects'; + import {GlobalState} from 'types/store'; -import {UserTimezone} from '@mattermost/types/users'; -import {PreferenceType} from '@mattermost/types/preferences'; import * as Timestamp from './timestamp'; -import {makeMapStateToProps} from './index'; +import {mapStateToProps} from './index'; const supportsHourCycleOg = Timestamp.supportsHourCycle; Object.defineProperty(Timestamp, 'supportsHourCycle', {get: () => supportsHourCycleOg}); @@ -39,57 +39,89 @@ describe('mapStateToProps', () => { describe('hourCycle', () => { test('hourCycle should be h12 when military time is false and the prop was not set', () => { - const props = makeMapStateToProps()(initialState, {}); + const props = mapStateToProps(initialState, {}); expect(props.hourCycle).toBe('h12'); }); test('hourCycle should be h23 when military time is true and the prop was not set', () => { - const testState = {...initialState}; - testState.entities.preferences.myPreferences['display_settings--use_military_time'] = { - category: 'display_settings', - name: 'use_military_time', - user_id: currentUserId, - value: 'true', - } as PreferenceType; + const testState = mergeObjects(initialState, { + entities: { + preferences: { + myPreferences: { + 'display_settings--use_military_time': { + category: 'display_settings', + name: 'use_military_time', + user_id: currentUserId, + value: 'true', + }, + }, + }, + }, + }); - const props = makeMapStateToProps()(testState, {}); + const props = mapStateToProps(testState, {}); expect(props.hourCycle).toBe('h23'); }); test('hourCycle should have the value of prop.hourCycle when given', () => { - const testState = {...initialState}; - testState.entities.preferences.myPreferences['display_settings--use_military_time'] = { - category: 'display_settings', - name: 'use_military_time', - user_id: currentUserId, - value: 'true', - } as PreferenceType; + const testState = mergeObjects(initialState, { + entities: { + preferences: { + myPreferences: { + 'display_settings--use_military_time': { + category: 'display_settings', + name: 'use_military_time', + user_id: currentUserId, + value: 'true', + }, + }, + }, + }, + }); - const props = makeMapStateToProps()(testState, {hourCycle: 'h24'}); + const props = mapStateToProps(testState, {hourCycle: 'h24'}); expect(props.hourCycle).toBe('h24'); }); }); describe('timeZone', () => { test('timeZone should be the user TZ when the prop was not set', () => { - const testState = {...initialState}; - testState.entities.users.profiles[currentUserId].timezone = { - useAutomaticTimezone: false, - manualTimezone: 'Europe/Paris', - } as UserTimezone; + const testState = mergeObjects(initialState, { + entities: { + users: { + profiles: { + [currentUserId]: { + timezone: { + useAutomaticTimezone: false, + manualTimezone: 'Europe/Paris', + }, + }, + }, + }, + }, + }); - const props = makeMapStateToProps()(testState, {}); + const props = mapStateToProps(testState, {}); expect(props.timeZone).toBe('Europe/Paris'); }); test('timeZone should be the value of prop.timeZone when given', () => { - const testState = {...initialState}; - testState.entities.users.profiles[currentUserId].timezone = { - useAutomaticTimezone: false, - manualTimezone: 'Europe/Paris', - } as UserTimezone; + const testState = mergeObjects(initialState, { + entities: { + users: { + profiles: { + [currentUserId]: { + timezone: { + useAutomaticTimezone: false, + manualTimezone: 'Europe/Paris', + }, + }, + }, + }, + }, + }); - const props = makeMapStateToProps()(testState, {timeZone: 'America/Phoenix'}); + const props = mapStateToProps(testState, {timeZone: 'America/Phoenix'}); expect(props.timeZone).toBe('America/Phoenix'); }); @@ -97,51 +129,72 @@ describe('mapStateToProps', () => { const testState = {...initialState}; testState.entities.general.config.ExperimentalTimezone = 'false'; - const props = makeMapStateToProps()(testState, {timeZone: 'America/Chicago'}); + const props = mapStateToProps(testState, {timeZone: 'America/Chicago'}); expect(props.timeZone).toBe('America/Chicago'); }); }); describe('hour12, hourCycle unsupported', () => { test('hour12 should be false when using military time', () => { - const testState = {...initialState}; - testState.entities.preferences.myPreferences['display_settings--use_military_time'] = { - category: 'display_settings', - name: 'use_military_time', - user_id: currentUserId, - value: 'true', - } as PreferenceType; + const testState = mergeObjects(initialState, { + entities: { + preferences: { + myPreferences: { + 'display_settings--use_military_time': { + category: 'display_settings', + name: 'use_military_time', + user_id: currentUserId, + value: 'true', + }, + }, + }, + }, + }); supportsHourCycleSpy.mockReturnValueOnce(false); - const props = makeMapStateToProps()(testState, {}); + const props = mapStateToProps(testState, {}); expect(props.hour12).toBe(false); }); test('hour12 should be true when not using military time', () => { - const testState = {...initialState}; - testState.entities.preferences.myPreferences['display_settings--use_military_time'] = { - category: 'display_settings', - name: 'use_military_time', - user_id: currentUserId, - value: 'false', - } as PreferenceType; + const testState = mergeObjects(initialState, { + entities: { + preferences: { + myPreferences: { + 'display_settings--use_military_time': { + category: 'display_settings', + name: 'use_military_time', + user_id: currentUserId, + value: 'false', + }, + }, + }, + }, + }); supportsHourCycleSpy.mockReturnValueOnce(false); - const props = makeMapStateToProps()(testState, {}); + const props = mapStateToProps(testState, {}); expect(props.hour12).toBe(true); }); test('hour12 should equal props.hour12 when defined', () => { - const testState = {...initialState}; - testState.entities.preferences.myPreferences['display_settings--use_military_time'] = { - category: 'display_settings', - name: 'use_military_time', - user_id: currentUserId, - value: 'false', - } as PreferenceType; + const testState = mergeObjects(initialState, { + entities: { + preferences: { + myPreferences: { + 'display_settings--use_military_time': { + category: 'display_settings', + name: 'use_military_time', + user_id: currentUserId, + value: 'false8', + }, + }, + }, + }, + }); supportsHourCycleSpy.mockReturnValueOnce(false); - const props = makeMapStateToProps()(testState, {hour12: false}); + const props = mapStateToProps(testState, {hour12: false}); expect(props.hour12).toBe(false); }); }); diff --git a/webapp/channels/src/components/timestamp/index.ts b/webapp/channels/src/components/timestamp/index.ts index b3803ec0c1..a81d8fa389 100644 --- a/webapp/channels/src/components/timestamp/index.ts +++ b/webapp/channels/src/components/timestamp/index.ts @@ -3,14 +3,11 @@ import {connect} from 'react-redux'; -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; -import {makeGetUserTimezone} from 'mattermost-redux/selectors/entities/timezone'; +import {getCurrentTimezoneFull, isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone'; import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils'; import {getBool} from 'mattermost-redux/selectors/entities/preferences'; import {UserTimezone} from '@mattermost/types/users'; -import {areTimezonesEnabledAndSupported} from 'selectors/general'; - import {GlobalState} from 'types/store'; import {Preferences} from 'utils/constants'; @@ -24,33 +21,27 @@ type Props = { hourCycle?: TimestampProps['hourCycle']; } -export function makeMapStateToProps() { - const getUserTimezone = makeGetUserTimezone(); +export function mapStateToProps(state: GlobalState, ownProps: Props) { + let timeZone: TimestampProps['timeZone']; + let hourCycle: TimestampProps['hourCycle']; + let hour12: TimestampProps['hour12']; - return (state: GlobalState, ownProps: Props) => { - const currentUserId = getCurrentUserId(state); + if (isTimezoneEnabled(state)) { + timeZone = getUserCurrentTimezone(ownProps.userTimezone ?? getCurrentTimezoneFull(state)) ?? undefined; + } - let timeZone: TimestampProps['timeZone']; - let hourCycle: TimestampProps['hourCycle']; - let hour12: TimestampProps['hour12']; + const useMilitaryTime = getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false); - if (areTimezonesEnabledAndSupported(state)) { - timeZone = getUserCurrentTimezone(ownProps.userTimezone ?? getUserTimezone(state, currentUserId)) ?? undefined; - } + if (supportsHourCycle) { + hourCycle = ownProps.hourCycle || (useMilitaryTime ? 'h23' : 'h12'); + } else { + hour12 = ownProps.hour12 ?? (!useMilitaryTime); + } - const useMilitaryTime = getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false); - - if (supportsHourCycle) { - hourCycle = ownProps.hourCycle || (useMilitaryTime ? 'h23' : 'h12'); - } else { - hour12 = ownProps.hour12 ?? (!useMilitaryTime); - } - - return {timeZone: ownProps.timeZone || timeZone, hourCycle, hour12}; - }; + return {timeZone: ownProps.timeZone || timeZone, hourCycle, hour12}; } -export default connect(makeMapStateToProps)(Timestamp); +export default connect(mapStateToProps)(Timestamp); export {default as SemanticTime} from './semantic_time'; import * as RelativeRanges from './relative_ranges'; diff --git a/webapp/channels/src/components/user_settings/display/index.ts b/webapp/channels/src/components/user_settings/display/index.ts index 725568b55e..fec3c71690 100644 --- a/webapp/channels/src/components/user_settings/display/index.ts +++ b/webapp/channels/src/components/user_settings/display/index.ts @@ -15,7 +15,7 @@ import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone'; import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; import {getCurrentUserId, getUser} from 'mattermost-redux/selectors/entities/users'; import {get, isCollapsedThreadsAllowed, getCollapsedThreadsPreference} from 'mattermost-redux/selectors/entities/preferences'; -import {getTimezoneLabel, makeGetUserTimezone} from 'mattermost-redux/selectors/entities/timezone'; +import {getCurrentTimezoneFull, getCurrentTimezoneLabel} from 'mattermost-redux/selectors/entities/timezone'; import {getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils'; import {CollapsedThreads} from '@mattermost/types/config'; @@ -34,15 +34,13 @@ type Actions = { } export function makeMapStateToProps() { - const getUserTimezone = makeGetUserTimezone(); - return (state: GlobalState) => { const config = getConfig(state); const currentUserId = getCurrentUserId(state); - const userTimezone = getUserTimezone(state, currentUserId); + const userTimezone = getCurrentTimezoneFull(state); const automaticTimezoneNotSet = userTimezone && userTimezone.useAutomaticTimezone && !userTimezone.automaticTimezone; const shouldAutoUpdateTimezone = !userTimezone || automaticTimezoneNotSet; - const timezoneLabel = getTimezoneLabel(state, currentUserId); + const timezoneLabel = getCurrentTimezoneLabel(state); const allowCustomThemes = config.AllowCustomThemes === 'true'; const enableLinkPreviews = config.EnableLinkPreviews === 'true'; const defaultClientLocale = config.DefaultClientLocale as string; diff --git a/webapp/channels/src/components/user_settings/display/manage_timezones/index.ts b/webapp/channels/src/components/user_settings/display/manage_timezones/index.ts index 0730f27d06..46daa4d0f2 100644 --- a/webapp/channels/src/components/user_settings/display/manage_timezones/index.ts +++ b/webapp/channels/src/components/user_settings/display/manage_timezones/index.ts @@ -10,9 +10,7 @@ import {updateMe} from 'mattermost-redux/actions/users'; import {ActionFunc, ActionResult} from 'mattermost-redux/types/actions'; import {UserProfile} from '@mattermost/types/users'; import {GlobalState} from '@mattermost/types/store'; -import {getTimezoneLabel} from 'mattermost-redux/selectors/entities/timezone'; - -import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; +import {getCurrentTimezoneLabel} from 'mattermost-redux/selectors/entities/timezone'; import ManageTimezones from './manage_timezones'; @@ -27,8 +25,7 @@ function mapDispatchToProps(dispatch: Dispatch) { }, dispatch)}; } function mapStateToProps(state: GlobalState) { - const currentUserId = getCurrentUserId(state); - const timezoneLabel = getTimezoneLabel(state, currentUserId); + const timezoneLabel = getCurrentTimezoneLabel(state); return { timezones, timezoneLabel, diff --git a/webapp/channels/src/components/widgets/inputs/check/check.scss b/webapp/channels/src/components/widgets/inputs/check/check.scss new file mode 100644 index 0000000000..e6e9809d39 --- /dev/null +++ b/webapp/channels/src/components/widgets/inputs/check/check.scss @@ -0,0 +1,19 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +.check-input { + display: flex; + align-items: flex-start; + margin-top: 24px; + margin-bottom: 32px; + + .text { + margin-left: 8px; + color: var(--center-channel-color-rgb); + font-family: 'Open Sans'; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; + } +} diff --git a/webapp/channels/src/components/widgets/inputs/check/index.tsx b/webapp/channels/src/components/widgets/inputs/check/index.tsx new file mode 100644 index 0000000000..cc05728217 --- /dev/null +++ b/webapp/channels/src/components/widgets/inputs/check/index.tsx @@ -0,0 +1,28 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {ReactNode} from 'react'; +import './check.scss'; + +type Props = { + id: string; + name: string; + text: ReactNode; + onChange: () => void; + checked: boolean; +} + +function CheckInput(props: Props) { + return ( +
    + + {props.text} +
    + ); +} + +export default CheckInput; diff --git a/webapp/channels/src/components/widgets/inputs/input/__snapshots__/input.test.tsx.snap b/webapp/channels/src/components/widgets/inputs/input/__snapshots__/input.test.tsx.snap new file mode 100644 index 0000000000..701ba83d1c --- /dev/null +++ b/webapp/channels/src/components/widgets/inputs/input/__snapshots__/input.test.tsx.snap @@ -0,0 +1,26 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/widgets/inputs/Input should match snapshot 1`] = ` +
    +
    + +
    + +
    +
    +
    +`; diff --git a/webapp/channels/src/components/widgets/inputs/input/input.scss b/webapp/channels/src/components/widgets/inputs/input/input.scss index 281c31175f..eacee3bfba 100644 --- a/webapp/channels/src/components/widgets/inputs/input/input.scss +++ b/webapp/channels/src/components/widgets/inputs/input/input.scss @@ -49,11 +49,16 @@ .Input_wrapper { display: flex; flex: 1; + align-items: center; padding: 0 16px; margin: 2px 0; color: rgba(var(--center-channel-color-rgb), 0.56); font-size: 14px; line-height: 20px; + + > :not(:first-child) { + margin-left: 8px; + } } .Input_limit-exceeded { @@ -196,4 +201,10 @@ background-color: rgba(var(--center-channel-color-rgb), 0.04); } } + + .Input__clear { + display: flex; + color: rgba(var(--center-channel-color), 0.68); + cursor: pointer; + } } diff --git a/webapp/channels/src/components/widgets/inputs/input/input.test.tsx b/webapp/channels/src/components/widgets/inputs/input/input.test.tsx new file mode 100644 index 0000000000..b77523ba2f --- /dev/null +++ b/webapp/channels/src/components/widgets/inputs/input/input.test.tsx @@ -0,0 +1,48 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {mount, shallow} from 'enzyme'; +import React from 'react'; + +import OverlayTrigger from 'components/overlay_trigger'; + +import Input from './input'; + +describe('components/widgets/inputs/Input', () => { + test('should match snapshot', () => { + const wrapper = shallow( + , + ); + + expect(wrapper).toMatchSnapshot(); + }); + + test('should render with clearable enabled', () => { + const value = 'value'; + const clearableTooltipText = 'tooltip text'; + const onClear = jest.fn(); + + const wrapper = shallow( + , + ); + + const clear = wrapper.find('.Input__clear'); + expect(clear.length).toEqual(1); + expect(wrapper.find('CloseCircleIcon').length).toEqual(1); + + const tooltip = wrapper.find(OverlayTrigger); + expect(tooltip.length).toEqual(1); + + const overlay = mount(tooltip.prop('overlay')); + expect(overlay.text()).toEqual(clearableTooltipText); + + clear.first().simulate('mousedown'); + + expect(onClear).toHaveBeenCalledTimes(1); + }); +}); diff --git a/webapp/channels/src/components/widgets/inputs/input/input.tsx b/webapp/channels/src/components/widgets/inputs/input/input.tsx index 1a55dbb9f4..bcc66e4a20 100644 --- a/webapp/channels/src/components/widgets/inputs/input/input.tsx +++ b/webapp/channels/src/components/widgets/inputs/input/input.tsx @@ -3,10 +3,14 @@ import React, {useState, useEffect} from 'react'; import {useIntl} from 'react-intl'; - import classNames from 'classnames'; -import {ItemStatus} from 'utils/constants'; +import {CloseCircleIcon} from '@mattermost/compass-icons/components'; + +import OverlayTrigger from 'components/overlay_trigger'; +import Tooltip from 'components/tooltip'; + +import Constants, {ItemStatus} from 'utils/constants'; import './input.scss'; @@ -32,6 +36,9 @@ interface InputProps extends React.InputHTMLAttributes { useLegend?: boolean; customMessage?: CustomMessageInputType; inputSize?: SIZE; + clearable?: boolean; + clearableTooltipText?: string; + onClear?: () => void; } const Input = React.forwardRef(( @@ -56,9 +63,12 @@ const Input = React.forwardRef(( maxLength, inputSize = SIZE.MEDIUM, disabled, + clearable, + clearableTooltipText, onFocus, onBlur, onChange, + onClear, ...otherProps }: InputProps, ref?: React.Ref, @@ -109,6 +119,12 @@ const Input = React.forwardRef(( } }; + const handleOnClear = () => { + if (onClear) { + onClear(); + } + }; + const validateInput = () => { if (!required || (value !== null && value !== '')) { return; @@ -121,6 +137,26 @@ const Input = React.forwardRef(( const error = customInputLabel?.type === 'error'; const limitExceeded = limit && value && !Array.isArray(value) ? value.toString().length - limit : 0; + const clearButton = value && clearable ? ( +
    + + {clearableTooltipText || formatMessage({id: 'widget.input.clear', defaultMessage: 'Clear'})} + + )} + > + + +
    + ) : null; + return (
    )} {inputSuffix} + {clearButton}
    {addon} diff --git a/webapp/channels/src/components/widgets/menu/menu_items/menu_cloud_trial.test.tsx b/webapp/channels/src/components/widgets/menu/menu_items/menu_cloud_trial.test.tsx index 214b51d0f4..5ed6855015 100644 --- a/webapp/channels/src/components/widgets/menu/menu_items/menu_cloud_trial.test.tsx +++ b/webapp/channels/src/components/widgets/menu/menu_items/menu_cloud_trial.test.tsx @@ -205,7 +205,6 @@ describe('components/widgets/menu/menu_items/menu_cloud_trial', () => { }; const store = mockStore(state); const wrapper = mountWithIntl(); - console.log(wrapper.debug()); expect(wrapper.find('.open-learn-more-trial-modal').exists()).toEqual(true); }); diff --git a/webapp/channels/src/i18n/bg.json b/webapp/channels/src/i18n/bg.json index c5331d8e9c..d7bdea40b7 100644 --- a/webapp/channels/src/i18n/bg.json +++ b/webapp/channels/src/i18n/bg.json @@ -11,7 +11,6 @@ "about.enterpriseEditione1": "Корпоративна версия", "about.hash": "Хеш на компилация:", "about.hashee": "Хеш на EE компилация:", - "about.hashwebapp": "Webapp билд хеш:", "about.licensed": "Лицензиран на:", "about.notice": "Използването на Mattermost е възможно благодарение на софтуер с отворен код използван в нашите сървърно, настолно и мобилно приложения.", "about.privacy": "Политика за поверителност", @@ -242,7 +241,6 @@ "admin.billing.history.title": "История на плащания", "admin.billing.history.total": "Общо", "admin.billing.history.transactions": "Транзакции", - "admin.billing.history.usersAndRates": "{fullUsers} потребители на пълна тарифа, {partialUsers} потребители с частично таксуване", "admin.billing.payment_info.add": "Добави кредитна карта", "admin.billing.payment_info.billingAddress": "Адрес за фактуриране", "admin.billing.payment_info.cardBrandAndDigits": "{brand} завършва на {digits}", @@ -333,8 +331,6 @@ "admin.billing.subscriptions.billing_summary.lastInvoice.taxes": "Такси", "admin.billing.subscriptions.billing_summary.lastInvoice.title": "Последна фактура", "admin.billing.subscriptions.billing_summary.lastInvoice.total": "Общо", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCount": " x {users} потребителя", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCountPartial": "{users} потребителя", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges": "Какво представляват частичните такси?", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges.message": "Потребителите, които не са били активирани през цялото време на месеца, се таксуват с пропорционална месечна ставка.", "admin.billing.subscriptions.billing_summary.noBillingHistory.description": "В бъдеще това е мястото, където ще се показва обобщение на вашите последни таксувания.", @@ -2363,7 +2359,6 @@ "app.channel.post_update_channel_purpose_message.removed": "{username} премахна целта на канала (беше: {old})", "app.channel.post_update_channel_purpose_message.updated_from": "{username} актуализира целта на канала от: {old} до: {new}", "app.channel.post_update_channel_purpose_message.updated_to": "{username} актуализира целта на канала до: {new}", - "app.plugin.marketplace_plugins.app_error": "Грешка при свързване със сървъра на пазара. Моля, проверете настройките си в [Системна конзола]({siteURL}/admin_console/plugins/plugin_management).", "apps.error": "Грешка: {error}", "apps.error.command.field_missing": "Липсват задължителни полета: `{fieldName}`.", "apps.error.command.same_channel": "Повтаря се каналът с поле `{fieldName}`: `{option}`.", @@ -3415,7 +3410,6 @@ "login_mfa.token": "МФУ маркер", "manage_channel_groups_modal.search_placeholder": "Търсене на групи", "manage_team_groups_modal.search_placeholder": "Търсене на групи", - "marketplace_list.count_total_page": "{startCount, number} - {endCount, number} {total, plural, one {приставка} other {приставки}} от {total, number} общо", "marketplace_modal.install_plugins": "Инсталирай приставки", "marketplace_modal.installing": "Инсталиране ...", "marketplace_modal.list.configure": "Конфигуриране", diff --git a/webapp/channels/src/i18n/de.json b/webapp/channels/src/i18n/de.json index 01990e3010..4654115f50 100644 --- a/webapp/channels/src/i18n/de.json +++ b/webapp/channels/src/i18n/de.json @@ -11,7 +11,6 @@ "about.enterpriseEditione1": "Enterprise Edition", "about.hash": "Build Hashwert:", "about.hashee": "EE Build Hashwert:", - "about.hashwebapp": "Webapp-Build-Hash:", "about.licensed": "Lizenziert für:", "about.notice": "Mattermost wird durch Open Source Software möglich gemacht, die in unseren Server-, Desktop- und mobilen Apps verwendet wird.", "about.privacy": "Datenschutzbedingungen", @@ -265,10 +264,7 @@ "admin.billing.history.allPaymentsShowHere": "Alle deine Rechnungen werden hier angezeigt", "admin.billing.history.date": "Datum", "admin.billing.history.description": "Beschreibung", - "admin.billing.history.fractionalAndRatedUsers": "{fractionalUsers} gebührenpflichtige Nutzer, {fullUsers} Nutzer zum vollen Tarif, {partialUsers} Nutzer mit Teilgebühren", - "admin.billing.history.fractionalUsers": "{fractionalUsers} Benutzer", "admin.billing.history.noBillingHistory": "Hier wird in Zukunft deine Abrechnungshistorie angezeigt.", - "admin.billing.history.onPremUsers": "{num} Benutzer", "admin.billing.history.pageInfo": "{startRecord} - {endRecord} von {totalRecords}", "admin.billing.history.paid": "Bezahlt", "admin.billing.history.paymentFailed": "Zahlungsvorgang fehlgeschlagen", @@ -278,7 +274,6 @@ "admin.billing.history.title": "Abrechnungshistorie", "admin.billing.history.total": "Total", "admin.billing.history.transactions": "Transaktionen", - "admin.billing.history.usersAndRates": "{fullUsers} Nutzer auf Vollrate, {partialUsers} Nutzer auf Teilrate", "admin.billing.payment_info.add": "Kreditkarte hinzufügen", "admin.billing.payment_info.billingAddress": "Rechnungsadresse", "admin.billing.payment_info.cardBrandAndDigits": "{brand} endet mit {digits}", @@ -416,8 +411,6 @@ "admin.billing.subscriptions.billing_summary.lastInvoice.taxes": "Steuern", "admin.billing.subscriptions.billing_summary.lastInvoice.title": "Letzte Rechnung", "admin.billing.subscriptions.billing_summary.lastInvoice.total": "Total", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCount": " x {users} Benutzer", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCountPartial": "{users} Benutzer", "admin.billing.subscriptions.billing_summary.lastInvoice.viewInvoice": "Rechnung ansehen", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges": "Was sind teilweise verrechnete Gebühren?", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges.message": "Benutzer, die nicht für die volle Dauer des Monats freigeschaltet waren, werden mit einem anteiligen Monatsrate berechnet.", @@ -1338,8 +1331,9 @@ "admin.license.renewalCard.reviewNumbers": "Überprüfe deine Zahlen unten, um sicherzustellen, dass du für die richtige Anzahl von Benutzern erneuerst.", "admin.license.renewalCard.usersNumbers": "**Aktive Benutzer:** {activeUsersNum}", "admin.license.title": "Edition und Lizenz", - "admin.license.trial-request.accept-terms": "Indem ich auf Starte Testl klicke, stimme ich dem Mattermost Software und Services License Agreement, Datenschutz-Richtlinien und dem Erhalt von Produkt-E-Mails zu.", + "admin.license.trial-request.accept-terms": "Indem ich auf Starte Test klicke, stimme ich dem Mattermost Software und Services License Agreement, Datenschutz-Richtlinien und dem Erhalt von Produkt-E-Mails zu.", "admin.license.trial-request.embargoed": "Wir konnten die Anfrage auf Grund von Embargo-Beschränkungen für bestimmte Länder nicht verarbeiten. Lerne mehr in unserer Dokumentation, oder kontaktiere legal@mattermost.com für Fragen rund um Exportbeschränkungen.", + "admin.license.trial-request.embargoed.button": "Schliessen", "admin.license.trial-request.startTrial": "Starte Test", "admin.license.trial-request.title": "Teste Mattermost Enterprise Edition kostenlos für die nächsten 30 Tage. Keine Kaufverpflichtung oder Kreditkarte erforderlich. ", "admin.license.trialCard.contactSales": "Vertrieb kontaktieren", @@ -1359,7 +1353,6 @@ "admin.license.upload-modal.file": "Datei", "admin.license.upload-modal.subtitle": "Lade eine Lizenzschlüssel für die Mattermost Enterprise Edition hoch, um den Server zu aktualisieren. ", "admin.license.upload-modal.successfulUpgrade": "Erfolgreiche Aktualisierung!", - "admin.license.upload-modal.successfulUpgradeText": "Du hast den {skuName} Plan für {licensedUsersNum, number} Benutzer aktualisiert. Diese Änderung ist aktiv vom {startsAt} bis zum {expiresAt}. ", "admin.license.upload-modal.title": "Lizenzschlüssel hochladen", "admin.license.uploadFile": "Datei hochladen", "admin.license.warn.renew": "Erneuern", @@ -2563,6 +2556,9 @@ "admin.webserverModeUncompressed": "Unkomprimiert", "admin.webserverModeUncompressedDescription": "Der Mattermost-Server wird statische Dateien unkomprimiert bereitstellen.", "admin_settings.save_unsaved_changes": "Bitte speichere ungespeicherte Änderungen zuerst", + "air_gapped_modal.close": "Schliessen", + "air_gapped_modal.description": "Um deinen Test zu starten, besuche bitte {link} und fordere einen Testschlüssel an.", + "air_gapped_modal.title": "Testschlüssel anfordern", "alert_banner.tooltipCloseBtn": "Schliessen", "analytics.chart.loading": "Lade...", "analytics.chart.meaningful": "Nicht genügend Daten für eine aussagekräftige Darstellung.", @@ -2576,7 +2572,6 @@ "analytics.system.postTypes": "Nachrichten, Dateien und Hashtags", "analytics.system.privateGroups": "Private Kanäle", "analytics.system.publicChannels": "Öffentliche Kanäle", - "analytics.system.seatsPurchased": "Total bezahlte Nutzer", "analytics.system.skippedIntensiveQueries": "Um die Performance zu maximieren, sind einige Statistiken deaktiviert. Du kannst sie in der config.json reaktivieren.", "analytics.system.textPosts": "Nur-Text Beiträge", "analytics.system.title": "Systemstatistiken", @@ -2596,7 +2591,6 @@ "analytics.team.activeUsers": "Aktive Benutzer mit Beiträgen", "analytics.team.newlyCreated": "Neu erstellte Benutzer", "analytics.team.noTeams": "Auf diesem Server gibt es keine Teams, für die Statistiken eingesehen werden können.", - "analytics.team.overageUsersSeats": "Dies übersteigt die Zahl der bezahlten Nutzer", "analytics.team.privateGroups": "Private Kanäle", "analytics.team.publicChannels": "Öffentliche Kanäle", "analytics.team.recentUsers": "Zuletzt aktive Benutzer", @@ -2648,7 +2642,7 @@ "app.channel.post_update_channel_purpose_message.removed": "{username} hat den Kanalzweck entfernt (war: {old})", "app.channel.post_update_channel_purpose_message.updated_from": "{username} hat den Kanalzweck aktualisiert von: {old} auf: {new}", "app.channel.post_update_channel_purpose_message.updated_to": "{username} hat den Kanalzweck geändert zu: {new}", - "app.plugin.marketplace_plugins.app_error": "Fehler beim Verbinden mit dem Marktplatz-Server. Bitte überprüfe deine Einstellungen in der [Systemkonsole]({siteURL}/admin_console/plugins/plugin_management).", + "app_bar.marketplace": "App-Marktplatz", "apps.error": "Fehler: {error}", "apps.error.command.field_missing": "Erforderliche Felder fehlen: `{fieldName}`.", "apps.error.command.same_channel": "Kanal wiederholt für Feld `{fieldName}`: `{option}`.", @@ -3437,6 +3431,9 @@ "filtered_user_list.userStatus": "Benutzerstatus:", "flag_post.flag": "Zur Nachverfolgung markieren", "flag_post.unflag": "Markierung entfernen", + "footer_pagination.count": "Zeige {startCount, number}-{endCount, number} von {total, number}", + "footer_pagination.next": "Weiter", + "footer_pagination.prev": "Zurück", "forward_post_button.label": "Weiterleiten", "forward_post_modal.button.cancel": "Abbrechen", "forward_post_modal.button.forward": "Weiterleiten", @@ -4081,7 +4078,7 @@ "mark_all_threads_as_read_modal.title": "Alle deine Unterhaltungen als gelesen markieren?", "marketplace_command.disabled": "Der Marktplatz ist deaktiviert. Bitte kontaktiere deinen Systemadmin für Details.", "marketplace_command.no_permission": "Du hast nicht die erforderlichen Berechtigungen um auf den Marktplatz zuzugreifen.", - "marketplace_list.count_total_page": "{startCount, number} - {endCount, number} {total, plural, one {Plugin} other {Plugins}} von insgesamt {total, number}", + "marketplace_modal.app_error": "Fehler beim Verbinden mit dem Marktplatz-Server. Bitte überprüfe deine Einstellungen in der Systemkonsole.", "marketplace_modal.install_plugins": "Plugins installieren", "marketplace_modal.installing": "Installiere...", "marketplace_modal.list.configure": "Konfigurieren", @@ -4099,12 +4096,13 @@ "marketplace_modal.list.update_confirmation.message.warning_major_version": "Dieses Update kann inkompatible Änderungen enthalten.", "marketplace_modal.list.update_confirmation.message.warning_major_version_with_release_notes": "Dieses Update kann inkompatible Änderungen enthalten. Prüfe die [Release Notes](!{releaseNotesUrl}) vor dem Upgrade.", "marketplace_modal.list.update_confirmation.title": "Plugin-Update bestätigen", - "marketplace_modal.no_plugins": "Zurzeit sind keine Plugins verfügbar.", - "marketplace_modal.no_plugins_installed": "Du hast keine Plugins installiert.", - "marketplace_modal.search": "Plugin-Marktplatz durchsuchen", + "marketplace_modal.no_plugins": "Keine Plugins gefunden", + "marketplace_modal.no_plugins_installed": "Keine installierten Plugins gefunden", + "marketplace_modal.search": "Marktplatz durchsuchen", "marketplace_modal.tabs.all_listing": "Alle", - "marketplace_modal.tabs.installed_listing": "Installierte", - "marketplace_modal.title": "Plugin-Marktplatz", + "marketplace_modal.tabs.installed_listing": "Installiert ({count})", + "marketplace_modal.title": "App-Marktplatz", + "marketplace_modal_list.no_plugins_filter": "Keine Ergebnisse für \"{filter}\"", "members_popover.button.message": "Nachricht", "menu.cloudFree.enterpriseTrialDescription": "Dein Test ist aktiv bis zum {trialEndDay}. Entdecke unsere besten Enterprise Funktionen. Erfahre mehr", "menu.cloudFree.enterpriseTrialTitle": "Enterprise Test", @@ -4156,22 +4154,13 @@ "modal.manual_status.title_offline": "Dein Status wurde auf \"Offline\" gesetzt", "modal.manual_status.title_ooo": "Dein Status ist auf \"Nicht im Büro\" gesetzt", "more.details": "Mehr Details", - "more_channels.channel_purpose": "Kanal-Informationen: Mitgliedschaftsindikator: Beigetreten, Mitglieder {memberCount}, Zweck: {channelPurpose}", - "more_channels.count": "{count} Ergebnisse", - "more_channels.count_one": "1 Ergebnis", - "more_channels.count_zero": "Keine Ergebnisse", "more_channels.create": "Kanal erstellen", - "more_channels.hide_joined": "Verbundene Kanäle ausblenden", - "more_channels.hide_joined_checked": "Kontrollkästchen Verbundene Kanäle ausblenden, aktiviert", - "more_channels.hide_joined_not_checked": "Kontrollkästchen Verbundene Kanäle ausblenden, deaktiviert", - "more_channels.joined": "Verknüpft", - "more_channels.membership_indicator": "Mitgliedschaftsindikator: Beigetreten", + "more_channels.createClick": "Klicke auf 'Neuen Kanal erstellen' um einen Neuen zu erzeugen", + "more_channels.join": "Beitreten", + "more_channels.joining": "Beitreten...", "more_channels.next": "Weiter", - "more_channels.noArchived": "Keine archivierten Kanäle", "more_channels.noMore": "Keine Ergebnisse für \"{text}\"", - "more_channels.noPublic": "Keine öffentlichen Kanäle", "more_channels.prev": "Zurück", - "more_channels.searchError": "Versuche, nach anderen Stichworten zu suchen, auf Tippfehlern zu prüfen oder die Filter anzupassen.", "more_channels.show_archived_channels": "Anzeigen: Archivierte Kanäle", "more_channels.show_public_channels": "Anzeigen: Öffentliche Kanäle", "more_channels.title": "Weitere Kanäle", @@ -4236,7 +4225,7 @@ "navbar_dropdown.logout": "Abmelden", "navbar_dropdown.manageGroups": "Gruppen verwalten", "navbar_dropdown.manageMembers": "Mitglieder verwalten", - "navbar_dropdown.marketplace": "Plugin-Marktplatz", + "navbar_dropdown.marketplace": "App-Marktplatz", "navbar_dropdown.menuAriaLabel": "Hauptmenü", "navbar_dropdown.nativeApps": "Apps herunterladen", "navbar_dropdown.profileSettings": "Profil", @@ -4294,9 +4283,10 @@ "onboardingTask.checklist.downloads": "Jetzt, da du alles konfiguriert hast, lade unsere Apps runter.", "onboardingTask.checklist.higher_security_features": "Interessiert an unseren Hochsicherheitsfunktionen?", "onboardingTask.checklist.main_subtitle": "Legen wir los.", + "onboardingTask.checklist.no_thanks": "Nein, danke", "onboardingTask.checklist.start_enterprise_now": "Starte jetzt Deinen kostenfreien Enterprise-Test!", "onboardingTask.checklist.task_complete_your_profile": "Vervollständige dein Profil.", - "onboardingTask.checklist.task_create_from_work_template": "Erstellen mit einer Vorlage - richte einen Kanal mit verknüpften Boards und Playbooks ein.", + "onboardingTask.checklist.task_create_from_work_template": "Erstellen aus einer Vorlage", "onboardingTask.checklist.task_download_mm_apps": "Lade die Desktop-, Tablet- und Handy-Apps runter.", "onboardingTask.checklist.task_explore_other_tools_in_platform": "Erkunde andere Tools auf der Plattform.", "onboardingTask.checklist.task_invite_team_members": "Lade Teammitglieder in den Arbeitsbereich ein.", @@ -4577,7 +4567,6 @@ "pricing_modal.planSummary.professional": "Skalierbare Lösungen für wachsende Teams", "pricing_modal.plan_label_trialDays": "{days} TAGE, DIE IM TEST VERBLEIBEN", "pricing_modal.price.freeForever": "Kostenlos für immer", - "pricing_modal.rate.userPerMonth": "USD pro Benutzer/Monat {br}(jährliche Abrechnung)", "pricing_modal.reviewDeploymentOptions": "Prüfe deine Bereitstellungsoptionen", "pricing_modal.start_trial.disclaimer": "Durch Auswahl von 30 Tage lang kostenlos testen, stimme ich dem Mattermost Software und Services License Agreement, der Datenschutz-Richtlinie und dem Erhalt von Produkt-E-Mails zu.", "pricing_modal.subtitle": "Wähle einen Plan um loszulegen", @@ -4715,12 +4704,9 @@ "self_hosted_signup.cta": "Aktualisieren", "self_hosted_signup.disclaimer": "Ich habe die Enterprise Edition Abonnementbedingungen gelesen und stimme ihnen zu.", "self_hosted_signup.error_invalid_number": "Gib eine gültige Anzahl von Plätzen ein", - "self_hosted_signup.error_max_seats": " Der Lizenzkauf unterstützt nur Käufe bis zu {num} Benutzern", - "self_hosted_signup.error_min_seats": "Dein Arbeitsbereich hat derzeit {num} Benutzer", "self_hosted_signup.failed_export.subtitle": "Wir werden die Dinge von unserer Seite aus überprüfen und uns innerhalb von 3 Tagen bei dir melden, sobald deine Lizenz genehmigt ist. In der Zwischenzeit kannst du gerne die kostenlose Version unseres Produkts weiter nutzen.", "self_hosted_signup.failed_export.title": "Deine Transaktion wird überprüft", "self_hosted_signup.license_applied": "Deine {planName} Lizenz wurde jetzt angewendet. {planName} Funktionen sind jetzt verfügbar und einsatzbereit.", - "self_hosted_signup.line_item_subtotal": "{num} Nutzer × 12 Mo.", "self_hosted_signup.organization": "Name der Organisation", "self_hosted_signup.progress_step.applying_license": "Übertrage deine {planName} Lizenz auf deine Mattermost-Instanz", "self_hosted_signup.progress_step.submitting_payment": "Übermittlung von Zahlungsinformationen", @@ -4730,10 +4716,10 @@ "self_hosted_signup.purchase_in_progress.by_self_restart": "Wenn du der Meinung bist, dass dies ein Fehler ist, starte den Kauf erneut.", "self_hosted_signup.purchase_in_progress.reset": "Kaufvorgang erneut starten", "self_hosted_signup.purchase_in_progress.title": "Kauf in Bearbeitung", + "self_hosted_signup.error_min_seats": "Dein Arbeitsbereich hat derzeit {num} Benutzer", "self_hosted_signup.retry": "Erneut versuchen", "self_hosted_signup.screening_description": "Wir werden die Dinge von unserer Seite aus überprüfen und uns innerhalb von 3 Tagen bei dir melden, sobald deine Lizenz genehmigt ist. In der Zwischenzeit kannst du gerne die kostenlose Version unseres Produkts weiter nutzen.", "self_hosted_signup.screening_title": "Deine Transaktion wird überprüft", - "self_hosted_signup.seats": "Benutzerplätze", "self_hosted_signup.signup_consequences": "Du erhältst eine Rechnung von heute. Deine Lizenz wird automatisch angewendet. Sieh, wie die Abrechnung funktioniert.", "self_hosted_signup.total": "Summe", "setting_item_max.cancel": "Abbrechen", @@ -4868,7 +4854,7 @@ "sidebar.directchannel.you": "{displayname} (Sie)", "sidebar.menu.item.notSelected": "nicht ausgewählt", "sidebar.menu.item.selected": "ausgewählt", - "sidebar.openDirectMessage": "Direktnachricht senden", + "sidebar.openDirectMessage": "Direktnachricht öffnen", "sidebar.show": "Anzeigen", "sidebar.sort": "Sortierung", "sidebar.sortedByRecencyLabel": "Letzte Aktivität", @@ -4883,6 +4869,8 @@ "sidebar.types.favorites": "FAVORITEN", "sidebar.types.unreads": "UNGELESENE", "sidebar.unreads": "Weitere Ungelesene", + "sidebar_left.addChannelsCta": "Kanäle hinzufügen", + "sidebar_left.add_channel_cta_dropdown.dropdownAriaLabel": "Kanal-Dropdown hinzufügen", "sidebar_left.add_channel_dropdown.browseChannels": "Kanäle durchsuchen", "sidebar_left.add_channel_dropdown.browseOrCreateChannels": "Kanäle erstellen oder durchsuchen", "sidebar_left.add_channel_dropdown.createCategory": "Neue Kategorie erstellen", @@ -4891,7 +4879,7 @@ "sidebar_left.add_channel_dropdown.invitePeople": "Personen einladen", "sidebar_left.add_channel_dropdown.invitePeopleExtraText": "Personen zum Team hinzufügen", "sidebar_left.add_channel_dropdown.work_template": "Aus einer Vorlage erstellen", - "sidebar_left.add_channel_dropdown.work_template_extra": "Richte einen Kanal mit verknüpften Boards und Playbooks ein", + "sidebar_left.add_channel_dropdown.work_template_extra": "Verknüpfe Kanäle, Boards und Playbooks miteinander", "sidebar_left.channel_filter.filterByUnread": "Gefiltert durch ungelesen", "sidebar_left.channel_filter.filterUnreadAria": "Ungelesen Filter", "sidebar_left.channel_filter.showAllChannels": "Alle Kanäle anzeigen", @@ -4931,6 +4919,7 @@ "sidebar_left.sidebar_channel_menu.unfavoriteChannel": "Nicht favorisieren", "sidebar_left.sidebar_channel_menu.unmuteChannel": "Stummschaltung aufheben", "sidebar_left.sidebar_channel_menu.unmuteConversation": "Stummschaltung aufheben", + "sidebar_left.sidebar_channel_navigator.addChannelsCta": "Kanäle hinzufügen", "sidebar_left.sidebar_channel_navigator.inviteUsers": "Benutzer einladen", "sidebar_right_menu.console": "Systemkonsole", "sidebar_right_menu.flagged": "Markierte Nachrichten", @@ -4979,13 +4968,21 @@ "start_trial.modal.gettingTrial": "Hole Test...", "start_trial.modal.loaded": "Geladen!", "start_trial.modal.loading": "Lade...", - "start_trial.modal_body": "Greife auf alle Plattform-Funktionen, inklusive erweiterter Sicherheit- und Unternehmens-Compliance, zu.", - "start_trial.modal_btn.nottnow": "Nicht jetzt", - "start_trial.modal_btn.start": "Starte dem freien 30-Tage Test", "start_trial.modal_btn.start_free_trial": "Kostenlose 30-Tage-Testversion starten", - "start_trial.modal_title": "Starte jetzt deine kostenlose Enterprise-Testversion", "start_trial.tutorialTip.desc": "Probiere unsere am meisten gefragten Premium Funktionen: Lege den Benutzerzugriff mit Gast-Konten fest, automatisiere Compliance Berichte und sende sichere, mobile Nur-ID Push-Nachrichten.", "start_trial.tutorialTip.title": "Teste kostenfrei Premium Funktionen", + "start_trial_form.company_name": "Firmenname", + "start_trial_form.company_size": "Größe des Unternehmens", + "start_trial_form.disclaimer": "Mit Auswählen von Starte Test, stimme ich dem Mattermost Software Evaluation Agreement und der Datenschutzrichtlinie zu, und erhalte Produktemails.", + "start_trial_form.email": "Geschäftliche E-Mail", + "start_trial_form.invalid_business_email": "Bitte gib eine gültige geschäftliche E-Mail-Adresse ein.", + "start_trial_form.modal_body": "Nur ein paar kurze Hinweise, die uns helfen, deine Testerfahrung individuell zu gestalten", + "start_trial_form.modal_btn.start": "Test starten", + "start_trial_form.modal_title": "Test starten", + "start_trial_form.name": "Name", + "start_trial_form_modal.failureModal.subtitle": "Es gab ein Problem bei der Bearbeitung deiner Testanfrage.", + "start_trial_form_modal.failureModal.subtitle2": "Bitte versuche es erneut oder kontaktiere den Support.", + "start_trial_form_modal.failureModal.title": "Bitte versuche es erneut", "status_dropdown.dnd_sub_menu_header": "Benachrichtigungen ausschalten bis:", "status_dropdown.dnd_sub_menu_item.custom": "Benutzerdefiniert", "status_dropdown.dnd_sub_menu_item.one_hour": "Eine Stunde", @@ -5669,6 +5666,7 @@ "welcome_post_renderer.user_message.first_paragraph": "Mattermost ist eine Open-Source-Plattform für sichere Kommunikation, Zusammenarbeit und Orchestrierung der Arbeit über Tools und Teams hinweg.", "welcome_post_renderer.user_message.second_paragraph": "Hier findest Du eine Liste von Befehlen, die Du verwenden kannst, um sich mit der Plattform vertraut zu machen.", "welcome_post_renderer.user_message.title": "Willkommen bei Mattermost! :rocket:", + "widget.input.clear": "Leeren", "widget.input.required": "Dieses Feld wird benötigt", "widget.passwordInput.createPassword": "Wähle ein Passwort", "widget.passwordInput.password": "Passwort", @@ -5687,7 +5685,7 @@ "work_templates.customize.name_label_channels_playbooks": "Benenne deinen Kanal und Playbook", "work_templates.customize.private_playbook_license_issue": "Für private Playbooks ist eine Enterprise-Lizenz erforderlich.", "work_templates.customize.visibility_title": "Wer soll Zugang dazu haben?", - "work_templates.menu.modal_title": "Beginne mit einer Vorlage", + "work_templates.menu.modal_title": "Erstellen aus einer Vorlage", "work_templates.menu.quick_use": "Schneller Einsatz", "work_templates.menu.template_title": "VORLAGE", "work_templates.menu.usecase_boards_count": "{boardsCount, plural, =1 {# Board} other {# Boards}}", diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index b6cdf2c336..522d68ae64 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -265,20 +265,20 @@ "admin.billing.history.allPaymentsShowHere": "All of your invoices will be shown here", "admin.billing.history.date": "Date", "admin.billing.history.description": "Description", - "admin.billing.history.fractionalAndRatedUsers": "{fractionalUsers} metered users, {fullUsers} users at full rate, {partialUsers} users with partial charges", - "admin.billing.history.fractionalUsers": "{fractionalUsers} users", + "admin.billing.history.fractionalAndRatedSeats": "{fractionalSeats} metered seats, {fullSeats} seats at full rate, {partialSeats} seats with partial charges", + "admin.billing.history.fractionalSeats": "{fractionalUsers} seats", "admin.billing.history.noBillingHistory": "In the future, this is where your billing history will show.", - "admin.billing.history.onPremUsers": "{num} users", + "admin.billing.history.onPremSeats": "{num} seats", "admin.billing.history.pageInfo": "{startRecord} - {endRecord} of {totalRecords}", "admin.billing.history.paid": "Paid", "admin.billing.history.paymentFailed": "Payment failed", "admin.billing.history.pending": "Pending", + "admin.billing.history.seatsAndRates": "{fullUsers} seats at full rate, {partialUsers} seats with partial charges", "admin.billing.history.seeHowBillingWorks": "See how billing works", "admin.billing.history.status": "Status", "admin.billing.history.title": "Billing History", "admin.billing.history.total": "Total", "admin.billing.history.transactions": "Transactions", - "admin.billing.history.usersAndRates": "{fullUsers} users at full rate, {partialUsers} users with partial charges", "admin.billing.payment_info_display.allCardsAccepted": "All major credit cards are accepted.", "admin.billing.payment_info_display.noPaymentInfo": "There are currently no credit cards on file.", "admin.billing.payment_info_display.savedPaymentDetails": "Your saved payment details", @@ -412,12 +412,12 @@ "admin.billing.subscriptions.billing_summary.lastInvoice.paid": "Paid", "admin.billing.subscriptions.billing_summary.lastInvoice.partialCharges": "Partial charges", "admin.billing.subscriptions.billing_summary.lastInvoice.pending": "Pending", + "admin.billing.subscriptions.billing_summary.lastInvoice.seatCount": " x {seats} seats", + "admin.billing.subscriptions.billing_summary.lastInvoice.seatCountPartial": "{seats} seats", "admin.billing.subscriptions.billing_summary.lastInvoice.seeBillingHistory": "See Billing History", "admin.billing.subscriptions.billing_summary.lastInvoice.taxes": "Taxes", "admin.billing.subscriptions.billing_summary.lastInvoice.title": "Last Invoice", "admin.billing.subscriptions.billing_summary.lastInvoice.total": "Total", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCount": " x {users} users", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCountPartial": "{users} users", "admin.billing.subscriptions.billing_summary.lastInvoice.viewInvoice": "View Invoice", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges": "What are partial charges?", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges.message": "Users who have not been enabled for the full duration of the month are charged at a prorated monthly rate.", @@ -1360,7 +1360,7 @@ "admin.license.upload-modal.file": "File", "admin.license.upload-modal.subtitle": "Upload a license key for Mattermost Enterprise Edition to upgrade this server. ", "admin.license.upload-modal.successfulUpgrade": "Successful Upgrade!", - "admin.license.upload-modal.successfulUpgradeText": "You have upgraded to the {skuName} plan for {licensedUsersNum, number} users. This is effective from {startsAt} until {expiresAt}. ", + "admin.license.upload-modal.successfulUpgradeText": "You have upgraded to the {skuName} plan for {licensedUsersNum, number} seats. This is effective from {startsAt} until {expiresAt}. ", "admin.license.upload-modal.title": "Upload a License Key", "admin.license.uploadFile": "Upload File", "admin.license.warn.renew": "Renew", @@ -2579,7 +2579,7 @@ "analytics.system.postTypes": "Posts, Files and Hashtags", "analytics.system.privateGroups": "Private Channels", "analytics.system.publicChannels": "Public Channels", - "analytics.system.seatsPurchased": "Total paid users", + "analytics.system.seatsPurchased": "Licensed Seats", "analytics.system.skippedIntensiveQueries": "To maximize performance, some statistics are disabled. You can re-enable them in config.json.", "analytics.system.textPosts": "Posts with Text-only", "analytics.system.title": "System Statistics", @@ -2599,7 +2599,7 @@ "analytics.team.activeUsers": "Active Users With Posts", "analytics.team.newlyCreated": "Newly Created Users", "analytics.team.noTeams": "This server has no teams for which to view statistics.", - "analytics.team.overageUsersSeats": "This exceeds total paid users", + "analytics.team.overageUsersSeats": "This exceeds total paid seats", "analytics.team.privateGroups": "Private Channels", "analytics.team.publicChannels": "Public Channels", "analytics.team.recentUsers": "Recent Active Users", @@ -2652,7 +2652,6 @@ "app.channel.post_update_channel_purpose_message.removed": "{username} removed the channel purpose (was: {old})", "app.channel.post_update_channel_purpose_message.updated_from": "{username} updated the channel purpose from: {old} to: {new}", "app.channel.post_update_channel_purpose_message.updated_to": "{username} updated the channel purpose to: {new}", - "app.plugin.marketplace_plugins.app_error": "Error connecting to the marketplace server. Please check your settings in the [System Console]({siteURL}/admin_console/plugins/plugin_management).", "apps.error": "Error: {error}", "apps.error.command.field_missing": "Required fields missing: `{fieldName}`.", "apps.error.command.same_channel": "Channel repeated for field `{fieldName}`: `{option}`.", @@ -3441,6 +3440,9 @@ "filtered_user_list.userStatus": "User Status:", "flag_post.flag": "Save", "flag_post.unflag": "Remove from Saved", + "footer_pagination.count": "Showing {startCount, number}-{endCount, number} of {total, number}", + "footer_pagination.next": "Next", + "footer_pagination.prev": "Previous", "forward_post_button.label": "Forward", "forward_post_modal.button.cancel": "Cancel", "forward_post_modal.button.forward": "Forward", @@ -4085,8 +4087,9 @@ "mark_all_threads_as_read_modal.title": "Mark all your threads as read?", "marketplace_command.disabled": "The marketplace is disabled. Please contact your System Administrator for details.", "marketplace_command.no_permission": "You do not have the appropriate permissions to access the marketplace.", - "marketplace_list.count_total_page": "{startCount, number} - {endCount, number} {total, plural, one {plugin} other {plugins}} of {total, number} total", - "marketplace_modal.install_plugins": "Install Plugins", + "marketplace_modal_list.no_plugins_filter": "No results for \"{filter}\"", + "marketplace_modal.app_error": "Error connecting to the marketplace server. Please check your settings in the System Console.", + "marketplace_modal.install_plugins": "Install plugins", "marketplace_modal.installing": "Installing...", "marketplace_modal.list.configure": "Configure", "marketplace_modal.list.configure.plugin": "Configure {plugin}", @@ -4103,12 +4106,12 @@ "marketplace_modal.list.update_confirmation.message.warning_major_version": "This update may contain breaking changes.", "marketplace_modal.list.update_confirmation.message.warning_major_version_with_release_notes": "This update may contain breaking changes. Consult the [release notes](!{releaseNotesUrl}) before upgrading.", "marketplace_modal.list.update_confirmation.title": "Confirm Plugin Update", - "marketplace_modal.no_plugins": "There are no plugins available at this time.", - "marketplace_modal.no_plugins_installed": "You do not have any plugins installed.", - "marketplace_modal.search": "Search Marketplace", + "marketplace_modal.no_plugins": "No plugins found", + "marketplace_modal.no_plugins_installed": "No plugins installed found", + "marketplace_modal.search": "Search marketplace", "marketplace_modal.tabs.all_listing": "All", - "marketplace_modal.tabs.installed_listing": "Installed", - "marketplace_modal.title": "Marketplace", + "marketplace_modal.tabs.installed_listing": "Installed ({count})", + "marketplace_modal.title": "App Marketplace", "members_popover.button.message": "message", "menu.cloudFree.enterpriseTrialDescription": "Your trial is active until {trialEndDay}. Discover our top Enterprise features. Learn more", "menu.cloudFree.enterpriseTrialTitle": "Enterprise Trial", @@ -4164,11 +4167,11 @@ "more_channels.join": "Join", "more_channels.joining": "Joining...", "more_channels.next": "Next", - "more_channels.noMore": "No results for \"{text}\"", + "more_channels.noMore": "No more channels to join", "more_channels.prev": "Previous", "more_channels.show_archived_channels": "Channel Type: Archived", "more_channels.show_public_channels": "Channel Type: Public", - "more_channels.title": "Browse Channels", + "more_channels.title": "More Channels", "more_channels.view": "View", "more_direct_channels.directchannel.deactivated": "{displayname} - Deactivated", "more_direct_channels.directchannel.you": "{displayname} (you)", @@ -4251,6 +4254,9 @@ "navbar.viewPinnedPosts": "View Pinned Posts", "newChannelWithBoard.tutorialTip.description": "The board you just created can be quickly accessed by clicking on the Boards icon in the App bar. You can view the boards that are linked to this channel in the right-hand sidebar and open one in full view.", "newChannelWithBoard.tutorialTip.title": "Access linked boards from the App Bar", + "newsletter_optin.checkmark.text": "I would like to receive Mattermost security updates via newsletter. Data Terms and Conditions apply", + "newsletter_optin.desc": "Sign up at {link}.", + "newsletter_optin.title": "Interested in receiving Mattermost security updates via newsletter?", "next_steps_view.welcomeToMattermost": "Welcome to Mattermost", "no_results.channel_files_filtered.subtitle": "This channel doesn't contains any file with the selected file format.", "no_results.channel_files_filtered.title": "No files found", @@ -4573,7 +4579,7 @@ "pricing_modal.planSummary.free": "Increased productivity for small teams", "pricing_modal.planSummary.professional": "Scalable solutions for growing teams", "pricing_modal.price.freeForever": "Free forever", - "pricing_modal.rate.userPerMonth": "USD per user/month {br}(billed annually)", + "pricing_modal.rate.seatPerMonth": "USD per seat/month {br}(billed annually)", "pricing_modal.reviewDeploymentOptions": "Review deployment options", "pricing_modal.start_trial.disclaimer": "By selecting Try free for 30 days, I agree to the Mattermost Software and Services License Agreement, Privacy Policy, and receiving product emails.", "pricing_modal.subtitle": "Choose a plan to get started", @@ -4711,12 +4717,12 @@ "self_hosted_signup.cta": "Upgrade", "self_hosted_signup.disclaimer": "I have read and agree to the Enterprise Edition Subscription Terms", "self_hosted_signup.error_invalid_number": "Enter a valid number of seats", - "self_hosted_signup.error_max_seats": " license purchase only supports purchases up to {num} users", + "self_hosted_signup.error_max_seats": " license purchase only supports purchases up to {num} seats", "self_hosted_signup.error_min_seats": "Your workspace currently has {num} users", "self_hosted_signup.failed_export.subtitle": "We will check things on our side and get back to you within 3 days once your license is approved. In the meantime, please feel free to continue using the free version of our product.", "self_hosted_signup.failed_export.title": "Your transaction is being reviewed", "self_hosted_signup.license_applied": "Your {planName} license has now been applied. {planName} features are now available and ready to use.", - "self_hosted_signup.line_item_subtotal": "{num} users × 12 mo.", + "self_hosted_signup.line_item_subtotal": "{num} seats × 12 mo.", "self_hosted_signup.organization": "Organization Name", "self_hosted_signup.progress_step.applying_license": "Applying your {planName} license to your Mattermost instance", "self_hosted_signup.progress_step.submitting_payment": "Submitting payment information", @@ -4729,7 +4735,7 @@ "self_hosted_signup.retry": "Try again", "self_hosted_signup.screening_description": "We will check things on our side and get back to you within 3 days once your license is approved. In the meantime, please feel free to continue using the free version of our product.", "self_hosted_signup.screening_title": "Your transaction is being reviewed", - "self_hosted_signup.seats": "User seats", + "self_hosted_signup.seats": "Seats", "self_hosted_signup.signup_consequences": "You will be billed today. Your license will be applied automatically. See how billing works.", "self_hosted_signup.total": "Total", "setting_item_max.cancel": "Cancel", @@ -5676,6 +5682,7 @@ "welcome_post_renderer.user_message.first_paragraph": "Mattermost is an open source platform for secure communication, collaboration, and orchestration of work across tools and teams.", "welcome_post_renderer.user_message.second_paragraph": "Here is a list of commands to use to try and get familiar with the platform.", "welcome_post_renderer.user_message.title": "Welcome to Mattermost! :rocket:", + "widget.input.clear": "Clear", "widget.input.required": "This field is required", "widget.passwordInput.createPassword": "Choose a Password", "widget.passwordInput.password": "Password", diff --git a/webapp/channels/src/i18n/en_AU.json b/webapp/channels/src/i18n/en_AU.json index 66c0e85690..de9bcd1f1c 100644 --- a/webapp/channels/src/i18n/en_AU.json +++ b/webapp/channels/src/i18n/en_AU.json @@ -11,7 +11,6 @@ "about.enterpriseEditione1": "Enterprise Edition", "about.hash": "Build Hash:", "about.hashee": "EE Build Hash:", - "about.hashwebapp": "Webapp Build Hash:", "about.licensed": "Licensed to:", "about.notice": "Mattermost is made possible by the open source software used in our server, desktop and mobile apps.", "about.privacy": "Privacy Policy", @@ -265,10 +264,10 @@ "admin.billing.history.allPaymentsShowHere": "All of your invoices will be shown here", "admin.billing.history.date": "Date", "admin.billing.history.description": "Description", - "admin.billing.history.fractionalAndRatedUsers": "{fractionalUsers} metered users, {fullUsers} users at full rate, {partialUsers} users with partial charges", - "admin.billing.history.fractionalUsers": "{fractionalUsers} users", + "admin.billing.history.fractionalAndRatedSeats": "{fractionalSeats} metered seats, {fullSeats} seats at full rate, {partialSeats} seats with partial charges", + "admin.billing.history.fractionalSeats": "{fractionalUsers} seats", "admin.billing.history.noBillingHistory": "In the future, this is where your billing history will show.", - "admin.billing.history.onPremUsers": "{num} users", + "admin.billing.history.onPremSeats": "{num} seats", "admin.billing.history.pageInfo": "{startRecord} - {endRecord} of {totalRecords}", "admin.billing.history.paid": "Paid", "admin.billing.history.paymentFailed": "Payment failed", @@ -278,7 +277,7 @@ "admin.billing.history.title": "Billing History", "admin.billing.history.total": "Total", "admin.billing.history.transactions": "Transactions", - "admin.billing.history.usersAndRates": "{fullUsers} users at full rate, {partialUsers} users with partial charges", + "admin.billing.history.seatsAndRates": "{fullSeats} seats at full rate, {partialSeats} seats with partial charges", "admin.billing.payment_info.add": "Add a Credit Card", "admin.billing.payment_info.billingAddress": "Billing Address", "admin.billing.payment_info.cardBrandAndDigits": "{brand} ending in {digits}", @@ -415,8 +414,8 @@ "admin.billing.subscriptions.billing_summary.lastInvoice.taxes": "Taxes", "admin.billing.subscriptions.billing_summary.lastInvoice.title": "Last Invoice", "admin.billing.subscriptions.billing_summary.lastInvoice.total": "Total", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCount": " x {users} users", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCountPartial": "{users} users", + "admin.billing.subscriptions.billing_summary.lastInvoice.seatCount": " x {seats} seats", + "admin.billing.subscriptions.billing_summary.lastInvoice.seatCountPartial": "{seats} seats", "admin.billing.subscriptions.billing_summary.lastInvoice.viewInvoice": "View Invoice", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges": "What are partial charges?", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges.message": "Users who have not been enabled for the full duration of the month are charged at a prorated monthly rate.", @@ -1357,7 +1356,7 @@ "admin.license.upload-modal.file": "File", "admin.license.upload-modal.subtitle": "Upload a licence key for Mattermost Enterprise Edition to upgrade this server. ", "admin.license.upload-modal.successfulUpgrade": "Upgrade successful!", - "admin.license.upload-modal.successfulUpgradeText": "You have upgraded to the {skuName} plan for {licensedUsersNum, number} users. This is effective from {startsAt} until {expiresAt}. ", + "admin.license.upload-modal.successfulUpgradeText": "You have upgraded to the {skuName} plan for {licensedUsersNum, number} seats. This is effective from {startsAt} until {expiresAt}. ", "admin.license.upload-modal.title": "Upload a Licence Key", "admin.license.uploadFile": "Upload File", "admin.license.warn.renew": "Renew", @@ -2572,7 +2571,7 @@ "analytics.system.postTypes": "Posts, Files and Hashtags", "analytics.system.privateGroups": "Private Channels", "analytics.system.publicChannels": "Public Channels", - "analytics.system.seatsPurchased": "Total paid users", + "analytics.system.seatsPurchased": "Licensed Seats", "analytics.system.skippedIntensiveQueries": "To maximise performance, some statistics are disabled. You can re-enable them in config.json.", "analytics.system.textPosts": "Posts with Text-only", "analytics.system.title": "System Statistics", @@ -2592,7 +2591,7 @@ "analytics.team.activeUsers": "Active Users With Posts", "analytics.team.newlyCreated": "Newly Created Users", "analytics.team.noTeams": "This server has no teams for which to view statistics.", - "analytics.team.overageUsersSeats": "This exceeds total paid users", + "analytics.team.overageUsersSeats": "This exceeds total paid seats", "analytics.team.privateGroups": "Private Channels", "analytics.team.publicChannels": "Public Channels", "analytics.team.recentUsers": "Recent Active Users", @@ -2644,7 +2643,6 @@ "app.channel.post_update_channel_purpose_message.removed": "{username} removed the channel purpose (was: {old})", "app.channel.post_update_channel_purpose_message.updated_from": "{username} updated the channel purpose from: {old} to: {new}", "app.channel.post_update_channel_purpose_message.updated_to": "{username} updated the channel purpose to: {new}", - "app.plugin.marketplace_plugins.app_error": "Error connecting to the marketplace server. Please check your settings in the [System Console]({siteURL}/admin_console/plugins/plugin_management).", "apps.error": "Error: {error}", "apps.error.command.field_missing": "Required fields missing: `{fieldName}`.", "apps.error.command.same_channel": "Channel repeated for field `{fieldName}`: `{option}`.", @@ -4077,7 +4075,6 @@ "mark_all_threads_as_read_modal.title": "Mark all your threads as read?", "marketplace_command.disabled": "The marketplace is disabled. Please contact your System Administrator for details.", "marketplace_command.no_permission": "You do not have the appropriate permissions to access the marketplace.", - "marketplace_list.count_total_page": "{startCount, number} - {endCount, number} {total, plural, one {plugin} other {plugins}} of {total, number} total", "marketplace_modal.install_plugins": "Install Plugins", "marketplace_modal.installing": "Installing...", "marketplace_modal.list.configure": "Configure", @@ -4540,7 +4537,7 @@ "pricing_modal.planSummary.professional": "Scalable solutions for growing teams", "pricing_modal.plan_label_trialDays": "{days} DAYS LEFT ON TRIAL", "pricing_modal.price.freeForever": "Free forever", - "pricing_modal.rate.userPerMonth": "USD per user/month {br}(billed annually)", + "pricing_modal.rate.seatPerMonth": "USD per seat/month {br}(billed annually)", "pricing_modal.reviewDeploymentOptions": "Review deployment options", "pricing_modal.start_trial.disclaimer": "By selecting Try free for 30 days, I agree to the Mattermost Software and Services Licence Agreement, Privacy Policy and receiving product emails.", "pricing_modal.subtitle": "Choose a plan to get started", @@ -4678,12 +4675,12 @@ "self_hosted_signup.cta": "Upgrade", "self_hosted_signup.disclaimer": "I have read and agree to the Enterprise Edition Subscription Terms", "self_hosted_signup.error_invalid_number": "Enter a valid number of seats", - "self_hosted_signup.error_max_seats": " licence purchase only supports purchases up to {num} users", + "self_hosted_signup.error_max_seats": " licence purchase only supports purchases up to {num} seats", "self_hosted_signup.error_min_seats": "Your workspace currently has {num} users", "self_hosted_signup.failed_export.subtitle": "You will receive an email within 3 days to confirm the approval of your licence. In the meantime, please feel free to continue using the free version of our product.", "self_hosted_signup.failed_export.title": "Your transaction is being reviewed", "self_hosted_signup.license_applied": "Your {planName} licence has now been applied. {planName} features are now available and ready to use.", - "self_hosted_signup.line_item_subtotal": "{num} users × 12 months.", + "self_hosted_signup.line_item_subtotal": "{num} seats × 12 months.", "self_hosted_signup.organization": "Organisation Name", "self_hosted_signup.progress_step.applying_license": "Applying your {planName} licence to your Mattermost instance", "self_hosted_signup.progress_step.submitting_payment": "Submitting payment information", @@ -4696,7 +4693,7 @@ "self_hosted_signup.retry": "Try again", "self_hosted_signup.screening_description": "You will receive an email within 3 days to confirm the approval of your licence. In the meantime, please feel free to continue using the free version of our product.", "self_hosted_signup.screening_title": "Your transaction is being reviewed", - "self_hosted_signup.seats": "User seats", + "self_hosted_signup.seats": "Seats", "self_hosted_signup.signup_consequences": "You will be billed today. Your licence will be applied automatically. See how billing works.", "self_hosted_signup.total": "Total", "setting_item_max.cancel": "Cancel", @@ -4942,11 +4939,7 @@ "start_trial.modal.gettingTrial": "Getting Trial...", "start_trial.modal.loaded": "Loaded!", "start_trial.modal.loading": "Loading...", - "start_trial.modal_body": "Access all platform features including advanced security and enterprise compliance.", - "start_trial.modal_btn.nottnow": "Not now", - "start_trial.modal_btn.start": "Start a free 30-day trial", "start_trial.modal_btn.start_free_trial": "Start a free 30-day trial", - "start_trial.modal_title": "Start your free Enterprise trial now", "start_trial.tutorialTip.desc": "Explore our most requested premium features. Determine user access with Guest Accounts, automate compliance reports, and send secure ID-only mobile push notifications.", "start_trial.tutorialTip.title": "Try our premium features for free", "status_dropdown.dnd_sub_menu_header": "Disable notifications until:", diff --git a/webapp/channels/src/i18n/es.json b/webapp/channels/src/i18n/es.json index 28de93f5e2..38f1141ad2 100644 --- a/webapp/channels/src/i18n/es.json +++ b/webapp/channels/src/i18n/es.json @@ -11,7 +11,6 @@ "about.enterpriseEditione1": "Edición Empresarial E1", "about.hash": "Hash de compilación:", "about.hashee": "Hash de compilación de EE:", - "about.hashwebapp": "Hash de compilación de la App Web:", "about.licensed": "Licenciado a:", "about.notice": "Mattermost es hecho posible gracias a software de código libre utilizado en nuestro servidor, desktop y apps móviles.", "about.privacy": "Política de Privacidad", @@ -259,8 +258,6 @@ "admin.billing.history.allPaymentsShowHere": "Todos sus pagos mensuales se mostrarán aquí", "admin.billing.history.date": "Fecha", "admin.billing.history.description": "Descripción", - "admin.billing.history.fractionalAndRatedUsers": "{fractionalUsers} usuarios medidos, {fullUsers} usuarios con tarifa completa, {partialUsers} usuarios con tarifa parcial", - "admin.billing.history.fractionalUsers": "{fractionalUsers} usuarios", "admin.billing.history.noBillingHistory": "En el futuro, aquí es donde se mostrará su historial de facturación.", "admin.billing.history.pageInfo": "{startRecord} - {endRecord} de {totalRecords}", "admin.billing.history.paid": "Pagado", @@ -271,7 +268,6 @@ "admin.billing.history.title": "Historial de facturación", "admin.billing.history.total": "Total", "admin.billing.history.transactions": "Transacciones", - "admin.billing.history.usersAndRates": "{fullUsers} usuarios a tarifa completa, {partialUsers} usuarios con cargos parciales", "admin.billing.payment_info.add": "Agregar tarjeta de crédito", "admin.billing.payment_info.billingAddress": "Dirección de Facturación", "admin.billing.payment_info.cardBrandAndDigits": "{brand} terminada en {digits}", @@ -387,8 +383,6 @@ "admin.billing.subscriptions.billing_summary.lastInvoice.taxes": "Impuestos", "admin.billing.subscriptions.billing_summary.lastInvoice.title": "Última factura", "admin.billing.subscriptions.billing_summary.lastInvoice.total": "Total", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCount": " x {users} usuarios", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCountPartial": "{users} usuarios", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges": "¿Qué son los cargos parciales?", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges.message": "Los usuarios que no se hayan habilitado durante todo el mes se les cobrará una tarifa mensual prorrateada.", "admin.billing.subscriptions.billing_summary.noBillingHistory.description": "En el futuro, aquí es donde se mostrará su resumen de factura más reciente.", @@ -1305,7 +1299,6 @@ "admin.license.upload-modal.file": "Archivo", "admin.license.upload-modal.subtitle": "Cargar una clave de licencia para Mattermost Enterprise Edition para actualizar este servidor. ", "admin.license.upload-modal.successfulUpgrade": "¡Actualización satisfactoria!", - "admin.license.upload-modal.successfulUpgradeText": "Has actualizado al plan {skuName} para los usuarios {licensedUsersNum, number}. Esto es efectivo a partir de {startsAt} y hasta {expiresAt}. ", "admin.license.upload-modal.title": "Cargar Clave de Licencia", "admin.license.uploadFile": "Cargar Archivo", "admin.license.warn.renew": "Renovar", @@ -2567,7 +2560,6 @@ "app.channel.post_update_channel_purpose_message.removed": "{username} eliminó el propósito del canal (era: {old})", "app.channel.post_update_channel_purpose_message.updated_from": "{username} ha actualizado el propósito del canal de: {old} a: {new}", "app.channel.post_update_channel_purpose_message.updated_to": "{username} ha actualizado el propósito del canal a: {new}", - "app.plugin.marketplace_plugins.app_error": "Error de conexión con el servidor del marketplace. Por favor revisa los ajustes en la [Consola del Sistema]({siteURL}/admin_console/plugins/plugin_management).", "apps.error": "Error: {error}", "apps.error.command.field_missing": "Faltan campos obligatorios: `{fieldName}`.", "apps.error.command.same_channel": "Canal repetido para el campo `{fieldName}`: `{option}`.", @@ -3828,7 +3820,6 @@ "manage_channel_groups_modal.search_placeholder": "Buscar grupos", "manage_team_groups_modal.search_placeholder": "Buscar grupos", "mark_all_threads_as_read_modal.cancel": "Cancelar", - "marketplace_list.count_total_page": "{startCount, number} - {endCount, number} {total, plural, one {plugin} other {plugins}} de {total, number} total", "marketplace_modal.install_plugins": "Instalar Plugins", "marketplace_modal.installing": "Instalando...", "marketplace_modal.list.configure": "Configurar", @@ -4176,7 +4167,6 @@ "pricing_modal.planSummary.enterprise": "Administración, seguridad y cumplimiento para grandes equipos", "pricing_modal.plan_label_trialDays": "{days} DÍAS RESTANTES DE LA PRUEBA", "pricing_modal.price.freeForever": "Libre por siempre", - "pricing_modal.rate.userPerMonth": "/usuario/mes", "pricing_modal.subtitle": "Elige un plan para empezar", "promote_to_user_modal.desc": "Esta acción promueve al huésped {username} a miembro. Esto permitirá que el usuario se pueda unir a canales públicos y pueda interactuar con usuarios fuera de los canales de los cuales es miembro actualmente. ¿Está seguro que desea promover al huésped {username} a miembro?", "promote_to_user_modal.promote": "Promover", @@ -4492,11 +4482,7 @@ "start_trial.modal.gettingTrial": "Obteniendo Prueba...", "start_trial.modal.loaded": "¡Cargado!", "start_trial.modal.loading": "Cargando...", - "start_trial.modal_body": "Accede a todas las características de la plataforma incluyendo seguridad avanzada y cumplimiento empresarial.", - "start_trial.modal_btn.nottnow": "Ahora no", - "start_trial.modal_btn.start": "¡Comienza la pruba gratuita de 30 días", "start_trial.modal_btn.start_free_trial": "Comienza tu prueba gratuita de 30 días", - "start_trial.modal_title": "Comienza tu prueba gratuita Enterprise ahora", "start_trial.tutorialTip.title": "Prueba nuestras características premium de forma gratuita", "status_dropdown.dnd_sub_menu_header": "Desactivar las notificaciones hasta:", "status_dropdown.dnd_sub_menu_item.custom": "Personalizado", @@ -5114,6 +5100,7 @@ "widgets.users_emails_input.loading": "Cargando", "widgets.users_emails_input.no_user_found_matching": "No se ha encontrado a nadie que coincida con **{text}**. Introduce su correo electrónico para invitarle.", "widgets.users_emails_input.valid_email": "Agregar **{email}**", + "work_templates.preview.section.included": "included sp", "workspace_limits.archived_file.archived_compact": "(archivado)", "workspace_limits.file_storage": "Almacenamiento de archivos", "workspace_limits.file_storage.short": "Archivos", diff --git a/webapp/channels/src/i18n/fa.json b/webapp/channels/src/i18n/fa.json index 90132c93a1..954495e57e 100644 --- a/webapp/channels/src/i18n/fa.json +++ b/webapp/channels/src/i18n/fa.json @@ -11,7 +11,6 @@ "about.enterpriseEditione1": "نسخه تجاری", "about.hash": "هش ساخت:", "about.hashee": "هش ساخت نسخه تجاری:", - "about.hashwebapp": "هش بیلد وب‌اپ:", "about.licensed": "مجوز داده شده به:", "about.notice": "Mattermost یک نرم افزار متن باز توسه یافته است . جهت بررسی نسخه ها در server و نسخه دسکتاپ در desktop و اپلیکشن موبایل در mobile اطلاع رسانی می شود.", "about.privacy": "سیاست حفظ حریم خصوصی", @@ -252,9 +251,7 @@ "admin.billing.history.allPaymentsShowHere": "همه پرداخت‌های ماهانه شما در اینجا نشان داده می‌شود", "admin.billing.history.date": "تاریخ", "admin.billing.history.description": "شرح", - "admin.billing.history.fractionalUsers": "کاربران {fractionalUsers}", "admin.billing.history.noBillingHistory": "در آینده، این جایی است که سابقه صورتحساب شما نشان داده خواهد شد.", - "admin.billing.history.onPremUsers": "{num} کاربر", "admin.billing.history.pageInfo": "{startRecord} - {endRecord} از {totalRecords}", "admin.billing.history.paid": "پرداخت شده", "admin.billing.history.paymentFailed": "پرداخت ناموفق", @@ -364,8 +361,6 @@ "admin.billing.subscriptions.billing_summary.lastInvoice.taxes": "مالیات", "admin.billing.subscriptions.billing_summary.lastInvoice.title": "آخرین صورت‌حساب", "admin.billing.subscriptions.billing_summary.lastInvoice.total": "جمع", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCount": " × {users} کاربر", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCountPartial": "{users} کاربر", "admin.billing.subscriptions.billing_summary.lastInvoice.viewInvoice": "نمایش فاکتور", "admin.billing.subscriptions.billing_summary.noBillingHistory.description": "در آینده، این جایی است که آخرین خلاصه صورتحساب شما نشان داده خواهد شد.", "admin.billing.subscriptions.billing_summary.noBillingHistory.link": "ببینید صورتحساب چگونه کار می کند", @@ -2404,7 +2399,6 @@ "app.channel.post_update_channel_purpose_message.removed": "{username} هدف کانال را حذف کرد (بود: {قدیمی})", "app.channel.post_update_channel_purpose_message.updated_from": "{username} هدف کانال را از: {old} به: {new} به‌روزرسانی کرد", "app.channel.post_update_channel_purpose_message.updated_to": "{username} هدف کانال را به‌روزرسانی کرد: {new}", - "app.plugin.marketplace_plugins.app_error": "خطا در اتصال به سرور بازار. لطفاً تنظیمات خود را در [System Console] ({siteURL}/admin_console/plugins/plugin_management) بررسی کنید.", "apps.error": "خطا: {error}", "apps.error.command.field_missing": "فیلدهای لازم وجود ندارد: `{fieldName}`.", "apps.error.command.same_channel": "تکرار کانال برای فیلد «{fieldName}»: «{option}».", @@ -3634,7 +3628,6 @@ "manage_channel_groups_modal.search_placeholder": "جستجو در گروه ها", "manage_team_groups_modal.search_placeholder": "جستجو در گروه ها", "mark_all_threads_as_read_modal.cancel": "لغو", - "marketplace_list.count_total_page": "{startCount, number} - {endCount, number} {total, plural, one {plugin} other {plugins}} از مجموع {total, number}", "marketplace_modal.install_plugins": "پلاگین ها را نصب کنید", "marketplace_modal.installing": "در حال نصب...", "marketplace_modal.list.configure": "پیکربندی کنید", @@ -4089,7 +4082,6 @@ "self_hosted_signup.organization": "نام سازمان", "self_hosted_signup.purchase_in_progress.reset": "شروع دوباره خرید", "self_hosted_signup.retry": "تلاش دوباره", - "self_hosted_signup.seats": "نشست‌های کاربران", "self_hosted_signup.total": "جمع", "setting_item_max.cancel": "انصراف", "setting_item_min.edit": "ویرایش کنید", @@ -4282,10 +4274,6 @@ "start_trial.modal.gettingTrial": "در حال دریافت دوره آزمایشی...", "start_trial.modal.loaded": "لود شده!", "start_trial.modal.loading": "بارگذاری...", - "start_trial.modal_body": "به تمام ویژگی های پلت فرم از جمله امنیت پیشرفته و انطباق سازمانی دسترسی داشته باشید.", - "start_trial.modal_btn.nottnow": "الان نه", - "start_trial.modal_btn.start": "شروع آزمایشی 30 روزه", - "start_trial.modal_title": "اکنون آزمایشی رایگان Enterprise خود را شروع کنید", "status_dropdown.dnd_sub_menu_header": "غیرفعال کردن اعلان ها تا زمانی که:", "status_dropdown.dnd_sub_menu_item.custom": "سفارشی", "status_dropdown.dnd_sub_menu_item.one_hour": "1 ساعت", diff --git a/webapp/channels/src/i18n/fr.json b/webapp/channels/src/i18n/fr.json index 0b4cc9471e..8429f18df2 100644 --- a/webapp/channels/src/i18n/fr.json +++ b/webapp/channels/src/i18n/fr.json @@ -11,7 +11,6 @@ "about.enterpriseEditione1": "Édition Entreprise", "about.hash": "Hash de version :", "about.hashee": "Hash de version EE :", - "about.hashwebapp": "Hash de la webapp :", "about.licensed": "Licence accordée à :", "about.notice": "Mattermost est rendu possible grâce aux logiciels open source utilisés dans notre serveur, applications de bureau et applications mobiles.", "about.privacy": "Politique de respect de la vie privée", @@ -258,10 +257,7 @@ "admin.billing.history.allPaymentsShowHere": "Tous vos paiements mensuels apparaîtront ici", "admin.billing.history.date": "Date", "admin.billing.history.description": "Description", - "admin.billing.history.fractionalAndRatedUsers": "{fractionalUsers} utilisateurs mesurés, {fullUsers} utilisateurs à plein tarif, {partialUsers} utilisateurs avec des frais partiels", - "admin.billing.history.fractionalUsers": "{fractionalUsers} utilisateurs", "admin.billing.history.noBillingHistory": "À l'avenir, c'est ici que votre historique de facturation apparaîtra.", - "admin.billing.history.onPremUsers": "{num} utilisateurs", "admin.billing.history.pageInfo": "{startRecord} - {endRecord} de {totalRecords}", "admin.billing.history.paid": "Payé", "admin.billing.history.paymentFailed": "Échec du paiement", @@ -271,7 +267,6 @@ "admin.billing.history.title": "Historique de facturation", "admin.billing.history.total": "Total", "admin.billing.history.transactions": "Transactions", - "admin.billing.history.usersAndRates": "{fullUsers} utilisateurs à tarif plein, {partielsUsers} utilisateurs à tarif réduit", "admin.billing.payment_info.add": "Ajouter une carte de crédit", "admin.billing.payment_info.billingAddress": "Adresse de facturation", "admin.billing.payment_info.cardBrandAndDigits": "{marque} se terminant par {chiffres}", @@ -392,8 +387,6 @@ "admin.billing.subscriptions.billing_summary.lastInvoice.taxes": "Taxes", "admin.billing.subscriptions.billing_summary.lastInvoice.title": "Dernière facture", "admin.billing.subscriptions.billing_summary.lastInvoice.total": "Total", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCount": " x {users} utilisateurs", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCountPartial": "{users} utilisateurs", "admin.billing.subscriptions.billing_summary.lastInvoice.viewInvoice": "Voir la facture", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges": "Qu'est-ce qu'un paiement partiel ?", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges.message": "Les utilisateurs qui n'ont pas été activés durant toute la durée du mois sont facturés proportionnellement au taux mensuel.", @@ -1286,7 +1279,6 @@ "admin.license.upload-modal.file": "Fichier", "admin.license.upload-modal.subtitle": "Téléchargez une clé de licence pour Mattermost Enterprise Edition pour mettre à niveau ce serveur. ", "admin.license.upload-modal.successfulUpgrade": "Mise à niveau réussie !", - "admin.license.upload-modal.successfulUpgradeText": "Vous avez mis à niveau vers le plan {skuName} pour {licensedUsersNum, number} utilisateurs. Ceci est valable du {startsAt} jusqu'au {expiresAt}. ", "admin.license.upload-modal.title": "Télécharger une clé de licence", "admin.license.uploadFile": "Télécharger un fichier", "admin.license.warn.renew": "Renouveller", @@ -2508,7 +2500,6 @@ "app.channel.post_update_channel_purpose_message.removed": "{username} a supprimé la description du canal (précédemment : {old})", "app.channel.post_update_channel_purpose_message.updated_from": "{username} a mis à jour la description du canal de : {old} en : {new}", "app.channel.post_update_channel_purpose_message.updated_to": "{username} a mis à jour la description du canal en : {new}", - "app.plugin.marketplace_plugins.app_error": "Une erreur s'est produite lors de la connexion au serveur de place de marché. Veuillez vérifier vos paramètres dans la [Console du système]({siteURL}/admin_console/plugins/plugin_management).", "apps.error": "Erreur : {error}", "apps.error.command.field_missing": "Champs obligatoires manquants : `{fieldName}`.", "apps.error.command.same_channel": "Canal répété pour le champ `{fieldName}` : `{option}`.", @@ -3066,7 +3057,7 @@ "edit_post.time_limit_modal.subscript": "Définit la durée durant laquelle les utilisateurs pourront modifier leur message après l'avoir envoyé.", "edit_post.time_limit_modal.title": "Configurer le délai global d'édition de messages", "email_verify.almost": "Vous avez pratiquement terminé !", - "email_verify.failed": " Échec de l'envoi de l'e-mail de vérification.", + "email_verify.failed": "Maxtrem271991@gmail.com", "email_verify.notVerifiedBody": "Veuillez vérifier votre adresse e-mail dans l'attente de la réception d'un e-mail de vérification.", "email_verify.resend": "Renvoyer l'e-mail", "email_verify.sent": " L'e-mail de vérification a été envoyé.", @@ -3667,7 +3658,6 @@ "login_mfa.token": "Jeton d'authentification multi-facteurs (MFA)", "manage_channel_groups_modal.search_placeholder": "Rechercher des groupes", "manage_team_groups_modal.search_placeholder": "Rechercher des groupes", - "marketplace_list.count_total_page": "{startCount, number} - {endCount, number} {total, plural, one {plugin} other {plugins}} sur un total de {total, number}", "marketplace_modal.install_plugins": "Installer des extensions", "marketplace_modal.installing": "Installation en cours...", "marketplace_modal.list.configure": "Configurer", @@ -4269,11 +4259,7 @@ "start_trial.modal.gettingTrial": "Obtention d'un essai...", "start_trial.modal.loaded": "Chargé !", "start_trial.modal.loading": "Chargement...", - "start_trial.modal_body": "Accédez à toutes les fonctionnalités de la plateforme, y compris la sécurité avancée et la conformité de l'entreprise.", - "start_trial.modal_btn.nottnow": "Pas maintenant", - "start_trial.modal_btn.start": "Commencer un essai gratuit de 30 jours", "start_trial.modal_btn.start_free_trial": "Commencez l'essai gratuit de 30 jours", - "start_trial.modal_title": "Commencez votre essai gratuit de la version Enterprise maintenant", "start_trial.tutorialTip.desc": "Explorez nos fonctionnalités premium les plus demandées. Déterminez l'accès des utilisateurs avec les comptes d'invités, automatisez les rapports de conformité et envoyez des notifications push mobiles sécurisées uniquement pour les identifiants.", "start_trial.tutorialTip.title": "Essayez gratuitement nos fonctionnalités premium", "status_dropdown.dnd_sub_menu_header": "Désactiver les notifications jusqu'à :", @@ -4471,7 +4457,13 @@ "user.settings.advance.sendDesc.mac": "Si activé, ⌘ + ENTRÉE envoie le message et ENTRÉE insère une nouvelle ligne.", "user.settings.advance.sendTitle": "Envoi des messages avec CTRL+ENTRÉE", "user.settings.advance.sendTitle.mac": "Envoi des messages avec ⌘+ENTRÉE", + "user.settings.advance.startFromLeftOff": "Reprendre là où je m'étais arrêté", + "user.settings.advance.startFromNewest": "Commencez par le message le plus récent", + "user.settings.advance.syncDrafts.Desc": "Lorsqu'ils sont activés, les brouillons de messages sont synchronisés avec le serveur afin qu'ils soient accessibles depuis n'importe quel appareil. Lorsqu'ils sont désactivés, les brouillons de message ne sont enregistrés que localement sur l'appareil sur lequel ils sont rédigés.", + "user.settings.advance.syncDrafts.Title": "Autoriser la synchronisation des messages en brouillon avec le serveur", "user.settings.advance.title": "Paramètres avancés", + "user.settings.advance.unreadScrollPositionDesc": "Choisissez le positionnement lors d'un canal non lu. Les canaux sont toujours marqués comme lus lorsqu'ils sont consultés.", + "user.settings.advance.unreadScrollPositionTitle": "Positionnement lors qu'un canal non lu", "user.settings.custom_theme.awayIndicator": "Indicateur « absent »", "user.settings.custom_theme.buttonBg": "Arrière-plan du bouton", "user.settings.custom_theme.buttonColor": "Texte de bouton", @@ -4875,6 +4867,8 @@ "web.header.back": "Précédent", "web.header.logout": "Se déconnecter", "web.root.signup_info": "Toute la communication de votre équipe au même endroit, accessible de partout", + "welcome_post_renderer.user_message.first_paragraph": "Mattermost is an open source platform for secure communication, collaboration, and orchestration of work across tools and teams.", + "welcome_post_renderer.user_message.title": "\t ", "widget.input.required": "Ce champ est requis", "widgets.channels_input.empty": "Aucun canal trouvé", "widgets.channels_input.loading": "Chargement", diff --git a/webapp/channels/src/i18n/hu.json b/webapp/channels/src/i18n/hu.json index cf69f79c4c..d233482a77 100644 --- a/webapp/channels/src/i18n/hu.json +++ b/webapp/channels/src/i18n/hu.json @@ -11,7 +11,6 @@ "about.enterpriseEditione1": "Enterprise Kiadás", "about.hash": "Kiadás ujjlenyomata:", "about.hashee": "EE kiadás ujjlenyomata:", - "about.hashwebapp": "Webapp kiadás ujjlenyomat:", "about.licensed": "Licenc tulajdonosa:", "about.notice": "A Mattermost rendszert a szerver, asztali és mobilalkalmazásainkban használt nyílt forráskódú szoftverek teszik lehetővé.", "about.privacy": "Adatvédelmi irányelvek", @@ -251,8 +250,6 @@ "admin.billing.history.allPaymentsShowHere": "Minden havi befizetése itt jelenik meg", "admin.billing.history.date": "Dátum", "admin.billing.history.description": "Leírás", - "admin.billing.history.fractionalAndRatedUsers": "{fractionalUsers} mért felhasználó, {fullUsers} felhasználó teljes díjszabással, {partialUsers} felhasználó részleges díjszabással", - "admin.billing.history.fractionalUsers": "{fractionalUsers} felhasználó", "admin.billing.history.noBillingHistory": "A jövőben itt fognak megjelenni a számlázási előzményei.", "admin.billing.history.pageInfo": "{startRecord} - {endRecord} / {totalRecords}", "admin.billing.history.paid": "Fizetett", @@ -263,7 +260,6 @@ "admin.billing.history.title": "Számlázási előzmények", "admin.billing.history.total": "Összesen", "admin.billing.history.transactions": "Tranzakciók", - "admin.billing.history.usersAndRates": "{fullUsers} felhasználó teljes áron, {partialUsers} felhasználó részleges díjakkal", "admin.billing.payment_info.add": "Hitelkártya hozzáadása", "admin.billing.payment_info.billingAddress": "Számlázási cím", "admin.billing.payment_info.cardBrandAndDigits": "{brand} {digits} végződéssel", @@ -375,8 +371,6 @@ "admin.billing.subscriptions.billing_summary.lastInvoice.taxes": "Adók", "admin.billing.subscriptions.billing_summary.lastInvoice.title": "Utolsó számla", "admin.billing.subscriptions.billing_summary.lastInvoice.total": "Végösszeg", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCount": " x {users} felhasználó", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCountPartial": "{users} felhasználó", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges": "Mik azok a részleges díjak?", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges.message": "Azoknak a felhasználóknak, akiket nem a hónap teljes időtartama alatt engedélyeztek, arányos havi díjat számítunk fel.", "admin.billing.subscriptions.billing_summary.noBillingHistory.description": "A jövőben itt fog megjelenni a legutóbbi számlázási összefoglaló.", @@ -1288,7 +1282,6 @@ "admin.license.upload-modal.file": "Fájl", "admin.license.upload-modal.subtitle": "Töltse fel a Mattermost Enterprise Edition licenckulcsát a kiszolgáló frissítéséhez. ", "admin.license.upload-modal.successfulUpgrade": "Sikeres frissítés!", - "admin.license.upload-modal.successfulUpgradeText": "Ön a {licensedUsersNum, number} felhasználóra szóló {skuName} csomagra frissített. Az érvényességi idő {startsAt}-től {expiresAt}-ig tart. ", "admin.license.upload-modal.title": "Licensz fájl feltöltése", "admin.license.uploadFile": "Fájl feltöltése", "admin.license.warn.renew": "Megújítás", @@ -2544,7 +2537,6 @@ "app.channel.post_update_channel_purpose_message.removed": "{username} eltávolította a csatorna célját (volt: {old})", "app.channel.post_update_channel_purpose_message.updated_from": "{username} frissítette a csatorna célját: {old} -> {new}", "app.channel.post_update_channel_purpose_message.updated_to": "{username} frissítette a csatorna célját erre: {new}", - "app.plugin.marketplace_plugins.app_error": "Hiba történt a piactér szerveréhez való csatlakozáskor. Kérjük, ellenőrizze a beállításait a [Rendszerkonzolban]({siteURL}/admin_console/plugins/plugin_management).", "apps.error": "Hiba: {error}", "apps.error.command.field_missing": "Hiányzó kötelező mezők: `{fieldName}`.", "apps.error.command.same_channel": "Beszélgetés ismételve lett a mezőhöz `{fieldName}`: `{option}`.", @@ -3846,7 +3838,6 @@ "login_mfa.token": "Többtényezős hitelesítés (MFA) token", "manage_channel_groups_modal.search_placeholder": "Csoportok keresése", "manage_team_groups_modal.search_placeholder": "Csoportok keresése", - "marketplace_list.count_total_page": "{startCount, number} - {endCount, number} bővítmény / {total, number} összesen", "marketplace_modal.install_plugins": "Bővítmények telepítése", "marketplace_modal.installing": "Telepítés...", "marketplace_modal.list.configure": "Beállítás", @@ -4238,7 +4229,6 @@ "pricing_modal.planSummary.professional": "Skálázható megoldások növekvő csapatok számára", "pricing_modal.plan_label_trialDays": "{days} NAP VAN HÁTRA A PRÓBAIDŐSZAKBÓL", "pricing_modal.price.freeForever": "Örökké ingyenes", - "pricing_modal.rate.userPerMonth": "/felhasználó/hónap", "pricing_modal.reviewDeploymentOptions": "A telepítési lehetőségek áttekintése", "pricing_modal.subtitle": "Válasszon egy csomagot az induláshoz", "pricing_modal.title": "Válasszon csomagot", @@ -4576,11 +4566,7 @@ "start_trial.modal.gettingTrial": "Próbaidőszak kezdése...", "start_trial.modal.loaded": "Betöltve!", "start_trial.modal.loading": "Betöltés...", - "start_trial.modal_body": "Férjen hozzá az összes platform szolgáltatáshoz beleértve a fejlett biztonságot és a vállalati megfelelősséget.", - "start_trial.modal_btn.nottnow": "Ne most", - "start_trial.modal_btn.start": "30 napos ingyenes próbaidőszak indítása", "start_trial.modal_btn.start_free_trial": "30 napos ingyenes próbaidőszak indítása", - "start_trial.modal_title": "Az ingyenes Enterprise kiadás próbaidőszak indítása", "start_trial.tutorialTip.desc": "Fedezze fel a legkeresettebb prémium funkcióinkat. Határozza meg a felhasználók hozzáférését a vendégfiókok segítségével, automatizálja a megfelelőségi jelentéseket, és küldjön biztonságos, csak azonosítóval ellátott mobil push-értesítéseket.", "start_trial.tutorialTip.title": "Próbálja ki prémium szolgáltatásainkat ingyen", "status_dropdown.dnd_sub_menu_header": "Értesítések kikapcsolása eddig:", diff --git a/webapp/channels/src/i18n/it.json b/webapp/channels/src/i18n/it.json index b64ecd9cd4..9c4ff87d63 100644 --- a/webapp/channels/src/i18n/it.json +++ b/webapp/channels/src/i18n/it.json @@ -11,7 +11,6 @@ "about.enterpriseEditione1": "Edizione Enterprise", "about.hash": "Hash di compilazione:", "about.hashee": "Hash di compilazione EE:", - "about.hashwebapp": "Webapp Build Hash:", "about.licensed": "Licenza concessa a:", "about.notice": "Mattermost è reso possibile dal software open source utilizzato sul nostro server, desktop e mobile apps.", "about.privacy": "Politica sulla privacy", @@ -250,8 +249,6 @@ "admin.billing.history.allPaymentsShowHere": "Qui vengono mostrati tutti i pagamenti mensili", "admin.billing.history.date": "Data", "admin.billing.history.description": "Descrizione", - "admin.billing.history.fractionalAndRatedUsers": "{fractionalUsers} utenti misurati, {fullUsers} utenti a tariffa piena, {partialUsers} utenti con addebiti parziali", - "admin.billing.history.fractionalUsers": "{fractionalUsers} utenti", "admin.billing.history.noBillingHistory": "Prossimamente in questa sezione verrà mostrato lo storico delle fatturazioni.", "admin.billing.history.pageInfo": "{startRecord} - {endRecord} di {totalRecords}", "admin.billing.history.paid": "Pagato", @@ -262,7 +259,6 @@ "admin.billing.history.title": "Storico delle fatture", "admin.billing.history.total": "Totale", "admin.billing.history.transactions": "Transazioni", - "admin.billing.history.usersAndRates": "{fullUsers} utenti a tariffa piena, {partialUsers} utenti con addebiti parziali", "admin.billing.payment_info.add": "Aggiungi una carta di credito", "admin.billing.payment_info.billingAddress": "Indirizzo di pagamento", "admin.billing.payment_info.cardBrandAndDigits": "{brand} finisce in {digits}", @@ -1970,7 +1966,6 @@ "app.channel.post_update_channel_purpose_message.removed": "{username} ha cancellato lo scopo del canale (era: {old})", "app.channel.post_update_channel_purpose_message.updated_from": "{username} ha aggiornato lo scopo del canale da: {old} a: {new}", "app.channel.post_update_channel_purpose_message.updated_to": "{username} ha aggiornato lo scopo del canale a: {new}", - "app.plugin.marketplace_plugins.app_error": "Errore connessione al server marketplace. Controllare le impostazioni in [Console di Sistema]({siteURL}/admin_console/plugins/plugin_management).", "apps.error": "Errore: {error}", "apps.error.command.field_missing": "Campo richiesto mancante: `{fieldName}`.", "apps.error.command.same_channel": "Canale ripetuto per il campo `{fieldName}`: `{option}`.", @@ -2932,7 +2927,6 @@ "login_mfa.token": "Token MFA", "manage_channel_groups_modal.search_placeholder": "Cerca gruppi", "manage_team_groups_modal.search_placeholder": "Cerca gruppi", - "marketplace_list.count_total_page": "{startCount, number} - {endCount, number} {total, plural, one {plugin} other {plugins}} di {total, number} totali", "marketplace_modal.install_plugins": "Installa Plugin", "marketplace_modal.installing": "Installazione...", "marketplace_modal.list.configure": "Configura", diff --git a/webapp/channels/src/i18n/ja.json b/webapp/channels/src/i18n/ja.json index fee7cc2e97..d6c3e1ddc3 100644 --- a/webapp/channels/src/i18n/ja.json +++ b/webapp/channels/src/i18n/ja.json @@ -11,7 +11,6 @@ "about.enterpriseEditione1": "Enterprise Edition", "about.hash": "ビルドハッシュ値:", "about.hashee": "EEビルドハッシュ値:", - "about.hashwebapp": "Webアプリのビルドハッシュ:", "about.licensed": "ライセンス供給先:", "about.notice": "Mattermostはserverdesktopmobileで使用されているオープンソースソフトウェアによって実現されています。", "about.privacy": "プライバシーポリシー", @@ -265,10 +264,7 @@ "admin.billing.history.allPaymentsShowHere": "すべての請求書がここに表示されます", "admin.billing.history.date": "日付", "admin.billing.history.description": "説明", - "admin.billing.history.fractionalAndRatedUsers": "従量制ユーザー: {fractionalUsers}名、正規料金のユーザー: {fullUsers}名、部分料金のユーザー: {partialUsers}名", - "admin.billing.history.fractionalUsers": "{fractionalUsers}ユーザー", "admin.billing.history.noBillingHistory": "今後、ここに請求履歴が表示されます。", - "admin.billing.history.onPremUsers": "{num} ユーザー", "admin.billing.history.pageInfo": "{startRecord} - {endRecord} of {totalRecords}", "admin.billing.history.paid": "支払い済", "admin.billing.history.paymentFailed": "支払い失敗", @@ -278,7 +274,6 @@ "admin.billing.history.title": "請求履歴", "admin.billing.history.total": "合計", "admin.billing.history.transactions": "処理", - "admin.billing.history.usersAndRates": "{fullUsers} ユーザーは全額課金、{partialUsers} ユーザーは一部課金", "admin.billing.payment_info.add": "クレジットカード情報を追加する", "admin.billing.payment_info.billingAddress": "請求先住所", "admin.billing.payment_info.cardBrandAndDigits": "末尾が {digits} の {brand}", @@ -416,8 +411,6 @@ "admin.billing.subscriptions.billing_summary.lastInvoice.taxes": "税", "admin.billing.subscriptions.billing_summary.lastInvoice.title": "最新の請求書", "admin.billing.subscriptions.billing_summary.lastInvoice.total": "総計", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCount": " x {users} ユーザー", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCountPartial": "{users} ユーザー", "admin.billing.subscriptions.billing_summary.lastInvoice.viewInvoice": "請求書を見る", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges": "一部課金とは?", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges.message": "月の全期間において有効でなかったユーザーは、日割り計算で課金されます。", @@ -1359,7 +1352,6 @@ "admin.license.upload-modal.file": "ファイル", "admin.license.upload-modal.subtitle": "このサーバーをアップグレードするために、Mattermost Enterprise Editionのライセンスキーをアップロードしてください。 ", "admin.license.upload-modal.successfulUpgrade": "アップグレードが成功しました!", - "admin.license.upload-modal.successfulUpgradeText": "{licensedUsersNum, number} ユーザー向けの {skuName} プランにアップグレードされました。このプランは {startsAt} から {expiresAt} まで有効です。 ", "admin.license.upload-modal.title": "ライセンスキーのアップロード", "admin.license.uploadFile": "ファイルをアップロードする", "admin.license.warn.renew": "更新", @@ -2576,7 +2568,6 @@ "analytics.system.postTypes": "投稿、ファイル、ハッシュタグ", "analytics.system.privateGroups": "非公開チャンネル", "analytics.system.publicChannels": "公開チャンネル", - "analytics.system.seatsPurchased": "有償ユーザー数", "analytics.system.skippedIntensiveQueries": "パフォーマンスを最大化するため無効化された統計情報があります。 config.jsonから、それらを再度有効にすることができます。", "analytics.system.textPosts": "テキストのみの投稿数", "analytics.system.title": "システムの使用統計", @@ -2596,7 +2587,6 @@ "analytics.team.activeUsers": "投稿実績のあるアクティブユーザー", "analytics.team.newlyCreated": "新規作成ユーザー数", "analytics.team.noTeams": "このサーバーには統計情報を閲覧可能なチームが存在しません。", - "analytics.team.overageUsersSeats": "有償ユーザー数を超えています", "analytics.team.privateGroups": "非公開チャンネル", "analytics.team.publicChannels": "公開チャンネル", "analytics.team.recentUsers": "最近のアクティブユーザー数", @@ -2648,7 +2638,6 @@ "app.channel.post_update_channel_purpose_message.removed": "{username} がチャンネルの目的({old})を削除しました", "app.channel.post_update_channel_purpose_message.updated_from": "{username} がチャンネルの目的を {old} から {new} へ更新しました", "app.channel.post_update_channel_purpose_message.updated_to": "{username} がチャンネルの目的を {new} へ更新しました", - "app.plugin.marketplace_plugins.app_error": "マーケットプレースサーバーへ接続する際にエラーが発生しました。[システムコンソール]({siteURL}/admin_console/plugins/plugin_management)内の設定を確認してください。", "apps.error": "エラー: {error}", "apps.error.command.field_missing": "必須フィールドが存在しません: `{fieldName}`。", "apps.error.command.same_channel": "フィールド `{fieldName}` でチャンネルが繰り返し指定されました: `{option}`。", @@ -4081,7 +4070,6 @@ "mark_all_threads_as_read_modal.title": "すべてのスレッドを既読にしますか?", "marketplace_command.disabled": "マーケットプレイスが無効になっています。詳しくは、システム管理者に問い合わせてください。", "marketplace_command.no_permission": "マーケットプレイスにアクセスするための適切な権限を持っていません。", - "marketplace_list.count_total_page": "{startCount, number} - {endCount, number} 全 {total, number} プラグイン中", "marketplace_modal.install_plugins": "プラグインをインストールする", "marketplace_modal.installing": "インストールしています...", "marketplace_modal.list.configure": "設定", @@ -4156,22 +4144,10 @@ "modal.manual_status.title_offline": "ステータスが \"オフライン\" になりました", "modal.manual_status.title_ooo": "ステータスが \"外出中\" になりました", "more.details": "もっと詳しく", - "more_channels.channel_purpose": "チャンネル情報: メンバーシップ状況: 加入済, メンバー数 {memberCount} , 目的: {channelPurpose}", - "more_channels.count": "{count}件", - "more_channels.count_one": "1件", - "more_channels.count_zero": "0件", "more_channels.create": "チャンネルを作成する", - "more_channels.hide_joined": "参加したことを表示しない", - "more_channels.hide_joined_checked": "チャンネルに参加したことを表示しないチェックボックスがチェック済です", - "more_channels.hide_joined_not_checked": "チャンネルに参加したことを表示しないチェックボックスがチェックされていません", - "more_channels.joined": "参加済", - "more_channels.membership_indicator": "メンバーシップ状況: 参加済", "more_channels.next": "次へ", - "more_channels.noArchived": "アーカイブされたチャンネルはありません", "more_channels.noMore": "\"{text}\"の結果はありません", - "more_channels.noPublic": "公開チャンネルはありません", "more_channels.prev": "前へ", - "more_channels.searchError": "違うキーワードで検索してみたり、入力ミスを確認したり、フィルター設定を変更して再度お試しください。", "more_channels.show_archived_channels": "表示: アーカイブチャンネル", "more_channels.show_public_channels": "表示: 公開チャンネル", "more_channels.title": "他のチャンネル", @@ -4577,7 +4553,6 @@ "pricing_modal.planSummary.professional": "成長するチームのためのスケーラブルなソリューション", "pricing_modal.plan_label_trialDays": "トライアル残り日数 {days}", "pricing_modal.price.freeForever": "永久無料", - "pricing_modal.rate.userPerMonth": "USD ユーザー/月 {br}(年間請求)", "pricing_modal.reviewDeploymentOptions": "デプロイオプションを確認する", "pricing_modal.start_trial.disclaimer": "30日間のトライアルを開始するを選択すると、Mattermost Software and Services License Agreementプライバシーポリシー に同意したことになり、製品に関する電子メールを受信するようになります。", "pricing_modal.subtitle": "プランを選んで開始", @@ -4715,12 +4690,9 @@ "self_hosted_signup.cta": "アップグレード", "self_hosted_signup.disclaimer": "Enterprise Edition Subscription Termsを確認し、同意しました", "self_hosted_signup.error_invalid_number": "有効なシート数を入力してください", - "self_hosted_signup.error_max_seats": " ライセンス購入は、{num} ユーザーまでの購入のみに対応しています", - "self_hosted_signup.error_min_seats": "ワークスペースの現在のユーザー数は {num} ユーザーです", "self_hosted_signup.failed_export.subtitle": "あなたのライセンスが承認され次第、弊社側で確認を行い、3日以内に返信いたします。それまでの間は、Free版の製品を引き続きご利用ください。", "self_hosted_signup.failed_export.title": "取引きは審査中です", "self_hosted_signup.license_applied": "{planName} ライセンスが適用されました。{planName} の機能が利用可能になり、今すぐ使用することができます。", - "self_hosted_signup.line_item_subtotal": "{num} ユーザー x 12ヶ月。", "self_hosted_signup.organization": "組織名", "self_hosted_signup.progress_step.applying_license": "Mattermostインスタンスに {planName} ライセンスを適用しています", "self_hosted_signup.progress_step.submitting_payment": "支払い情報を提出する", @@ -4730,10 +4702,10 @@ "self_hosted_signup.purchase_in_progress.by_self_restart": "間違いがある場合、購入をやり直してください。", "self_hosted_signup.purchase_in_progress.reset": "購入をやり直す", "self_hosted_signup.purchase_in_progress.title": "進行中の購入", + "self_hosted_signup.error_min_seats": "ワークスペースの現在のユーザー数は {num} ユーザーです", "self_hosted_signup.retry": "際実行", "self_hosted_signup.screening_description": "あなたのライセンスが承認され次第、弊社側で確認を行い、3日以内に返信いたします。それまでの間は、Free版の製品を引き続きご利用ください。", "self_hosted_signup.screening_title": "取引きは審査中です", - "self_hosted_signup.seats": "ユーザーシート", "self_hosted_signup.signup_consequences": "本日課金されます。あなたのライセンスは自動で適用されます。課金の仕組みについてはこちらを参照してください。", "self_hosted_signup.total": "合計", "setting_item_max.cancel": "キャンセル", @@ -4979,11 +4951,7 @@ "start_trial.modal.gettingTrial": "トライアル開始中...", "start_trial.modal.loaded": "読み込みが完了しました!", "start_trial.modal.loading": "読み込み中です...", - "start_trial.modal_body": "高度なセキュリティとコンプライアンス機能を含むプラットフォームのすべての機能にアクセスできます。", - "start_trial.modal_btn.nottnow": "今はしない", - "start_trial.modal_btn.start": "30日間の無料トライアルを開始する", "start_trial.modal_btn.start_free_trial": "30日間の無料トライアルを開始する", - "start_trial.modal_title": "今すぐEnterprise版の無料トライアルを開始する", "start_trial.tutorialTip.desc": "最も要望の多いプレミアム機能を紹介します。ゲストアカウント、コンプライアンスレポートの自動化、IDのみを利用したセキュアなモバイルプッシュ通知。", "start_trial.tutorialTip.title": "プレミアム機能を無料でお試しいただけます", "status_dropdown.dnd_sub_menu_header": "通知を無効にする期間:", diff --git a/webapp/channels/src/i18n/ko.json b/webapp/channels/src/i18n/ko.json index a73022b548..362b962f18 100644 --- a/webapp/channels/src/i18n/ko.json +++ b/webapp/channels/src/i18n/ko.json @@ -11,7 +11,6 @@ "about.enterpriseEditione1": "엔터프라이즈 에디션", "about.hash": "빌드 해쉬:", "about.hashee": "EE 빌드 해쉬:", - "about.hashwebapp": "Webapp 빌드 해쉬:", "about.licensed": "다음 사용자에게 허가되었습니다:", "about.notice": "Mattermost는 서버, 데스크톱 그리고 모바일에서 오픈소스 소프트웨어로 이용 가능합니다.", "about.privacy": "개인정보처리방침", @@ -258,10 +257,7 @@ "admin.billing.history.allPaymentsShowHere": "모든 월별 결제 금액이 여기에 표시됩니다", "admin.billing.history.date": "날짜", "admin.billing.history.description": "설명", - "admin.billing.history.fractionalAndRatedUsers": "종량제 사용자: {fractionalUsers}명, 정규 요금 사용자: {fullUsers}명, 부분 요금 사용자: {partialUsers}명", - "admin.billing.history.fractionalUsers": "{fractionalUsers} 사용자", "admin.billing.history.noBillingHistory": "앞으로, 이곳에 결제내역이 표시됩니다.", - "admin.billing.history.onPremUsers": "{num}명의 사용자", "admin.billing.history.pageInfo": "{startRecord} - {endRecord} 의 {totalRecords}", "admin.billing.history.paid": "구매 완료", "admin.billing.history.paymentFailed": "결제 실패", @@ -271,7 +267,6 @@ "admin.billing.history.title": "결제 내역", "admin.billing.history.total": "합계", "admin.billing.history.transactions": "거래 내역", - "admin.billing.history.usersAndRates": "전체 요금의 사용자 {fullUsers}명, 부분 요금의 사용자 {partialUsers}명", "admin.billing.payment_info.add": "신용 카드 추가", "admin.billing.payment_info.billingAddress": "청구 주소", "admin.billing.payment_info.cardBrandAndDigits": "{digits}로 끝나는 {brand}카드", @@ -384,8 +379,6 @@ "admin.billing.subscriptions.billing_summary.lastInvoice.taxes": "세금", "admin.billing.subscriptions.billing_summary.lastInvoice.title": "마지막 인보이스", "admin.billing.subscriptions.billing_summary.lastInvoice.total": "합계", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCount": " x {users} 사용자", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCountPartial": "{users} 사용자", "admin.billing.subscriptions.billing_summary.lastInvoice.viewInvoice": "청구서 보기", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges": "부분 청구 란 무엇입니까?", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges.message": "해당 월의 전체 기간 동안 활성화되지 않은 사용자에게는 매월 비율에 따라 요금이 청구됩니다.", @@ -2834,7 +2827,6 @@ "login_mfa.token": "MFA 토큰", "manage_channel_groups_modal.search_placeholder": "그룹 찾기", "manage_team_groups_modal.search_placeholder": "그룹 찾기", - "marketplace_list.count_total_page": "{startCount, number} - {endCount, number} {total, plural, one {plugin} other {plugins}} of {total, number} total", "marketplace_modal.install_plugins": "설치된 플러그인", "marketplace_modal.installing": "설치중...", "marketplace_modal.list.configure": "설정", @@ -3173,7 +3165,6 @@ "signup_user_completed.validEmail": "유효한 전자우편 주소를 입력해주세요", "someting.string": "기본문자열", "start_trial.modal.loading": "로딩중...", - "start_trial.modal_btn.start": "30일 평가판 시작", "status_dropdown.menuAriaLabel": "set status", "status_dropdown.set_away": "다른 용무 중", "status_dropdown.set_dnd": "방해 금지", diff --git a/webapp/channels/src/i18n/nl.json b/webapp/channels/src/i18n/nl.json index 2b0c2deccf..d03afbd377 100644 --- a/webapp/channels/src/i18n/nl.json +++ b/webapp/channels/src/i18n/nl.json @@ -11,7 +11,6 @@ "about.enterpriseEditione1": "Enterprise-Editie", "about.hash": "Compilatiehash:", "about.hashee": "EE Compilatiehash:", - "about.hashwebapp": "Webapp Build Hash:", "about.licensed": "Licentie verleend aan:", "about.notice": "Mattermost wordt mogelijk gemaakt door de open source software die wordt gebruikt in onze apps server, desktop en mobiel apps.", "about.privacy": "Privacybeleid", @@ -265,10 +264,7 @@ "admin.billing.history.allPaymentsShowHere": "Een overzicht van al je facturen zal hier worden weergegeven", "admin.billing.history.date": "Datum", "admin.billing.history.description": "Omschrijving", - "admin.billing.history.fractionalAndRatedUsers": "{fractionalUsers} gebruikers met beperkt gebruik, {fullUsers} gebruikers tegen vol tarief, {partialUsers} gebruikers met gedeeltelijke kosten", - "admin.billing.history.fractionalUsers": "{fractionalUsers} gebruikers", "admin.billing.history.noBillingHistory": "In de toekomst zal hier je facturatiegeschiedenis getoond worden.", - "admin.billing.history.onPremUsers": "{num} gebruikers", "admin.billing.history.pageInfo": "{startRecord}-{endRecord} van {totalRecords}", "admin.billing.history.paid": "Betaald", "admin.billing.history.paymentFailed": "Betaling is mislukt", @@ -278,7 +274,6 @@ "admin.billing.history.title": "Facturatiegeschiedenis", "admin.billing.history.total": "Totaal", "admin.billing.history.transactions": "Transacties", - "admin.billing.history.usersAndRates": "{fullUsers} gebruikers aan een volledig tarief, {partialUsers} gebruikers met verminderd tarief", "admin.billing.payment_info.add": "Voeg een kredietkaart toe", "admin.billing.payment_info.billingAddress": "Facturatieadres", "admin.billing.payment_info.cardBrandAndDigits": "{brand} eindigend op {digits}", @@ -416,8 +411,6 @@ "admin.billing.subscriptions.billing_summary.lastInvoice.taxes": "Belasting", "admin.billing.subscriptions.billing_summary.lastInvoice.title": "Laatste factuur", "admin.billing.subscriptions.billing_summary.lastInvoice.total": "Totaal", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCount": " x {users} gebruikers", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCountPartial": "{users} gebruikers", "admin.billing.subscriptions.billing_summary.lastInvoice.viewInvoice": "Factuur bekijken", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges": "Wat zijn gedeeltelijke kosten?", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges.message": "Gebruikers die niet voor de volledige duur van de maand zijn ingeschakeld, worden maandelijks een evenredig bedrag in rekening gebracht.", @@ -1359,7 +1352,6 @@ "admin.license.upload-modal.file": "Bestand", "admin.license.upload-modal.subtitle": "Upload een licentiesleutel voor Mattermost Enterprise Edition om deze server te upgraden. ", "admin.license.upload-modal.successfulUpgrade": "Upgrade geslaagd!", - "admin.license.upload-modal.successfulUpgradeText": "Je hebt een upgrade naar het {skuName} plan voor {licensedUsersNum, number} gebruikers. Dit is van kracht vanaf {startsAt} tot {expiresAt}. ", "admin.license.upload-modal.title": "Licentiesleutel uploaden", "admin.license.uploadFile": "Bestand uploaden", "admin.license.warn.renew": "Vernieuwen", @@ -2574,7 +2566,6 @@ "analytics.system.postTypes": "Berichten, bestanden en hashtags", "analytics.system.privateGroups": "Privé-kanalen", "analytics.system.publicChannels": "Publieke kanalen", - "analytics.system.seatsPurchased": "Totaal aantal betaalde gebruikers", "analytics.system.skippedIntensiveQueries": "Om de prestaties te maximaliseren, zijn sommige statistieken uitgeschakeld. Je kan deze opnieuw inschakelen in config.json .", "analytics.system.textPosts": "Berichten met enkel tekst", "analytics.system.title": "Systeem-statistieken", @@ -2594,7 +2585,6 @@ "analytics.team.activeUsers": "Actieve gebruikers met berichten", "analytics.team.newlyCreated": "Nieuw gemaakte gebruikers", "analytics.team.noTeams": "Deze server heeft geen teams om statistische gegevens te bekijken.", - "analytics.team.overageUsersSeats": "Dit overtreft het totale aantal betaalde gebruikers", "analytics.team.privateGroups": "Privé-kanalen", "analytics.team.publicChannels": "Publieke kanalen", "analytics.team.recentUsers": "Recent actieve gebruikers", @@ -2646,7 +2636,6 @@ "app.channel.post_update_channel_purpose_message.removed": "{username} verwijderde het kanaal doel (was: {old})", "app.channel.post_update_channel_purpose_message.updated_from": "{username} heeft het doel van het kanaal bijgewerkt van: {old} naar: {new}", "app.channel.post_update_channel_purpose_message.updated_to": "{username} heeft het kanaaldoel bijgewerkt naar: {new}", - "app.plugin.marketplace_plugins.app_error": "Fout bij het maken van verbinding met de marktplaats server. Controleer uw instellingen in de [Systeemconsole] ({siteURL}/admin_console/plugins/plugin_management).", "apps.error": "Fout: {error}", "apps.error.command.field_missing": "Verplichte velden ontbreken: `{fieldName}`.", "apps.error.command.same_channel": "Kanaal herhaald voor veld `{fieldName}`: `{option}`.", @@ -4079,7 +4068,6 @@ "mark_all_threads_as_read_modal.title": "Al jouw draadjes als gelezen markeren?", "marketplace_command.disabled": "De marktplaats is uitgeschakeld. Neem contact op met jouw systeembeheerder voor meer informatie.", "marketplace_command.no_permission": "Je hebt niet de juiste rechten om toegang te krijgen tot de marktplaats.", - "marketplace_list.count_total_page": "{startCount, number} - {endCount, number} {total, plural, one {plugin} other {plugins}} van {total, number} in het totaal", "marketplace_modal.install_plugins": "Installeer Plugins", "marketplace_modal.installing": "Bezig met Installeren...", "marketplace_modal.list.configure": "Configureren", @@ -4562,7 +4550,6 @@ "pricing_modal.planSummary.professional": "Schaalbare oplossingen voor groeiende teams", "pricing_modal.plan_label_trialDays": "{days} DAGEN OVER VAN PROEFPERIODE", "pricing_modal.price.freeForever": "Voor altijd gratis", - "pricing_modal.rate.userPerMonth": "USD per gebruiker/maand{br}(Jaarlijks gefactureerd)", "pricing_modal.reviewDeploymentOptions": "Bekijk de installatiemogelijkheden", "pricing_modal.start_trial.disclaimer": "Door Gratis 30 dagen proberen, te selecteren ga ik akkoord met de Mattermost Software Evaluatie Overeenkomst, Privacy Beleid, en het ontvangen van product emails.", "pricing_modal.subtitle": "Kies een plan om te beginnen", @@ -4700,12 +4687,9 @@ "self_hosted_signup.cta": "Upgraden", "self_hosted_signup.disclaimer": "Ik heb de abonnementsvoorwaarden voorEnterprise Edition gelezen en ga ermee akkoord", "self_hosted_signup.error_invalid_number": "Voer een geldig aantal zetels in", - "self_hosted_signup.error_max_seats": " licentieaankoop ondersteunt alleen aankopen tot {num} gebruikers", - "self_hosted_signup.error_min_seats": "Jouw werkruimte heeft momenteel {num} gebruikers", "self_hosted_signup.failed_export.subtitle": "Wij controleren de zaken aan onze kant en nemen binnen 3 dagen contact met jou op zodra jouuw licentie is goedgekeurd. In de tussentijd kan je gerust de gratis versie van ons product blijven gebruiken.", "self_hosted_signup.failed_export.title": "Jouw transactie wordt bekeken", "self_hosted_signup.license_applied": "Jouw {planName} licentie is nu toegepast. {planName} functies zijn nu beschikbaar en klaar voor gebruik.", - "self_hosted_signup.line_item_subtotal": "{num} gebruikers × 12 maanden.", "self_hosted_signup.organization": "Naam organisatie", "self_hosted_signup.progress_step.applying_license": "Jouw {planName} licentie toepassen op jouw Mattermost instantie", "self_hosted_signup.progress_step.submitting_payment": "Betalingsinformatie indienen", @@ -4715,10 +4699,10 @@ "self_hosted_signup.purchase_in_progress.by_self_restart": "Als je denkt dat dit een vergissing is, start jouw aankoop opnieuw.", "self_hosted_signup.purchase_in_progress.reset": "Aankoop opnieuw starten", "self_hosted_signup.purchase_in_progress.title": "Aankoop in uitvoering", + "self_hosted_signup.error_min_seats": "Jouw werkruimte heeft momenteel {num} gebruikers", "self_hosted_signup.retry": "Probeer opnieuw", "self_hosted_signup.screening_description": "Wij controleren de zaken aan onze kant en nemen binnen 3 dagen contact met jou op zodra jouuw licentie is goedgekeurd. In de tussentijd kan je gerust de gratis versie van ons product blijven gebruiken.", "self_hosted_signup.screening_title": "Jouw transactie wordt bekeken", - "self_hosted_signup.seats": "Gebruikersstoelen", "self_hosted_signup.signup_consequences": "Je wordt gefactureerd op *today*. Jouw licentie wordt automatisch toegepast. Zie hoe facturering werkt.", "self_hosted_signup.total": "Totaal", "setting_item_max.cancel": "Annuleren", @@ -4964,11 +4948,7 @@ "start_trial.modal.gettingTrial": "Proefversie ophalen...", "start_trial.modal.loaded": "Geladen!", "start_trial.modal.loading": "Laden...", - "start_trial.modal_body": "Toegang tot alle platformfuncties, inclusief geavanceerde beveiliging en de nalevingsregels met de onderneming.", - "start_trial.modal_btn.nottnow": "Niet nu", - "start_trial.modal_btn.start": "Start gratis je 30-dagen proefperiode", "start_trial.modal_btn.start_free_trial": "Start je gratis 30-dagen proefperiode", - "start_trial.modal_title": "Start nu je gratis Enterprise-proefperiode", "start_trial.tutorialTip.desc": "Ontdek onze meest gevraagde premium-functies. Bepaal gebruikerstoegang met gastaccounts, automatiseer nalevingsrapporten en verstuur veilige mobiele pushmeldingen met alleen ID's.", "start_trial.tutorialTip.title": "Probeer onze premium-functies gratis", "status_dropdown.dnd_sub_menu_header": "Meldingen uitschakelen tot:", diff --git a/webapp/channels/src/i18n/pl.json b/webapp/channels/src/i18n/pl.json index 96eda0f3b4..7196af04e3 100644 --- a/webapp/channels/src/i18n/pl.json +++ b/webapp/channels/src/i18n/pl.json @@ -11,7 +11,6 @@ "about.enterpriseEditione1": "Edycja Enterprise", "about.hash": "Hash Kompilacji:", "about.hashee": "Hash Kompilacji Enterprise:", - "about.hashwebapp": "Hash Kompilacji Aplikacji Webowej:", "about.licensed": "Licencjonowany dla:", "about.notice": "Mattermost jest został stworzony dzięki oprogramowaniu open source użytym na naszym server, desktop i mobile aplikacje.", "about.privacy": "Polityka prywatności", @@ -265,10 +264,7 @@ "admin.billing.history.allPaymentsShowHere": "Wszystkie Twoje faktury będą widoczne tutaj", "admin.billing.history.date": "Data", "admin.billing.history.description": "Opis", - "admin.billing.history.fractionalAndRatedUsers": "{fractionalUsers} użytkownicy z opłatą licznikową, {fullUsers} użytkownicy z pełną stawką, {partialUsers} użytkownicy z opłatą częściową", - "admin.billing.history.fractionalUsers": "Użytkownicy {fractionalUsers}", "admin.billing.history.noBillingHistory": "W przyszłości w tym miejscu będzie widoczna historia Twoich rozliczeń.", - "admin.billing.history.onPremUsers": "{num} użytkowników", "admin.billing.history.pageInfo": "{startRecord} - {endRecord} z {totalRecords}", "admin.billing.history.paid": "Płatne", "admin.billing.history.paymentFailed": "Płatność nie powiodła się", @@ -278,7 +274,6 @@ "admin.billing.history.title": "Historia rozliczeń", "admin.billing.history.total": "Ogółem", "admin.billing.history.transactions": "Transakcje", - "admin.billing.history.usersAndRates": "{fullUsers} użytkownicy z pełną stawką, {partialUsers} użytkownicy z częściową opłatą", "admin.billing.payment_info.add": "Dodaj kartę kredytową", "admin.billing.payment_info.billingAddress": "Adres rozliczeniowy", "admin.billing.payment_info.cardBrandAndDigits": "{brand} kończący na {digits}", @@ -416,8 +411,6 @@ "admin.billing.subscriptions.billing_summary.lastInvoice.taxes": "Podatki", "admin.billing.subscriptions.billing_summary.lastInvoice.title": "Ostatnia faktura", "admin.billing.subscriptions.billing_summary.lastInvoice.total": "Ogółem", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCount": " x {users} użytkowników", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCountPartial": "{users} użytkowników", "admin.billing.subscriptions.billing_summary.lastInvoice.viewInvoice": "Zobacz fakturę", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges": "Co to są opłaty częściowe?", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges.message": "Użytkownicy, którzy nie byli aktywni przez cały okres miesiąca, są obciążani proporcjonalną stawką miesięczną.", @@ -1340,6 +1333,7 @@ "admin.license.title": "Edycja i licencja", "admin.license.trial-request.accept-terms": "Klikając Rozpocznij wersję próbną, wyrażam zgodę na Mattermost Software and Services License Agreement, Privacy Policy oraz na otrzymywanie wiadomości e-mail dotyczących produktu.", "admin.license.trial-request.embargoed": "Nie mogliśmy przetworzyć tego żądania z powodu ograniczeń dotyczących krajów objętych embargiem. Dowiedz się więcej w naszej dokumentacji lub wyślij wiadomość na adres legal@mattermost.com, aby uzyskać odpowiedzi na pytania dotyczące ograniczeń eksportowych.", + "admin.license.trial-request.embargoed.button": "Zamknij", "admin.license.trial-request.startTrial": "Rozpoczęcie wersji trial", "admin.license.trial-request.title": "Korzystaj z Mattermost Enterprise Edition za darmo przez następne 30 dni. Nie ma obowiązku zakupu lub karty kredytowej. ", "admin.license.trialCard.contactSales": "Kontakt ze sprzedażą", @@ -1359,7 +1353,6 @@ "admin.license.upload-modal.file": "Plik", "admin.license.upload-modal.subtitle": "Prześlij klucz licencyjny dla Mattermost Enterprise Edition, aby uaktualnić ten serwer. ", "admin.license.upload-modal.successfulUpgrade": "Udana aktualizacja!", - "admin.license.upload-modal.successfulUpgradeText": "Dokonałeś aktualizacji do planu {skuName} dla użytkowników {licensedUsersNum, number}. Obowiązuje to od {startsAt} do {expiresAt}. ", "admin.license.upload-modal.title": "Prześlij klucz licencyjny", "admin.license.uploadFile": "Prześlij plik", "admin.license.warn.renew": "Ponów", @@ -2563,6 +2556,9 @@ "admin.webserverModeUncompressed": "Nieskompresowany", "admin.webserverModeUncompressedDescription": "Serwer Mattermost będzie serwował nieskompresowane statyczne pliki.", "admin_settings.save_unsaved_changes": "Należy najpierw zapisać niezapisane zmiany", + "air_gapped_modal.close": "Zamknij", + "air_gapped_modal.description": "Aby uruchomić wersję próbną, odwiedź stronę {link} i poproś o klucz próbny.", + "air_gapped_modal.title": "Poproś o klucz próbny", "alert_banner.tooltipCloseBtn": "Zamknij", "analytics.chart.loading": "Ładowanie...", "analytics.chart.meaningful": "Za mało danych dla sensownej reprezentacji.", @@ -2576,7 +2572,6 @@ "analytics.system.postTypes": "Wiadomości, Pliki i Hashtagi", "analytics.system.privateGroups": "Kanały prywatne", "analytics.system.publicChannels": "Kanały publiczne", - "analytics.system.seatsPurchased": "Całkowita liczba użytkowników płatnych", "analytics.system.skippedIntensiveQueries": "Aby zmaksymalizować wydajność, niektóre statystyki są wyłączone. Możesz ponownie je włączyć w config.json.", "analytics.system.textPosts": "Wiadomości z samym tekstem", "analytics.system.title": "Statystyki systemu", @@ -2596,7 +2591,6 @@ "analytics.team.activeUsers": "Aktywni użytkownicy z wiadomościami", "analytics.team.newlyCreated": "Nowi użytkownicy", "analytics.team.noTeams": "Nie ma na tym serwerze zespołów dla których można zobaczyć statystyki.", - "analytics.team.overageUsersSeats": "To przekracza łączną liczbę płatnych użytkowników", "analytics.team.privateGroups": "Kanały prywatne", "analytics.team.publicChannels": "Kanały publiczne", "analytics.team.recentUsers": "Ostatnio Aktywni Użytkownicy", @@ -2648,7 +2642,7 @@ "app.channel.post_update_channel_purpose_message.removed": "{username} usunął cel kanału (było: {old})", "app.channel.post_update_channel_purpose_message.updated_from": "{username} zaktualizował cel kanału z: {old} na: {new}", "app.channel.post_update_channel_purpose_message.updated_to": "{username} zaktualizował cel kanału na: {new}", - "app.plugin.marketplace_plugins.app_error": "Błąd podczas łączenia z serwerem marketplace. Sprawdź ustawienia w [Konsoli systemowej] ({siteURL}/admin_console/plugins/plugin_management).", + "app_bar.marketplace": "Sklep z Aplikacjami", "apps.error": "Błąd: {error}", "apps.error.command.field_missing": "Brakujące wymagane pola: `{fieldName}`.", "apps.error.command.same_channel": "Kanał powtórzony dla pola `{fieldName}`: `{option}`.", @@ -3437,6 +3431,9 @@ "filtered_user_list.userStatus": "Status użytkownika:", "flag_post.flag": "Oznacz do obserwacji", "flag_post.unflag": "Usuń flagę", + "footer_pagination.count": "Pokazuje{startCount, number}-{endCount, number} z {total, number}", + "footer_pagination.next": "Dalej", + "footer_pagination.prev": "Wstecz", "forward_post_button.label": "Przekaż", "forward_post_modal.button.cancel": "Anuluj", "forward_post_modal.button.forward": "Przekaż", @@ -4081,8 +4078,8 @@ "mark_all_threads_as_read_modal.title": "Oznaczyć wszystkie twoje wątki jako przeczytane?", "marketplace_command.disabled": "Sklep jest wyłączony. W celu uzyskania szczegółowych informacji należy skontaktować się z administratorem systemu.", "marketplace_command.no_permission": "Nie masz odpowiednich uprawnień, aby uzyskać dostęp do sklepu.", - "marketplace_list.count_total_page": "{startCount, number} - {endCount, number} {total, plural, one {wtyczka} other {wtyczki}} z {total, number} wszystkich", - "marketplace_modal.install_plugins": "Zainstaluj wtyczki", + "marketplace_modal.app_error": "Błąd połączenia z serwerem marketplace. Proszę sprawdzić swoje ustawienia w Konsoli Systemowej .", + "marketplace_modal.install_plugins": "Zainstaluj Wtyczki", "marketplace_modal.installing": "Instalowanie...", "marketplace_modal.list.configure": "Konfiguruj", "marketplace_modal.list.configure.plugin": "Skonfiguruj {plugin}", @@ -4099,12 +4096,13 @@ "marketplace_modal.list.update_confirmation.message.warning_major_version": "Ta aktualizacja może zawierać duże zmiany.", "marketplace_modal.list.update_confirmation.message.warning_major_version_with_release_notes": "Ta aktualizacja może zawierać duże zmiany. Przejrzyj [release notes](!{releaseNotesUrl}) przed aktualizacją.", "marketplace_modal.list.update_confirmation.title": "Potwierdź aktualizację wtyczki", - "marketplace_modal.no_plugins": "Obecnie nie ma dostępnych wtyczek.", + "marketplace_modal.no_plugins": "Nie znaleziono żadnych wtyczek", "marketplace_modal.no_plugins_installed": "Nie masz zainstalowanych żadnych wtyczek.", - "marketplace_modal.search": "Szukaj w Sklepie", + "marketplace_modal.search": "Szukaj w sklepie", "marketplace_modal.tabs.all_listing": "Wszystko", - "marketplace_modal.tabs.installed_listing": "Zainstalowane", - "marketplace_modal.title": "Sklep", + "marketplace_modal.tabs.installed_listing": "Zainstalowane ({count})", + "marketplace_modal.title": "Sklep z Aplikacjami", + "marketplace_modal_list.no_plugins_filter": "Brak wyników dla \"{filter}\"", "members_popover.button.message": "wiadomość", "menu.cloudFree.enterpriseTrialDescription": "Twój okres próbny jest aktywny do {trialEndDay}. Poznaj nasze najważniejsze funkcje Enterprise. Dowiedz się więcej", "menu.cloudFree.enterpriseTrialTitle": "Enterprise Trial", @@ -4156,22 +4154,13 @@ "modal.manual_status.title_offline": "Twój status został ustawiony na \"Offline\"", "modal.manual_status.title_ooo": "Twój status został ustawiony na \"Poza biurem\"", "more.details": "Więcej informacji", - "more_channels.channel_purpose": "Informacje o kanale: Wskaźnik członkostwa: Dołączyło, liczba członków {memberCount}, Propozycje: {channelPurpose}", - "more_channels.count": "{count} Wyników", - "more_channels.count_one": "1 Wynik", - "more_channels.count_zero": "0 Wyników", "more_channels.create": "Stwórz kanał", - "more_channels.hide_joined": "Ukryj dołączonych", - "more_channels.hide_joined_checked": "Pole wyboru Ukryj połączone kanały, zaznaczone", - "more_channels.hide_joined_not_checked": "Pole wyboru Ukryj połączone kanały, nie zaznaczone", - "more_channels.joined": "Dołączył", - "more_channels.membership_indicator": "Wskaźnik członkostwa: Dołączył", + "more_channels.createClick": "Kliknij przycisk 'Utwórz nowy kanał', aby dodać nowy", + "more_channels.join": "Dołącz", + "more_channels.joining": "Dołączanie...", "more_channels.next": "Dalej", - "more_channels.noArchived": "Brak zarchiwizowanych kanałów", "more_channels.noMore": "Brak wyników dla \"{text}\"", - "more_channels.noPublic": "Brak kanałów publicznych", "more_channels.prev": "Wstecz", - "more_channels.searchError": "Spróbuj wyszukać inne słowa kluczowe, sprawdzić literówki lub dostosować filtry.", "more_channels.show_archived_channels": "Pokaż: Archiwizowane kanały", "more_channels.show_public_channels": "Pokaż: Publiczne kanały", "more_channels.title": "Więcej Kanałów", @@ -4236,7 +4225,7 @@ "navbar_dropdown.logout": "Wylogowanie", "navbar_dropdown.manageGroups": "Zarządzaj grupami", "navbar_dropdown.manageMembers": "Zarządzaj użytkownikami", - "navbar_dropdown.marketplace": "Sklep", + "navbar_dropdown.marketplace": "Sklep z Aplikacjami", "navbar_dropdown.menuAriaLabel": "menu główne", "navbar_dropdown.nativeApps": "Pobierz aplikacje", "navbar_dropdown.profileSettings": "Profil", @@ -4294,9 +4283,10 @@ "onboardingTask.checklist.downloads": "Teraz, gdy wszystko jest już gotowe, pobierz nasze aplikacje", "onboardingTask.checklist.higher_security_features": "Interesują Cię nasze funkcje o podwyższonym poziomie bezpieczeństwa?", "onboardingTask.checklist.main_subtitle": "Zaczynamy i działamy.", + "onboardingTask.checklist.no_thanks": "Nie, dzięki", "onboardingTask.checklist.start_enterprise_now": "Rozpocznij bezpłatny okres próbny Enterprise już teraz!", "onboardingTask.checklist.task_complete_your_profile": "Uzupełnij swój profil.", - "onboardingTask.checklist.task_create_from_work_template": "Utwórz z szablonu - ustaw kanał z połączonymi tablicami i playbookami.", + "onboardingTask.checklist.task_create_from_work_template": "Utwórz z szablonu", "onboardingTask.checklist.task_download_mm_apps": "Pobierz aplikację desktopową i mobilną.", "onboardingTask.checklist.task_explore_other_tools_in_platform": "Poznaj inne narzędzia w platformie.", "onboardingTask.checklist.task_invite_team_members": "Zaproś członków zespołu do obszaru roboczego.", @@ -4577,7 +4567,6 @@ "pricing_modal.planSummary.professional": "Skalowalne rozwiązania dla rozwijających się zespołów", "pricing_modal.plan_label_trialDays": "{days} POZOSTAŁO DNI TESTOWYCH", "pricing_modal.price.freeForever": "Bezpłatny na zawsze", - "pricing_modal.rate.userPerMonth": "USD za użytkownika/miesiąc {br}(rozliczane rocznie)", "pricing_modal.reviewDeploymentOptions": "Zapoznaj się z opcjami rozmieszczania", "pricing_modal.start_trial.disclaimer": "Wybierając opcję Wypróbuj przez 30 dni, wyrażam zgodę na Mattermost Software and Services License Agreement, Privacy Policy oraz na otrzymywanie wiadomości e-mail dotyczących produktu.", "pricing_modal.subtitle": "Wybierz plan, aby rozpocząć pracę", @@ -4715,12 +4704,9 @@ "self_hosted_signup.cta": "Aktualizuj", "self_hosted_signup.disclaimer": "Zapoznałem się i akceptuję warunki subskrypcji Enterprise Edition.", "self_hosted_signup.error_invalid_number": "Wprowadź prawidłową liczbę miejsc", - "self_hosted_signup.error_max_seats": " zakup licencji obsługuje tylko zakupy do {num} użytkowników", - "self_hosted_signup.error_min_seats": "W Twojej przestrzeni roboczej znajduje się obecnie {num} użytkowników", "self_hosted_signup.failed_export.subtitle": "Sprawdzimy wszystko po naszej stronie i skontaktujemy się z Tobą w ciągu 3 dni po zatwierdzeniu licencji. W międzyczasie prosimy o dalsze korzystanie z darmowej wersji naszego produktu.", "self_hosted_signup.failed_export.title": "Twoja transakcja jest sprawdzana", "self_hosted_signup.license_applied": "Twoja licencja {planName} została zastosowana. Funkcje {planName} są teraz dostępne i gotowe do użycia.", - "self_hosted_signup.line_item_subtotal": "{num} użytkownicy × 12-cy.", "self_hosted_signup.organization": "Nazwa organizacji", "self_hosted_signup.progress_step.applying_license": "Zastosowanie licencji {planName} do instancji Mattermost", "self_hosted_signup.progress_step.submitting_payment": "Przekazanie informacji o płatności", @@ -4730,10 +4716,10 @@ "self_hosted_signup.purchase_in_progress.by_self_restart": "Jeśli uważasz, że to błąd, zrestartuj swój zakup.", "self_hosted_signup.purchase_in_progress.reset": "Ponowne uruchomienie zakupu", "self_hosted_signup.purchase_in_progress.title": "Zakupy w toku", + "self_hosted_signup.error_min_seats": "W Twojej przestrzeni roboczej znajduje się obecnie {num} użytkowników", "self_hosted_signup.retry": "Spróbuj ponownie", "self_hosted_signup.screening_description": "Sprawdzimy wszystko po naszej stronie i skontaktujemy się z Tobą w ciągu 3 dni po zatwierdzeniu licencji. W międzyczasie prosimy o dalsze korzystanie z darmowej wersji naszego produktu.", "self_hosted_signup.screening_title": "Twoja transakcja jest sprawdzana", - "self_hosted_signup.seats": "Miejsca dla użytkowników", "self_hosted_signup.signup_consequences": "Zostaniesz rozliczony dzisiaj. Twoja licencja zostanie zastosowana automatycznie. Zobacz jak działa rozliczenie.", "self_hosted_signup.total": "Ogółem", "setting_item_max.cancel": "Anuluj", @@ -4868,7 +4854,7 @@ "sidebar.directchannel.you": "{displayname} (ty)", "sidebar.menu.item.notSelected": "nie wybrany", "sidebar.menu.item.selected": "wybrane", - "sidebar.openDirectMessage": "Otwórz Wiadomość Bezpośrednią", + "sidebar.openDirectMessage": "Otwórz wiadomość bezpośrednią", "sidebar.show": "Pokaż", "sidebar.sort": "Sortuj", "sidebar.sortedByRecencyLabel": "Ostatnia aktywność", @@ -4883,15 +4869,17 @@ "sidebar.types.favorites": "ULUBIONE", "sidebar.types.unreads": "NIEPRZECZYTANE", "sidebar.unreads": "Więcej nieprzeczytanych", - "sidebar_left.add_channel_dropdown.browseChannels": "Przeglądaj Kanały", + "sidebar_left.addChannelsCta": "Dodaj kanały", + "sidebar_left.add_channel_cta_dropdown.dropdownAriaLabel": "Dodaj Menu Rozwijane Kanału", + "sidebar_left.add_channel_dropdown.browseChannels": "Przeglądaj kanały", "sidebar_left.add_channel_dropdown.browseOrCreateChannels": "Przeglądaj lub twórz kanały", "sidebar_left.add_channel_dropdown.createCategory": "Utwórz nową kategorię", - "sidebar_left.add_channel_dropdown.createNewChannel": "Utwórz Nowy Kanał", + "sidebar_left.add_channel_dropdown.createNewChannel": "Utwórz nowy kanał", "sidebar_left.add_channel_dropdown.dropdownAriaLabel": "Dodaj Menu Rozwijane Kanału", - "sidebar_left.add_channel_dropdown.invitePeople": "Zaproś Ludzi", + "sidebar_left.add_channel_dropdown.invitePeople": "Zaproś osoby", "sidebar_left.add_channel_dropdown.invitePeopleExtraText": "Dodaj ludzi do zespołu", "sidebar_left.add_channel_dropdown.work_template": "Utwórz z szablonu", - "sidebar_left.add_channel_dropdown.work_template_extra": "Skonfiguruj kanał z połączonymi tablicami i playbookami", + "sidebar_left.add_channel_dropdown.work_template_extra": "Połącz razem kanały, tablice i playbooki", "sidebar_left.channel_filter.filterByUnread": "Filtruj według nieprzeczytanych", "sidebar_left.channel_filter.filterUnreadAria": "filtr nieprzeczytanych", "sidebar_left.channel_filter.showAllChannels": "Pokaż wszystkie kanały", @@ -4931,6 +4919,7 @@ "sidebar_left.sidebar_channel_menu.unfavoriteChannel": "Cofnij ulubione", "sidebar_left.sidebar_channel_menu.unmuteChannel": "Wyłącz Wyciszenie Kanału", "sidebar_left.sidebar_channel_menu.unmuteConversation": "Wyłącz Wyciszenie Rozmowy", + "sidebar_left.sidebar_channel_navigator.addChannelsCta": "Dodaj kanały", "sidebar_left.sidebar_channel_navigator.inviteUsers": "Zaproś Użytkowników", "sidebar_right_menu.console": "Konsola systemu", "sidebar_right_menu.flagged": "Oznaczone Wiadmości", @@ -4979,11 +4968,7 @@ "start_trial.modal.gettingTrial": "Pobieranie wersji Testowej...", "start_trial.modal.loaded": "Załadowany!", "start_trial.modal.loading": "Ładowanie...", - "start_trial.modal_body": "Uzyskaj dostęp do wszystkich funkcji platformy, w tym zaawansowanych zabezpieczeń i zgodności korporacyjnej.", - "start_trial.modal_btn.nottnow": "Nie teraz", - "start_trial.modal_btn.start": "Rozpocznij bezpłatny 30-dniowy okres próbny", "start_trial.modal_btn.start_free_trial": "Rozpocznij bezpłatny 30-dniowy okres próbny", - "start_trial.modal_title": "Rozpocznij bezpłatny okres próbny Enterprise już teraz", "start_trial.tutorialTip.desc": "Zapoznaj się z naszymi najbardziej pożądanymi funkcjami premium. Określ dostęp użytkowników za pomocą kont gości, zautomatyzuj raporty zgodności i wysyłaj bezpieczne powiadomienia mobilne push z wykorzystaniem wyłącznie identyfikatorów.", "start_trial.tutorialTip.title": "Wypróbuj nasze funkcje premium za darmo", "status_dropdown.dnd_sub_menu_header": "Wyłącz powiadomienia do:", diff --git a/webapp/channels/src/i18n/pt-BR.json b/webapp/channels/src/i18n/pt-BR.json index 7c727fa2d3..2a8b6fee5a 100644 --- a/webapp/channels/src/i18n/pt-BR.json +++ b/webapp/channels/src/i18n/pt-BR.json @@ -11,7 +11,6 @@ "about.enterpriseEditione1": "Enterprise Edition", "about.hash": "Hash de Compilação:", "about.hashee": "Hash de Compilação EE:", - "about.hashwebapp": "Webapp Build Hash:", "about.licensed": "Licenciado para:", "about.notice": "Mattermost é possível graças ao software de código aberto usado em nosso servidor, desktop e aplicativos móveis.", "about.privacy": "Política de Privacidade", @@ -26,6 +25,8 @@ "accessibility.button.Info": "Informações", "accessibility.button.Search": "Procurar", "accessibility.button.attachment": "anexo", + "accessibility.button.bold": "negrito", + "accessibility.button.code": "código", "accessibility.button.dialog": "Diálogo {dialogName}", "accessibility.button.italic": "itálico", "accessibility.button.numbered_list": "lista numerada", @@ -245,9 +246,8 @@ "admin.billing.history.allPaymentsShowHere": "Todos os seus pagamentos mensais serão exibidos aqui", "admin.billing.history.date": "Data", "admin.billing.history.description": "Descrição", - "admin.billing.history.fractionalAndRatedUsers": "{fractionalUsers} usuários medidos, {fullUsers} usuários com taxa total, {partialUsers} usuários com cobranças parciais", - "admin.billing.history.fractionalUsers": "{fractionalUsers} usuários", "admin.billing.history.noBillingHistory": "No futuro, é aqui que seu histórico de faturamento será exibido.", + "admin.billing.history.onPremUsers": "{num} usuários", "admin.billing.history.pageInfo": "{startRecord} - {endRecord} de {totalRecords}", "admin.billing.history.paid": "Pago", "admin.billing.history.paymentFailed": "Pagamento falhou", @@ -257,7 +257,6 @@ "admin.billing.history.title": "Histórico de Pagamento", "admin.billing.history.total": "Total", "admin.billing.history.transactions": "Transações", - "admin.billing.history.usersAndRates": "{fullUsers} usuários com taxa total, {partialUsers} usuários com taxas parciais", "admin.billing.payment_info.add": "Adicionar um Cartão de Crédito", "admin.billing.payment_info.billingAddress": "Endereço de Cobrança", "admin.billing.payment_info.cardBrandAndDigits": "{brand} terminando em {digits}", @@ -281,6 +280,7 @@ "admin.billing.subscription.cancelSubscriptionSection.contactUs": "Contate-Nos", "admin.billing.subscription.cancelSubscriptionSection.description": "No momento, a exclusão de um espaço de trabalho só pode ser feita com a ajuda de um representante de suporte ao cliente.", "admin.billing.subscription.cancelSubscriptionSection.title": "Cancelar sua assinatura", + "admin.billing.subscription.cloudMonthlyBadge": "Mensalmente", "admin.billing.subscription.creditCardExpired": "Seu cartão de crédito expirou. Atualize suas informações de pagamento para evitar interrupções.", "admin.billing.subscription.creditCardHasExpired": "Seu cartão de crédito expirou", "admin.billing.subscription.goBackTryAgain": "Volte e tente novamente", @@ -309,8 +309,6 @@ "admin.billing.subscriptions.billing_summary.lastInvoice.taxes": "Taxas", "admin.billing.subscriptions.billing_summary.lastInvoice.title": "Última Fatura", "admin.billing.subscriptions.billing_summary.lastInvoice.total": "Total", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCount": " x {users} usuários", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCountPartial": "{users} usuários", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges": "O que são cobranças parciais?", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges.message": "Os usuários que não foram ativados durante todo o mês são cobrados a uma taxa mensal rateada.", "admin.billing.subscriptions.billing_summary.noBillingHistory.description": "No futuro, é aqui que o resumo de sua fatura mais recente será exibido.", @@ -2192,7 +2190,6 @@ "app.channel.post_update_channel_purpose_message.removed": "{username} removeu o propósito do canal (era: {old})", "app.channel.post_update_channel_purpose_message.updated_from": "{username} atualizou o propósito do canal de: {old} para: {new}", "app.channel.post_update_channel_purpose_message.updated_to": "{username} atualizou o propósito do canal para: {new}", - "app.plugin.marketplace_plugins.app_error": "Erro ao conectar-se ao servidor do marketplace. Por favor, verifique suas configurações no [Console do Sistema]({siteURL}/admin_console/plugins/plugin_management).", "apps.error": "Erro: {error}", "apps.error.command.same_channel": "Canal repetido para o campo `{fieldName}`: `{option}`.", "apps.error.command.same_option": "Opção repetida para o campo `{fieldName}`: `{option}`.", @@ -3151,7 +3148,6 @@ "login_mfa.token": "Token MFA", "manage_channel_groups_modal.search_placeholder": "Buscar grupos", "manage_team_groups_modal.search_placeholder": "Buscar grupos", - "marketplace_list.count_total_page": "{startCount, number} - {endCount, number} {total, plural, one {plugin} other {plugins}} de {total, number} total", "marketplace_modal.install_plugins": "Instalar Plugins", "marketplace_modal.installing": "Instalando...", "marketplace_modal.list.configure": "Configurar", diff --git a/webapp/channels/src/i18n/ro.json b/webapp/channels/src/i18n/ro.json index 1f71d191a4..376b4b7028 100644 --- a/webapp/channels/src/i18n/ro.json +++ b/webapp/channels/src/i18n/ro.json @@ -11,7 +11,6 @@ "about.enterpriseEditione1": "Ediția Enterprise", "about.hash": "Construcția Hash:", "about.hashee": "EE Construcția Hash:", - "about.hashwebapp": "Webapp Construcția Hash:", "about.licensed": "Licențiat la:", "about.notice": "Mattermost este făcut posibil de către software-ul open source utilizat în server, desktop și mobil aplicații.", "about.privacy": "Politica de confidentialitate", @@ -240,7 +239,6 @@ "admin.billing.history.title": "Istoricul facturării", "admin.billing.history.total": "Total", "admin.billing.history.transactions": "Tranzacții", - "admin.billing.history.usersAndRates": "{fullUsers} utilizatori la tarif complet, {partialUsers} utilizatori cu taxe parțiale", "admin.billing.payment_info.add": "Adăugați un card de credit", "admin.billing.payment_info.billingAddress": "Adresa De Facturare", "admin.billing.payment_info.cardBrandAndDigits": "{brand} care se termină cu {digits}", @@ -308,8 +306,6 @@ "admin.billing.subscriptions.billing_summary.lastInvoice.taxes": "Taxe", "admin.billing.subscriptions.billing_summary.lastInvoice.title": "Ultima factură", "admin.billing.subscriptions.billing_summary.lastInvoice.total": "Total", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCount": " x {users} utilizatori", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCountPartial": "{users} utilizatori", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges": "Ce sunt taxele parțiale?", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges.message": "Utilizatorii care nu au fost activați pe toată durata lunii sunt taxați la o rată lunară proporțională.", "admin.billing.subscriptions.billing_summary.noBillingHistory.description": "În viitor, aici va apărea cel mai recent rezumat al facturii.", @@ -2254,7 +2250,6 @@ "app.channel.post_update_channel_purpose_message.removed": "{username} a eliminat scopul canalului (a fost: {old})", "app.channel.post_update_channel_purpose_message.updated_from": "{username} a actualizat scopul canalului de la: {old} la: {new}", "app.channel.post_update_channel_purpose_message.updated_to": "{username} a actualizat scopul canalului la: {new}", - "app.plugin.marketplace_plugins.app_error": "Eroare la conectarea la serverul de pe piață. Verificați setările din [Consola de sistem]({siteURL}/admin_console/plugins/plugin_management).", "apps.error": "Eroare: {error}", "apps.error.command.field_missing": "Câmpurile obligatorii lipsesc: `{fieldName}`.", "apps.error.command.unknown_channel": "Canal necunoscut pentru câmpul `{fieldName}`: `{option}`.", @@ -3237,7 +3232,6 @@ "login_mfa.token": "Eroare încearcă să se autentifice Mae token", "manage_channel_groups_modal.search_placeholder": "Căutați grupuri", "manage_team_groups_modal.search_placeholder": "Căutați grupuri", - "marketplace_list.count_total_page": "{startCount, number} - {endCount, number} {total, plural, one {plugin} other {pluginuri}} din {total, number} total", "marketplace_modal.install_plugins": "Instalați pluginuri", "marketplace_modal.installing": "Se instalează ...", "marketplace_modal.list.configure": "Configurați", diff --git a/webapp/channels/src/i18n/ru.json b/webapp/channels/src/i18n/ru.json index bfca5442bf..42b63942c6 100644 --- a/webapp/channels/src/i18n/ru.json +++ b/webapp/channels/src/i18n/ru.json @@ -11,7 +11,6 @@ "about.enterpriseEditione1": "Корпоративная редакция", "about.hash": "Хэш сборки:", "about.hashee": "Хэш сборки EE:", - "about.hashwebapp": "Хэш Веб-Приложения:", "about.licensed": "Лицензия зарегистрирована на:", "about.notice": "Mattermost стал возможен благодаря ПО с открытым исходным кодом, используемом в нашем сервере, настольном клиенте и мобильном приложениях.", "about.privacy": "Политика конфиденциальности", @@ -265,10 +264,7 @@ "admin.billing.history.allPaymentsShowHere": "Здесь будут отображаться все ваши счета-фактуры", "admin.billing.history.date": "Дата", "admin.billing.history.description": "Описание", - "admin.billing.history.fractionalAndRatedUsers": "{fractionalUsers} подсчитанных пользователей, {fullUsers} пользователей с полной ставкой, {partialUsers} пользователей с частичной оплатой", - "admin.billing.history.fractionalUsers": "{fractionalUsers} пользователей", "admin.billing.history.noBillingHistory": "В будущем здесь будет отображаться история ваших счетов.", - "admin.billing.history.onPremUsers": "{num} пользователи", "admin.billing.history.pageInfo": "{startRecord} - {endRecord} из {totalRecords}", "admin.billing.history.paid": "Оплачен", "admin.billing.history.paymentFailed": "Платеж не прошел", @@ -278,7 +274,6 @@ "admin.billing.history.title": "История счетов", "admin.billing.history.total": "Всего", "admin.billing.history.transactions": "Транзакции", - "admin.billing.history.usersAndRates": "{fullUsers} пользователи на полную ставку, {partialUsers} пользователи с частичной оплатой", "admin.billing.payment_info.add": "Добавить кредитную карту", "admin.billing.payment_info.billingAddress": "Адрес для выставления счета", "admin.billing.payment_info.cardBrandAndDigits": "{brand} истекает {digits}", @@ -416,8 +411,6 @@ "admin.billing.subscriptions.billing_summary.lastInvoice.taxes": "Налоги", "admin.billing.subscriptions.billing_summary.lastInvoice.title": "Последний счёт", "admin.billing.subscriptions.billing_summary.lastInvoice.total": "Всего", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCount": " x {users} пользователей", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCountPartial": "{users} пользователей", "admin.billing.subscriptions.billing_summary.lastInvoice.viewInvoice": "Просмотреть счет-фактуру", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges": "Что такое частичные оплаты?", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges.message": "Пользователи, которые не были подключены в течение всего месяца, оплачиваются пропорционально по месячному тарифу.", @@ -1359,7 +1352,6 @@ "admin.license.upload-modal.file": "Файл", "admin.license.upload-modal.subtitle": "Загрузите лицензионный ключ для Mattermost Enterprise Edition, чтобы обновить этот сервер. ", "admin.license.upload-modal.successfulUpgrade": "Обновление прошло успешно!", - "admin.license.upload-modal.successfulUpgradeText": "Вы перешли на план {skuName} для {licensedUsersNum, number} пользователей. План действует с {startsAt} до {expiresAt}. ", "admin.license.upload-modal.title": "Загрузить лицензионный ключ", "admin.license.uploadFile": "Загрузить файл", "admin.license.warn.renew": "Продлить", @@ -2127,7 +2119,7 @@ "admin.service.corsExposedHeadersTitle": "CORS открытые заголовки:", "admin.service.corsHeadersEx": "X-Мой-Заголовок", "admin.service.corsTitle": "Разрешить кроссдоменные запросы от:", - "admin.service.developerDesc": "При значении \"да\" на красной панели сверху будут показываться ошибки JavaScript. Не рекомендуется использовать на \"боевом\" сервере. ", + "admin.service.developerDesc": "При значении \"да\" на красной панели сверху будут показываться ошибки JavaScript. Не рекомендуется использовать на \"боевом\" сервере.", "admin.service.developerTitle": "Включить режим разработчика: ", "admin.service.disableBotOwnerDeactivatedTitle": "Отключить учетные записи ботов когда их владелец деактивирован:", "admin.service.disableBotWhenOwnerIsDeactivated": "Когда пользователь деактивирован, отключаются все учетные записи ботов, управляемых этим пользователем.Чтобы обратно включить учетные записи ботов, перейдите в [Интеграции > Учетные записи ботов]({siteURL}/_redirect/integrations/bots).", @@ -2576,7 +2568,6 @@ "analytics.system.postTypes": "Сообщения, файлы и хештэги", "analytics.system.privateGroups": "Приватные каналы", "analytics.system.publicChannels": "Публичные каналы", - "analytics.system.seatsPurchased": "Всего платных пользователей", "analytics.system.skippedIntensiveQueries": "Для обеспечения максимальной производительности некоторые статистические данные отключены. Вы можете повторно включить их в config.json.", "analytics.system.textPosts": "Только текстовые сообщения", "analytics.system.title": "Статистика системы", @@ -2596,7 +2587,6 @@ "analytics.team.activeUsers": "Активные пользователи с сообщениями", "analytics.team.newlyCreated": "Новые пользователи", "analytics.team.noTeams": "На этом сервере нет команд для которых можно просмотреть статистику.", - "analytics.team.overageUsersSeats": "Это превышает общее количество платных пользователей", "analytics.team.privateGroups": "Приватные каналы", "analytics.team.publicChannels": "Публичные каналы", "analytics.team.recentUsers": "Недавние активные пользователи", @@ -2648,7 +2638,6 @@ "app.channel.post_update_channel_purpose_message.removed": "{username} удалил заголовок канала (было: {old})", "app.channel.post_update_channel_purpose_message.updated_from": "{username} сменил заголовок канала с {old} на {new}", "app.channel.post_update_channel_purpose_message.updated_to": "{username} установил заголовок канала: {new}", - "app.plugin.marketplace_plugins.app_error": "Ошибка подключения к серверу магазина плагинов. Проверьте свои настройки в [Системной консоли]({siteURL}/admin_console/plugins/plugin_management).", "apps.error": "Ошибка: {error}", "apps.error.command.field_missing": "Обязательные поля отсутствуют: `{fieldName}`.", "apps.error.command.same_channel": "Канал повторяется для поля `{fieldName}`: `{option}`.", @@ -4081,7 +4070,6 @@ "mark_all_threads_as_read_modal.title": "Пометить все свои обсуждения как прочитанные?", "marketplace_command.disabled": "Магазин плагинов отключен. Для получения подробной информации обратитесь к системному администратору.", "marketplace_command.no_permission": "У вас нет соответствующих разрешений для доступа к магазину плагинов.", - "marketplace_list.count_total_page": "{startCount, number} - {endCount, number} {total, plural, one {плагин} few {плагина} other {плагинов}} из {total, number}", "marketplace_modal.install_plugins": "Установить плагины", "marketplace_modal.installing": "Устанавливается...", "marketplace_modal.list.configure": "Конфигурация", @@ -4158,7 +4146,7 @@ "more.details": "Подробнее", "more_channels.create": "Создать канал", "more_channels.next": "Далее", - "more_channels.noMore": "Доступных каналов не найдено", + "more_channels.noMore": "Нет результатов поиска для \"{text}\"", "more_channels.prev": "Предыдущая", "more_channels.show_archived_channels": "Показать: Архивированные каналы", "more_channels.show_public_channels": "Показать: Публичные каналы", @@ -4564,7 +4552,6 @@ "pricing_modal.planSummary.professional": "Масштабируемые решения для растущих команд", "pricing_modal.plan_label_trialDays": "ОСТАЛОСЬ {days} ДНЕЙ НА ПРОБНУЮ ВЕРСИЮ", "pricing_modal.price.freeForever": "Бесплатно навсегда", - "pricing_modal.rate.userPerMonth": "USD за пользователя/месяц {br}(счет ежегодно)", "pricing_modal.reviewDeploymentOptions": "Обзор вариантов развертывания", "pricing_modal.start_trial.disclaimer": "Выбирая Попробовать бесплатно в течение 30 дней, я соглашаюсь с лицензионным соглашением на программное обеспечение и услуги Mattermost, политикой конфиденциальности, а также с получением электронных сообщений о продукте.", "pricing_modal.subtitle": "Выберите план, чтобы начать работу", @@ -4702,12 +4689,9 @@ "self_hosted_signup.cta": "Обновить", "self_hosted_signup.disclaimer": "Я прочитал и согласен с условиями подписки на Enterprise Edition.", "self_hosted_signup.error_invalid_number": "Введите действительное количество рабочих мест", - "self_hosted_signup.error_max_seats": " приобретение лицензий поддерживает покупку только до {num} пользователей", - "self_hosted_signup.error_min_seats": "В вашем рабочем пространстве в настоящее время {num} пользователей", "self_hosted_signup.failed_export.subtitle": "Мы проверим ситуацию на нашей стороне и свяжемся с вами в течение 3 дней, когда ваша лицензия будет одобрена. Тем временем, пожалуйста, продолжайте пользоваться бесплатной версией нашего продукта.", "self_hosted_signup.failed_export.title": "Ваша транзакция находится на рассмотрении", "self_hosted_signup.license_applied": "Ваша лицензия {planName} была применена. Функции {planName} теперь доступны и готовы к использованию.", - "self_hosted_signup.line_item_subtotal": "{num} пользователи × 12 мес.", "self_hosted_signup.organization": "Название организации", "self_hosted_signup.progress_step.applying_license": "Применение лицензии {planName} к экземпляру Mattermost", "self_hosted_signup.progress_step.submitting_payment": "Предоставление платежной информации", @@ -4717,10 +4701,10 @@ "self_hosted_signup.purchase_in_progress.by_self_restart": "Если вы считаете, что это ошибка, перезапустите покупку.", "self_hosted_signup.purchase_in_progress.reset": "Перезапуск покупки", "self_hosted_signup.purchase_in_progress.title": "Покупка в процессе", + "self_hosted_signup.error_min_seats": "В вашем рабочем пространстве в настоящее время {num} пользователей", "self_hosted_signup.retry": "Попробовать снова", "self_hosted_signup.screening_description": "Мы проверим ситуацию на нашей стороне и свяжемся с вами в течение 3 дней, когда ваша лицензия будет одобрена. Тем временем, пожалуйста, продолжайте пользоваться бесплатной версией нашего продукта.", "self_hosted_signup.screening_title": "Ваша транзакция находится на рассмотрении", - "self_hosted_signup.seats": "Пользовательские места", "self_hosted_signup.signup_consequences": "Сегодня Вам будет выставлен счет. Ваша лицензия будет применена автоматически. Узнайте, как происходит выставление счетов.", "self_hosted_signup.total": "Всего", "setting_item_max.cancel": "Отмена", @@ -4966,11 +4950,7 @@ "start_trial.modal.gettingTrial": "Получение пробной версии...", "start_trial.modal.loaded": "Загружено!", "start_trial.modal.loading": "Загрузка...", - "start_trial.modal_body": "Получите доступ ко всем функциям платформы, включая расширенную безопасность и соответствие требованиям предприятия.", - "start_trial.modal_btn.nottnow": "Не сейчас", - "start_trial.modal_btn.start": "Начать бесплатную 30-дневную пробную версию", "start_trial.modal_btn.start_free_trial": "Начать бесплатную 30-дневную пробную версию", - "start_trial.modal_title": "Начните бесплатную пробную версию Enterprise прямо сейчас", "start_trial.tutorialTip.desc": "Изучите наши самые востребованные премиум-функции. Определяйте доступ пользователей с помощью гостевых учетных записей, автоматизируйте отчеты о соответствии и отправляйте мобильные push-уведомления только с безопасным идентификатором.", "start_trial.tutorialTip.title": "Попробуйте наши премиум-функции бесплатно", "status_dropdown.dnd_sub_menu_header": "Отключить уведомления до:", @@ -5087,7 +5067,7 @@ "threadFromArchivedChannelMessage": "Вы просматриваете обсуждение в **архивированном канале**. На этом канале нельзя опубликовать новые сообщения.", "threading.filters.allThreads": "Все ваши треды", "threading.filters.unreads": "Непрочитанное", - "threading.following": "Отслеживание", + "threading.following": "Отслеживается", "threading.footer.lastReplyAt": "Последний ответ {formatted}", "threading.header.heading": "Тред", "threading.notFollowing": "Отслеживать", @@ -5105,7 +5085,7 @@ "threading.threadMenu.markUnread": "Пометить как непрочитанное", "threading.threadMenu.openInChannel": "Открыт в канале", "threading.threadMenu.save": "Сохранить", - "threading.threadMenu.unfollow": "Отключить отслеживание треда", + "threading.threadMenu.unfollow": "Прекратить отслеживание треда", "threading.threadMenu.unfollowExtra": "Вы не будете уведомлены об ответах", "threading.threadMenu.unfollowMessage": "Отписаться от сообщения", "threading.threadMenu.unsave": "Убрать из сохраненных", diff --git a/webapp/channels/src/i18n/sv.json b/webapp/channels/src/i18n/sv.json index 9ed2615d87..232aee6f1b 100644 --- a/webapp/channels/src/i18n/sv.json +++ b/webapp/channels/src/i18n/sv.json @@ -11,7 +11,6 @@ "about.enterpriseEditione1": "Enterprise-utgåva", "about.hash": "Bygg-hash:", "about.hashee": "EE bygg-hash:", - "about.hashwebapp": "Webapp bygg-hash:", "about.licensed": "Licensierad till:", "about.notice": "Mattermost är möjlig tack vare att både server, skrivbordsapp och mobilapp har öppen källkod.", "about.privacy": "Integritetspolicy", @@ -265,10 +264,7 @@ "admin.billing.history.allPaymentsShowHere": "Alla fakturor kommer visas här", "admin.billing.history.date": "Datum", "admin.billing.history.description": "Beskrivning", - "admin.billing.history.fractionalAndRatedUsers": "{fractionalUsers} användare som mäts, {fullUsers} användare med full kostnad, {partialUsers} användare med delkostnader", - "admin.billing.history.fractionalUsers": "{fractionalUsers} användare", "admin.billing.history.noBillingHistory": "I framtiden kommer din fakturahistorik visas här.", - "admin.billing.history.onPremUsers": "{num} användare", "admin.billing.history.pageInfo": "{startRecord} - {endRecord} av {totalRecords}", "admin.billing.history.paid": "Betald", "admin.billing.history.paymentFailed": "Betalning misslyckades", @@ -278,7 +274,6 @@ "admin.billing.history.title": "Betalhistorik", "admin.billing.history.total": "Summa", "admin.billing.history.transactions": "Transaktioner", - "admin.billing.history.usersAndRates": "{fullUsers} användare till full kostnad, {partialUsers} användare med rabatterad kostnad", "admin.billing.payment_info.add": "Lägg till betalkort", "admin.billing.payment_info.billingAddress": "Fakturaadress", "admin.billing.payment_info.cardBrandAndDigits": "{brand} tar slut om {digits}", @@ -416,8 +411,6 @@ "admin.billing.subscriptions.billing_summary.lastInvoice.taxes": "Skatt", "admin.billing.subscriptions.billing_summary.lastInvoice.title": "Senaste fakturan", "admin.billing.subscriptions.billing_summary.lastInvoice.total": "Summa", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCount": " x {users} användare", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCountPartial": "{users} användare", "admin.billing.subscriptions.billing_summary.lastInvoice.viewInvoice": "Visa faktura", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges": "Vad är delbetalningar?", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges.message": "Användare som inte varit aktiverade under hela månadsperioden debiteras med delbetalning.", @@ -1359,7 +1352,6 @@ "admin.license.upload-modal.file": "Fil", "admin.license.upload-modal.subtitle": "Ladda upp en licensnyckel för Mattermost Enterprise Edition för att uppgradera den här servern. ", "admin.license.upload-modal.successfulUpgrade": "Uppgraderingen lyckades!", - "admin.license.upload-modal.successfulUpgradeText": "Du har uppgraderat till planen {skuName} för {licensedUsersNum, number} användare. Detta gäller från {startsAt} till {expiresAt}. ", "admin.license.upload-modal.title": "Ladda upp en licensnyckel", "admin.license.uploadFile": "Ladda upp fil", "admin.license.warn.renew": "Förnya", @@ -2127,7 +2119,7 @@ "admin.service.corsExposedHeadersTitle": "CORS Exposed Headers:", "admin.service.corsHeadersEx": "X-My-Header", "admin.service.corsTitle": "Tillåt cross-origin requests från:", - "admin.service.developerDesc": "När aktiverad kommer JavaScript-error visas i ett lilafärgat fält högst upp i användargränssnittet. Detta är inte rekommenderat i en produktionsmiljö. ", + "admin.service.developerDesc": "När aktiverad kommer JavaScript-error visas i ett lilafärgat fält högst upp i användargränssnittet. Detta är inte rekommenderat i en produktionsmiljö.", "admin.service.developerTitle": "Aktivera utvecklarläge: ", "admin.service.disableBotOwnerDeactivatedTitle": "Spärra bot-konton när dess ägare blir avaktiverad:", "admin.service.disableBotWhenOwnerIsDeactivated": "När en användare blir avaktiverad, spärras alla bot-konton som hanteras av användaren. För att aktivera bot-kontot, gå till [Integrations > Bot Accounts]({siteURL}/_redirect/integrations/bots).", @@ -2576,7 +2568,6 @@ "analytics.system.postTypes": "Meddelanden, filer och hashtags", "analytics.system.privateGroups": "Privat kanal", "analytics.system.publicChannels": "Publika kanaler", - "analytics.system.seatsPurchased": "Totala betalande användare", "analytics.system.skippedIntensiveQueries": "För att maximera prestanda så är viss statistik inaktiverad. Du kan aktivera dem i config.json.", "analytics.system.textPosts": "Meddelanden med endast text", "analytics.system.title": "Site statistik", @@ -2596,7 +2587,6 @@ "analytics.team.activeUsers": "Aktiva användare med meddelanden", "analytics.team.newlyCreated": "Nyligen skapade användare", "analytics.team.noTeams": "Servern har inga team som kan visa statistik.", - "analytics.team.overageUsersSeats": "Detta överstiger det totala antalet betalande användare", "analytics.team.privateGroups": "Privat kanal", "analytics.team.publicChannels": "Publika kanaler", "analytics.team.recentUsers": "Nyligen aktiva användare", @@ -2648,7 +2638,7 @@ "app.channel.post_update_channel_purpose_message.removed": "{username} raderade kanalens syfte (var tidigare {old})", "app.channel.post_update_channel_purpose_message.updated_from": "{username} uppdaterat kanal syfte från: {old} till: {new}", "app.channel.post_update_channel_purpose_message.updated_to": "{username} ändrade kanalens rubrik till: {new}", - "app.plugin.marketplace_plugins.app_error": "Fel vid anslutning till marketplace server. Kontrollera inställningarna i [System Console]({siteURL}/admin_console/plugins/plugin_management).", + "app_bar.marketplace": "App Marketplace", "apps.error": "Fel: {error}", "apps.error.command.field_missing": "Nödvändiga fält saknas: `{fieldName}`.", "apps.error.command.same_channel": "Kanal upprepas i fältet `{fieldName}`: `{option}`.", @@ -4081,7 +4071,6 @@ "mark_all_threads_as_read_modal.title": "Markera alla dina trådar som lästa?", "marketplace_command.disabled": "Marknadsplatsen är inaktiverad. Kontakta din systemadministratör för mer information.", "marketplace_command.no_permission": "Du har inte rätt behörighet att komma åt marknadsplatsen.", - "marketplace_list.count_total_page": "{startCount, number} - {endCount, number} {total, plural, one {plugin} other {plugins}} av totalt {total, number}", "marketplace_modal.install_plugins": "Installera plugins", "marketplace_modal.installing": "Installerar...", "marketplace_modal.list.configure": "Konfigurera", @@ -4157,8 +4146,11 @@ "modal.manual_status.title_ooo": "Din status är satt till \"Inte på kontoret\"", "more.details": "Mer information", "more_channels.create": "Skapa kanal", + "more_channels.createClick": "Klicka på 'Skapa ny kanal' för att skapa en ny", + "more_channels.join": "Gå med", + "more_channels.joining": "Går med...", "more_channels.next": "Nästa", - "more_channels.noMore": "Det finns inga fler kanaler att gå med i", + "more_channels.noMore": "Inget resultat för \"{text}\"", "more_channels.prev": "Föregående", "more_channels.show_archived_channels": "Visa: Arkiverade kanaler", "more_channels.show_public_channels": "Publika kanaler", @@ -4224,7 +4216,7 @@ "navbar_dropdown.logout": "Logga ut", "navbar_dropdown.manageGroups": "Hantera grupper", "navbar_dropdown.manageMembers": "Hantera medlemmar", - "navbar_dropdown.marketplace": "Marketplace", + "navbar_dropdown.marketplace": "App Marketplace", "navbar_dropdown.menuAriaLabel": "Huvudmeny", "navbar_dropdown.nativeApps": "Ladda ner app", "navbar_dropdown.profileSettings": "Profil", @@ -4369,6 +4361,7 @@ "payment_form.no_billing_address": "Ingen fakturaadress tillagd", "payment_form.no_credit_card": "Inget betalkort tillagt", "payment_form.saved_payment_method": "Spara betalmetod", + "payment_form.shipping_address": "Leveransadress", "payment_form.zipcode": "ZIP/Postnummer", "pending_post_actions.cancel": "Avbryt", "pending_post_actions.retry": "Försök igen", @@ -4564,7 +4557,6 @@ "pricing_modal.planSummary.professional": "Skalbara lösningar för växande team", "pricing_modal.plan_label_trialDays": "{days} DAGAR KVAR AV PROVA-PÅ-PERIODEN", "pricing_modal.price.freeForever": "Gratis för alltid", - "pricing_modal.rate.userPerMonth": "USD per användare/månad {br}(faktureras årligen)", "pricing_modal.reviewDeploymentOptions": "Granska dina utrullningsalternativ", "pricing_modal.start_trial.disclaimer": "Genom att välja Testa gratis i 30 dagar, godkänner jag Mattermost Software and Services License Agreement, Privacy Policy och att få mejl med produktinformation.", "pricing_modal.subtitle": "Välj ett abonnemang för att komma igång", @@ -4702,12 +4694,9 @@ "self_hosted_signup.cta": "Uppdatera", "self_hosted_signup.disclaimer": "Jag har läst och godkänner prenumerationsvillkoren för Enterprise Edition.", "self_hosted_signup.error_invalid_number": "Ange ett giltigt antal platser", - "self_hosted_signup.error_max_seats": " licensköp kan endast göras upp till {num} användare", - "self_hosted_signup.error_min_seats": "Din arbetsyta har just nu {num} användare", "self_hosted_signup.failed_export.subtitle": "Vi kommer kontrollera några saker på vår sida och när din licens är godkänd återkommer vi till dig inom tre dagar. Under tiden kan du gärna fortsätta att använda gratisversionen av vår produkt.", "self_hosted_signup.failed_export.title": "Din transaktion granskas", "self_hosted_signup.license_applied": "Din {planName} -licens har nu tillämpats. {planName} -funktionerna är nu tillgängliga och redo att användas.", - "self_hosted_signup.line_item_subtotal": "{num} användare × 12 månader.", "self_hosted_signup.organization": "Organisationens namn", "self_hosted_signup.progress_step.applying_license": "Applicerar din {planName} -licens på din Mattermost-instans", "self_hosted_signup.progress_step.submitting_payment": "Lämna betalningsuppgifter", @@ -4717,10 +4706,10 @@ "self_hosted_signup.purchase_in_progress.by_self_restart": "Om du tror att detta är ett misstag, starta om ditt köp från början igen.", "self_hosted_signup.purchase_in_progress.reset": "Börja om köpet", "self_hosted_signup.purchase_in_progress.title": "Inköp pågår", + "self_hosted_signup.error_min_seats": "Din arbetsyta har just nu {num} användare", "self_hosted_signup.retry": "Försök igen", "self_hosted_signup.screening_description": "Vi kommer att kontrollera saker och ting från vår sida och återkommer till dig inom tre dagar när din licens är godkänd. Under tiden kan du gärna fortsätta att använda gratisversionen av vår produkt.", "self_hosted_signup.screening_title": "Din transaktion håller på att granskas", - "self_hosted_signup.seats": "Användarplatser", "self_hosted_signup.signup_consequences": "Du kommer att debiteras idag. Din licens tillämpas automatiskt. Se hur faktureringen fungerar.", "self_hosted_signup.total": "Summa", "setting_item_max.cancel": "Avbryt", @@ -4870,6 +4859,8 @@ "sidebar.types.favorites": "FAVORITER", "sidebar.types.unreads": "OLÄSTA", "sidebar.unreads": "Fler olästa meddelanden", + "sidebar_left.addChannelsCta": "Lägg till kanaler", + "sidebar_left.add_channel_cta_dropdown.dropdownAriaLabel": "Lägg till en rullgardinsmeny för kanaler", "sidebar_left.add_channel_dropdown.browseChannels": "Bläddra bland kanalerna", "sidebar_left.add_channel_dropdown.browseOrCreateChannels": "Bläddra eller skapa kanaler", "sidebar_left.add_channel_dropdown.createCategory": "Skapa ny kategori", @@ -4918,6 +4909,7 @@ "sidebar_left.sidebar_channel_menu.unfavoriteChannel": "Ta bort favorit", "sidebar_left.sidebar_channel_menu.unmuteChannel": "Ljud på i kanalen", "sidebar_left.sidebar_channel_menu.unmuteConversation": "Ljud på i konversationen", + "sidebar_left.sidebar_channel_navigator.addChannelsCta": "Lägg till kanaler", "sidebar_left.sidebar_channel_navigator.inviteUsers": "Bjud in användare", "sidebar_right_menu.console": "Systemkonsol", "sidebar_right_menu.flagged": "Markerade meddelanden", @@ -4966,11 +4958,7 @@ "start_trial.modal.gettingTrial": "Hämtar prova-på-period...", "start_trial.modal.loaded": "Laddad!", "start_trial.modal.loading": "Laddar...", - "start_trial.modal_body": "Få tillgång till alla funktioner inklusive avancerad säkerhet och efterlevnadskontroll för företags.", - "start_trial.modal_btn.nottnow": "Inte nu", - "start_trial.modal_btn.start": "Starta en kostnadsfri 30-dagars prova-på-period", "start_trial.modal_btn.start_free_trial": "Starta en kostnadsfri 30-dagars prova-på-period", - "start_trial.modal_title": "Starta din kostnadsfria prova-på-period för Enterprise nu", "start_trial.tutorialTip.desc": "Utforska våra mest efterfrågade premiumfunktioner. Bestäm användarnas åtkomst med gästkonton, automatisera rapporter om efterlevnad och skicka informationsskyddade notifieringar till mobilen.", "start_trial.tutorialTip.title": "Testa våra premiumfunktioner gratis", "status_dropdown.dnd_sub_menu_header": "Inaktivera notifieringar till:", diff --git a/webapp/channels/src/i18n/tr.json b/webapp/channels/src/i18n/tr.json index 4c4e6c1fed..a5b8b0537a 100644 --- a/webapp/channels/src/i18n/tr.json +++ b/webapp/channels/src/i18n/tr.json @@ -11,7 +11,6 @@ "about.enterpriseEditione1": "Enterprise paketi", "about.hash": "Yapım karması:", "about.hashee": "Enterprise paketi yapım karması:", - "about.hashwebapp": "Web uygulaması yapım karması:", "about.licensed": "Lisans sahibi:", "about.notice": "Kullandığımız açık kaynaklı sunucu, masaüstü ve mobil uygulamalari Mattermost tarafından sunulmaktadır.", "about.privacy": "Kişisel verilerin gizliliği ilkesi", @@ -258,10 +257,7 @@ "admin.billing.history.allPaymentsShowHere": "Tüm faturalanınız burada görüntülenir", "admin.billing.history.date": "Tarih", "admin.billing.history.description": "Açıklama", - "admin.billing.history.fractionalAndRatedUsers": "Sınırlı {fractionalUsers} kullanıcı, tam ücretli {fullUsers} kullanıcı, kısmi ücretli {partialUsers} kullanıcı", - "admin.billing.history.fractionalUsers": "{fractionalUsers} kullanıcı", "admin.billing.history.noBillingHistory": "Gelecekte, fatura geçmişiniz burada görüntülenecek.", - "admin.billing.history.onPremUsers": "{num} kullanıcı", "admin.billing.history.pageInfo": "{startRecord} - {endRecord} / {totalRecords}", "admin.billing.history.paid": "Ödendi", "admin.billing.history.paymentFailed": "Ödenmedi", @@ -271,7 +267,6 @@ "admin.billing.history.title": "Faturalama geçmişi", "admin.billing.history.total": "Toplam", "admin.billing.history.transactions": "İşlemler", - "admin.billing.history.usersAndRates": "{fullUsers} kullanıcı için tam ödeme, {partialUsers} kullanıcı için kısmi ödeme", "admin.billing.payment_info.add": "Kredi kartı ekle", "admin.billing.payment_info.billingAddress": "Fatura adresi", "admin.billing.payment_info.cardBrandAndDigits": "{brand} {digits} ile biten", @@ -393,8 +388,6 @@ "admin.billing.subscriptions.billing_summary.lastInvoice.taxes": "Vergiler", "admin.billing.subscriptions.billing_summary.lastInvoice.title": "Son fatura", "admin.billing.subscriptions.billing_summary.lastInvoice.total": "Toplam", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCount": " x {users} kullanıcı", - "admin.billing.subscriptions.billing_summary.lastInvoice.userCountPartial": "{users} kullanıcı", "admin.billing.subscriptions.billing_summary.lastInvoice.viewInvoice": "Faturayı görüntüle", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges": "Kısmi ödemeler nedir?", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges.message": "Ayın tamamı boyunca etkin olmayan kullanıcılardan aylık kullanım ile orantılı bir ödeme alınır.", @@ -1336,7 +1329,6 @@ "admin.license.upload-modal.file": "Dosya", "admin.license.upload-modal.subtitle": "Bu sunucuyu üst tarifeye geçirmek için bir Mattermost Enterprise paketi lisans anahtarı yükleyin. ", "admin.license.upload-modal.successfulUpgrade": "Üst tarifeye geçildi!", - "admin.license.upload-modal.successfulUpgradeText": "{licensedUsersNum, number} kullanıcı için {skuName} tarifesine geçtiniz. {startsAt} ile {expiresAt} tarihleri arasında geçerli olacak. ", "admin.license.upload-modal.title": "Bir lisans anahtarı yükleyin", "admin.license.uploadFile": "Dosya yükle", "admin.license.warn.renew": "Yenile", @@ -2534,7 +2526,6 @@ "analytics.system.postTypes": "İletiler, dosyalar ve hashtaglar", "analytics.system.privateGroups": "Özel kanallar", "analytics.system.publicChannels": "Herkese açık kanallar", - "analytics.system.seatsPurchased": "Ücreti ödenmiş kullanıcı sayısı", "analytics.system.skippedIntensiveQueries": "En iyi başarımı elde etmek için bazı istatistikler devre dışı bırakılmıştır. Bu istatistikleri config.json içinden etkinleştirebilirsiniz.", "analytics.system.textPosts": "Yalnızca metin içeren iletiler", "analytics.system.title": "Sistem istatistikleri", @@ -2554,7 +2545,6 @@ "analytics.team.activeUsers": "İleti yazmış etkin kullanıcılar", "analytics.team.newlyCreated": "Yeni eklenen kullanıcılar", "analytics.team.noTeams": "Bu sunucuda istatistikleri görüntülenebilecek bir takım yok.", - "analytics.team.overageUsersSeats": "Bu ücreti ödenmiş kullanıcı sayısını aşıyor", "analytics.team.privateGroups": "Özel kanallar", "analytics.team.publicChannels": "Herkese açık kanallar", "analytics.team.recentUsers": "Son etkin kullanıcılar", @@ -2606,7 +2596,6 @@ "app.channel.post_update_channel_purpose_message.removed": "{username} kanal amacını kaldırdı (önceki: {old})", "app.channel.post_update_channel_purpose_message.updated_from": "{username}, {old} eski kanal amacını {new} olarak değiştirdi", "app.channel.post_update_channel_purpose_message.updated_to": "{username} kanal amacını {new} olarak güncelledi", - "app.plugin.marketplace_plugins.app_error": "Mağaza sunucusu ile bağlantı kurulurken sorun çıktı. Lütfen [Sistem panosu]({siteURL}/admin_console/plugins/plugin_management) üzerinden ayarlarınızı denetleyin.", "apps.error": "Hata: {error}", "apps.error.command.field_missing": "Zorunlu alan eksik: `{fieldName}`.", "apps.error.command.same_channel": "`{fieldName}`alanında kanal yineleniyor: `{option}`.", @@ -4019,7 +4008,6 @@ "mark_all_threads_as_read_modal.title": "Tüm konularınız okunmuş olarak işaretlensin mi?", "marketplace_command.disabled": "Mağaza devre dışı bırakılmış. Lütfen ayrıntılı bilgi almak için sistem yöneticiniz ile görüşün.", "marketplace_command.no_permission": "Mağazaya erişmek için yeterli izinleriniz yok.", - "marketplace_list.count_total_page": "{startCount, number} - {endCount, number} {total, plural, one {uygulama eki} other {uygulama eki}} / {total, number}", "marketplace_modal.install_plugins": "Uygulama ekleri kur", "marketplace_modal.installing": "Kuruluyor...", "marketplace_modal.list.configure": "Yapılandır", @@ -4466,7 +4454,6 @@ "pricing_modal.planSummary.professional": "Büyük ekipler için yönetim, güvenlik ve uygunluk", "pricing_modal.plan_label_trialDays": "DENEMENİN BİTMESİNE {days} GÜN KALDI", "pricing_modal.price.freeForever": "Sonsuza dek ücretsiz", - "pricing_modal.rate.userPerMonth": "USD kullanıcı/ay{br}(yıllık faturalanır)", "pricing_modal.reviewDeploymentOptions": "Dağıtım seçeneklerini gözden geçirin", "pricing_modal.start_trial.disclaimer": "30 günlük ücretsiz denemeyi başlat üzerine tıklayarak, Mattermost yazılım ve hizmet lisans sözleşmesi, Kişisel verilerin gizliliği ilkesi metinlerini ve ürün ile ilgili e-postaları almayı kabul ediyorum.", "pricing_modal.subtitle": "Başlamak için bir tarife seçin", @@ -4603,12 +4590,9 @@ "self_hosted_signup.cta": "Yükselt", "self_hosted_signup.disclaimer": "Enterprise Edition abonelik koşullarını okudum ve kabul ediyorum", "self_hosted_signup.error_invalid_number": "Geçerli bir koltuk lisansı sayısı yazın", - "self_hosted_signup.error_max_seats": " yalnızca {num} kullanıcıya kadar lisans satın alımı desteklenir", - "self_hosted_signup.error_min_seats": "Çalışma alanınızda şu anda {num} kullanıcı var", "self_hosted_signup.failed_export.subtitle": "Kontrollerimizi yapacağız ve lisansınızı onaylandıktan sonra 3 gün içinde size geri döneceğiz. Bu arada, lütfen ürünümüzün ücretsiz sürümünü kullanmayı sürdürmekten çekinmeyin.", "self_hosted_signup.failed_export.title": "İşleminiz inceleniyor", "self_hosted_signup.license_applied": "{planName} lisansınız etkinleştirildi. {planName} özelliklerini kullanabilirsiniz.", - "self_hosted_signup.line_item_subtotal": "{num} kullanıcı × 12 ay.", "self_hosted_signup.organization": "Kuruluş adı", "self_hosted_signup.progress_step.applying_license": "{planName} lisansınız Mattermost kopyanıza uygulanıyor", "self_hosted_signup.progress_step.submitting_payment": "Ödeme bilgileri gönderiliyor", @@ -4618,10 +4602,10 @@ "self_hosted_signup.purchase_in_progress.by_self_restart": "Bir hata olduğunu düşünüyorsanız, satın alma işleminizi yeniden başlatın.", "self_hosted_signup.purchase_in_progress.reset": "Satın almayı yeniden başlat", "self_hosted_signup.purchase_in_progress.title": "Satın alma işlemi sürüyor", + "self_hosted_signup.error_min_seats": "Çalışma alanınızda şu anda {num} kullanıcı var", "self_hosted_signup.retry": "Yeniden dene", "self_hosted_signup.screening_description": "Kontrollerimizi yapacağız ve lisansınızı onaylandıktan sonra 3 gün içinde size geri döneceğiz. Bu arada, lütfen ürünümüzün ücretsiz sürümünü kullanmayı sürdürmekten çekinmeyin.", "self_hosted_signup.screening_title": "İşleminiz inceleniyor", - "self_hosted_signup.seats": "Kullanıcı koltuk lisansı", "self_hosted_signup.signup_consequences": "Faturanız bugün kesilecek. Lisansınız otomatik olarak etkinleştirilecek. Faturalamanın nasıl işlediğine bakın.", "self_hosted_signup.total": "Toplam", "setting_item_max.cancel": "İptal", @@ -4867,11 +4851,7 @@ "start_trial.modal.gettingTrial": "Deneme sürümü alınıyor...", "start_trial.modal.loaded": "Yüklendi!", "start_trial.modal.loading": "Yükleniyor...", - "start_trial.modal_body": "Gelişmiş güvenlik ve kurumsal uyumluluk ile tüm platform özelliklerine erişin.", - "start_trial.modal_btn.nottnow": "Şimdi değil", - "start_trial.modal_btn.start": "30 günlük ücretsiz denemeyi başlat", "start_trial.modal_btn.start_free_trial": "30 günlük ücretsiz denemeyi başlat", - "start_trial.modal_title": "Ücretsiz Enterprise tarifesi deneme sürenizi başlatın", "start_trial.tutorialTip.desc": "En çok istenilen ücretli özelliklerimizi keşfedin. Konuk hesapları ile kullanıcı erişimini sınırlayın, uyumluluk raporlarını otomatikleştirin ve yalnızca kimliğe özel güvenli mobil anında iletileri gönderin.", "start_trial.tutorialTip.title": "Ücretli özelliklerimizi ücretsiz olarak deneyin", "status_dropdown.dnd_sub_menu_header": "Bildirimler şu zamana kadar kapatılsın:", diff --git a/webapp/channels/src/i18n/uk.json b/webapp/channels/src/i18n/uk.json index 4afba9192e..20abccf1f8 100644 --- a/webapp/channels/src/i18n/uk.json +++ b/webapp/channels/src/i18n/uk.json @@ -11,7 +11,6 @@ "about.enterpriseEditione1": "Enterprise Edition", "about.hash": "Хеш збірки:", "about.hashee": "Хеш збірки EE:", - "about.hashwebapp": "Хеш збірки Webapp:", "about.licensed": "Ліцензовано на:", "about.notice": "Mattermost це стало можливим за допомогою програмного забезпечення з відкритим кодом, яке використовується на нашому сервері , desktop та mobile додатків.", "about.privacy": "Політика конфіденційності", @@ -2193,7 +2192,6 @@ "login.verified": "Електронну пошту підтверджено", "login_mfa.submit": "Відправити", "login_mfa.token": "Токен MFA", - "marketplace_list.count_total_page": "{startCount, number} - {endCount, number} {count, plural, one {user} other {users}} of {total, number} total", "marketplace_modal.install_plugins": "Встановлені плагіни:", "marketplace_modal.list.try_again": "Спробуй ще раз ", "marketplace_modal.list.update": "Оновити", diff --git a/webapp/channels/src/i18n/zh-CN.json b/webapp/channels/src/i18n/zh-CN.json index 2db89a3255..4aaf9b2f93 100644 --- a/webapp/channels/src/i18n/zh-CN.json +++ b/webapp/channels/src/i18n/zh-CN.json @@ -11,7 +11,6 @@ "about.enterpriseEditione1": "企业版", "about.hash": "编译哈希:", "about.hashee": "企业版编译哈希:", - "about.hashwebapp": "网页应用编译哈系:", "about.licensed": "授权给:", "about.notice": "开源软件帮助 Mattermost 实现了我们的服务端桌面以及移动应用。", "about.privacy": "隐私政策", @@ -265,10 +264,7 @@ "admin.billing.history.allPaymentsShowHere": "您所有的发票都将显示在这里", "admin.billing.history.date": "日期", "admin.billing.history.description": "描述", - "admin.billing.history.fractionalAndRatedUsers": "{fractionalUsers} 计量用户, {fullUsers} 全款用户, {partialUsers} 部分收费用户", - "admin.billing.history.fractionalUsers": "{fractionalUsers} 位用户", "admin.billing.history.noBillingHistory": "这是您的帐单记录显示的地方。", - "admin.billing.history.onPremUsers": "{num} 用户", "admin.billing.history.pageInfo": "{startRecord} - {endRecord},共 {totalRecords} 个", "admin.billing.history.paid": "已支付", "admin.billing.history.paymentFailed": "支付失败", @@ -278,7 +274,6 @@ "admin.billing.history.title": "帐单记录", "admin.billing.history.total": "总计", "admin.billing.history.transactions": "交易", - "admin.billing.history.usersAndRates": "{fullUsers} 位用户全额收费,{partialUsers} 位用户收取部分费用", "admin.billing.payment_info.add": "添加信用卡", "admin.billing.payment_info.billingAddress": "帐单地址", "admin.billing.payment_info.cardBrandAndDigits": "{brand} 以 {digits} 结尾", @@ -319,7 +314,16 @@ "admin.billing.subscription.creditCardExpired": "您的信用卡已过期。请更新您的付款信息,以免造成任何中断。", "admin.billing.subscription.creditCardHasExpired": "您的信用卡已过期", "admin.billing.subscription.creditCardHasExpired.description": "请更新您的 付款信息 以避免任何中断服务。", + "admin.billing.subscription.deleteWorkspaceModal.cancelButton": "保持订阅", + "admin.billing.subscription.deleteWorkspaceModal.deleteButton": "删除工作区", + "admin.billing.subscription.deleteWorkspaceModal.downgradeButton": "降级为免费", + "admin.billing.subscription.deleteWorkspaceModal.title": "您确定要删除吗?", + "admin.billing.subscription.deleteWorkspaceModal.usage": "作为你付费订阅Mattermost {sku} 的一部分,你目前创建了 ", "admin.billing.subscription.deleteWorkspaceModal.usageDetails": "{messageCount} 条消息和 {fileSize} 的文件", + "admin.billing.subscription.deleteWorkspaceModal.warning": "删除您的工作区是决定性的操作。在删除时,您将失去所有相关数据并且没有能力进行恢复。如果您降级到免费版,您将仍然能看到此信息。", + "admin.billing.subscription.deleteWorkspaceSection.delete": "删除工作区", + "admin.billing.subscription.deleteWorkspaceSection.description": "删除 {workspaceLink} 是最终决定并且以后不能恢复。", + "admin.billing.subscription.deleteWorkspaceSection.title": "删除你的工作区", "admin.billing.subscription.downgradedSuccess": "您现在订阅了 {productName}", "admin.billing.subscription.downgrading": "降级您的工作区", "admin.billing.subscription.featuresAvailable": "{productName} 功能现在已可以使用。", @@ -370,6 +374,7 @@ "admin.billing.subscription.planDetails.userCount": "{userCount} 位用户", "admin.billing.subscription.privateCloudCard.cloudEnterprise.description": "在 Mattermost,我们与您和您的团队合作,以满足您对整个产品的需求。如果您正在寻找年度折扣,请联系我们的销售团队。", "admin.billing.subscription.privateCloudCard.cloudEnterprise.title": "在寻找年度折扣吗? ", + "admin.billing.subscription.privateCloudCard.cloudFree.description": "使用访客账户,Office365套件集起,GitLab SSO和高级权限优化你的流程。", "admin.billing.subscription.privateCloudCard.cloudFree.title": "升级至 Cloud Professional 版", "admin.billing.subscription.privateCloudCard.cloudProfessional.description": "拥有高级支持的高级安全性和合规性功能。详细信息请参阅 {pricingLink}。", "admin.billing.subscription.privateCloudCard.cloudProfessional.title": "升级到企业级云", @@ -387,6 +392,7 @@ "admin.billing.subscription.providePaymentDetails": "提供您的付款明细", "admin.billing.subscription.returnToTeam": "返回到{team}", "admin.billing.subscription.stateprovince": "州/省", + "admin.billing.subscription.switchedToAnnual.title": "您现在的切换到了 {selectedProductName}", "admin.billing.subscription.title": "订阅", "admin.billing.subscription.updatePaymentInfo": "更新付款信息", "admin.billing.subscription.upgradedSuccess": "您现在已升级到{productName}", @@ -394,6 +400,8 @@ "admin.billing.subscription.userCount.tooltipTitle": "当前用户数", "admin.billing.subscription.verifyPaymentInformation": "验证您的付款信息中", "admin.billing.subscription.viewBilling": "查看账单", + "admin.billing.subscriptions.billing_summary.explore_enterprise": "浏览企业版功能", + "admin.billing.subscriptions.billing_summary.explore_enterprise.cta": "查看所有功能", "admin.billing.subscriptions.billing_summary.lastInvoice.failed": "失败", "admin.billing.subscriptions.billing_summary.lastInvoice.monthlyFlatFee": "每月固定费用", "admin.billing.subscriptions.billing_summary.lastInvoice.paid": "已支付", @@ -405,11 +413,26 @@ "admin.billing.subscriptions.billing_summary.lastInvoice.total": "总计", "admin.billing.subscriptions.billing_summary.lastInvoice.userCount": " x {users} 用户", "admin.billing.subscriptions.billing_summary.lastInvoice.userCountPartial": "{users} 用户", + "admin.billing.subscriptions.billing_summary.lastInvoice.viewInvoice": "查看发票", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges": "什么是部分费用?", "admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges.message": "在一个月的整个时间内未启用的用户将按比例按月计费。", "admin.billing.subscriptions.billing_summary.noBillingHistory.description": "将来,这是您最近的帐单摘要将显示的位置。", "admin.billing.subscriptions.billing_summary.noBillingHistory.link": "查看计费方式", "admin.billing.subscriptions.billing_summary.noBillingHistory.title": "尚无帐单记录", + "admin.billing.subscriptions.billing_summary.try_enterprise": "免费试用企业版功能", + "admin.billing.subscriptions.billing_summary.try_enterprise.cta": "免费试用{trialLength} 天", + "admin.billing.subscriptions.billing_summary.upcomingInvoice.has_more_line_items": "有{count}更多条目", + "admin.billing.subscriptions.billing_summary.upgrade_professional": "升级至专业套餐", + "admin.billing.subscriptions.billing_summary.upgrade_professional.cta": "升级", + "admin.billing.trueUpReview.button_download": "下载数据", + "admin.billing.trueUpReview.button_share": "分享到Mattermost", + "admin.billing.trueUpReview.docsLinkCTA": "了解更多关于true-up.", + "admin.billing.trueUpReview.due_date": "到期 ", + "admin.billing.trueUpReview.share_data_for_review": "与Mattermost分享您的系统统计数据以进行季度的True-Up审核。{link}", + "admin.billing.trueUpReview.submit.thanks_for_sharing": "谢谢您分享与True-Up审核相关的数据。", + "admin.billing.trueUpReview.submit_error": "发送您的TrueUp审核时发生错误。请重试。", + "admin.billing.trueUpReview.submit_success": "成功!", + "admin.billing.trueUpReview.title": "True Up审核", "admin.bleve.bulkIndexingTitle": "批量索引:", "admin.bleve.createJob.help": "数据库中所有的用户、频道以及消息将从旧到新顺序索引。Bleve 可以在索引过程中使用但搜索结果可能不完整。", "admin.bleve.createJob.title": "立刻索引", @@ -531,6 +554,7 @@ "admin.cluster.status_table.url": "Gossip 地址", "admin.cluster.status_table.version": "版本", "admin.cluster.unknown": "未知", + "admin.cluster.version_mismatch_warning": "警告:您的高可用集群检测出多个版本的Mattermost。如果您不是正在进行升级,请确认集群所有的节点使用相同的Mattermost版本,以防平台功能异常。", "admin.compliance.complianceMonitoring": "合规监视", "admin.compliance.directoryDescription": "用户保存守规报告。如果为空,将被设置为 ./data/。", "admin.compliance.directoryExample": "例如 \"./data/\"", @@ -604,6 +628,8 @@ "admin.connectionSecurityTlsDescription": "加密Mattermost和您的服务器之间的通信。", "admin.custom_terms_of_service_feature_discovery.copy": "创建您自己的服务条款,新用户在访问桌面、网页或移动设备上的 Mattermost 之前必须接受该条款。", "admin.custom_terms_of_service_feature_discovery.title": "使用 Mattermost Enterprise 创建自定义服务条款", + "admin.customization.allowSyncedDrafts": "开启服务端草稿消息同步:", + "admin.customization.allowSyncedDraftsDesc": "当开启,用户的消息草稿将同步至服务端,所有的设备都可以看到。用户可以在帐户设置里关闭这个功能。", "admin.customization.androidAppDownloadLinkDesc": "添加安卓应用下载链接。用移动设备访问的用户将看到应用下载提示页面。此栏留空将不显示。", "admin.customization.androidAppDownloadLinkTitle": "安卓应用下载链接:", "admin.customization.announcement.allowBannerDismissalDesc": "当设为是时,用户可以撤掉横幅直到下次更新。当设为否时,横幅将永久显示直到被系统管理员关闭。", @@ -633,6 +659,8 @@ "admin.customization.enableLinkPreviewsTitle": "启用网站链接预览:", "admin.customization.enablePermalinkPreviewsDesc": "启用后,指向 Mattermost 消息的链接将为有权访问原始消息的任何用户生成预览。请查看我们的文档了解详细信息。", "admin.customization.enablePermalinkPreviewsTitle": "开启消息链接预览:", + "admin.customization.enablePublicSharedBoardsDesc": "此项允许面板编辑使用链接分享面板给所有人访问。", + "admin.customization.enablePublicSharedBoardsTitle": "启用公共访问面板:", "admin.customization.enableSVGsDesc": "开启 SVG 附件预览并让它们显示在消息中。", "admin.customization.enableSVGsTitle": "开启 SVG:", "admin.customization.gfycatApiKey": "Gfycat API Key:", @@ -708,6 +736,12 @@ "admin.database.title": "数据库", "admin.developer.title": "开发人员设置", "admin.elasticsearch.bulkIndexingTitle": "批量索引:", + "admin.elasticsearch.caExample": "例:\"./elasticsearch/ca.pem\"", + "admin.elasticsearch.caTitle": "CA path:", + "admin.elasticsearch.clientCertExample": "例:\"./elasticsearch/client-cert.pem\"", + "admin.elasticsearch.clientCertTitle": "客户端证书路径:", + "admin.elasticsearch.clientKeyExample": "例:\"./elasticsearch/client-cert.pem\"", + "admin.elasticsearch.clientKeyTitle": "客户端证书钥匙路径:", "admin.elasticsearch.connectionUrlDescription": "Elasticsearch 服务器地址。{documentationLink}", "admin.elasticsearch.connectionUrlExample": "例如:\"https://elasticsearch.example.org:9200\"", "admin.elasticsearch.connectionUrlExample.documentationLinkText": "请见服务器架设说明文档。", @@ -798,6 +832,9 @@ "admin.environment.notifications.replyToAddress.label": "通知 Reply-To 地址:", "admin.environment.notifications.replyToAddress.placeholder": "例如:\"mattermost@yourcompany.com\", \"admin@yourcompany.com\"", "admin.environment.notifications.supportAddress.placeholder": "例:“support@yourcompany.com”,“admin@yourcompany.com”", + "admin.environment.notifications.supportEmail.help": "客户支持邮件里显示的电子邮件地址。", + "admin.environment.notifications.supportEmail.label": "客户支持邮件地址:", + "admin.environment.notifications.supportEmail.required": "需要“客户支持电子邮件地址”", "admin.environment.pushNotificationServer": "推送通知服务器", "admin.environment.smtp": "SMTP", "admin.environment.smtp.connectionSecurity.option.none": "无", @@ -812,6 +849,7 @@ "admin.environment.smtp.smtpAuth.description": "当设为是时,开启 SMTP 验证。", "admin.environment.smtp.smtpAuth.title": "开启 SMTP 验证:", "admin.environment.smtp.smtpFail": "连接失败:{error}", + "admin.environment.smtp.smtpFailure": "SMTP没有在在系统控制台里配置。可以在 这里配置。", "admin.environment.smtp.smtpPassword.description": "从邮件服务器管理员获得此凭据。", "admin.environment.smtp.smtpPassword.placeholder": "例如:\"yourpassword\"、\"jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY\"", "admin.environment.smtp.smtpPassword.title": "SMTP 服务器密码:", @@ -2427,7 +2465,6 @@ "app.channel.post_update_channel_purpose_message.removed": "{username} 移除了频道作用 (曾是:{old})", "app.channel.post_update_channel_purpose_message.updated_from": "{username} 更新了频道作用从:{old} 到:{new}", "app.channel.post_update_channel_purpose_message.updated_to": "{username} 更新了频道作用到:{new}", - "app.plugin.marketplace_plugins.app_error": "连接到集市服务器出错。请检查[系统控制台]({siteURL}/admin_console/plugins/plugin_management)的设定。", "apps.error": "错误:{error}", "apps.error.command.field_missing": "缺少必填字段:`{fieldName}`。", "apps.error.command.same_channel": "频道重复,字段 `{fieldName}`:`{option}`。", @@ -3525,7 +3562,6 @@ "manage_channel_groups_modal.search_placeholder": "搜索组", "manage_team_groups_modal.search_placeholder": "搜索组", "mark_all_threads_as_read_modal.cancel": "取消", - "marketplace_list.count_total_page": "{startCount, number} - {endCount, number} {count} 个,共 {total, number} 个", "marketplace_modal.install_plugins": "安装插件", "marketplace_modal.installing": "安装中...", "marketplace_modal.list.configure": "配置", @@ -4043,9 +4079,6 @@ "start_trial.modal.failed": "失败", "start_trial.modal.loaded": "已加载!", "start_trial.modal.loading": "加载中...", - "start_trial.modal_btn.nottnow": "现在不要", - "start_trial.modal_btn.start": "开始 30 天试用", - "start_trial.modal_title": "立即开始您的免费企业版试用", "status_dropdown.dnd_sub_menu_header": "禁用通知,直到:", "status_dropdown.dnd_sub_menu_item.custom": "自定义", "status_dropdown.dnd_sub_menu_item.one_hour": "1 小时", @@ -4127,7 +4160,7 @@ "team_settings_modal.title": "团队设置", "team_sidebar.join": "您可以加入的其他团队", "terms_of_service.agreeButton": "我同意", - "terms_of_service.api_error": "无法完成请求。如果此问题持续,请联系您的系统管理员。", + "terms_of_service.api_error": "无法完成请求。如果此问题仍然存在,请联系您的系统管理员。", "terms_of_service.disagreeButton": "我不同意", "test": "文本按钮", "textbox.bold": "**加粗**", diff --git a/webapp/channels/src/i18n/zh-TW.json b/webapp/channels/src/i18n/zh-TW.json index 63c968580a..423beece99 100644 --- a/webapp/channels/src/i18n/zh-TW.json +++ b/webapp/channels/src/i18n/zh-TW.json @@ -11,7 +11,6 @@ "about.enterpriseEditione1": "企業版", "about.hash": "編譯 Hash:", "about.hashee": "企業版編譯 Hash:", - "about.hashwebapp": "網頁程式建置雜湊:", "about.licensed": "授權給:", "about.notice": "藉由在伺服器桌面行動裝置上使用的開源軟體,才得以實現 Mattermost。", "about.privacy": "隱私政策", @@ -1929,7 +1928,6 @@ "app.channel.post_update_channel_purpose_message.removed": "{username} 已移除頻道用途(原為:{old})", "app.channel.post_update_channel_purpose_message.updated_from": "{username} 已更新頻道用途:從 {old} 改為 {new}", "app.channel.post_update_channel_purpose_message.updated_to": "{username} 已更新頻道用途為:{new}", - "app.plugin.marketplace_plugins.app_error": "連線至市集伺服器失敗。請檢查[系統控制台]({siteURL}/admin_console/plugins/plugin_management)的設定。", "apps.error": "錯誤:{error}", "apps.error.command.field_missing": "遺漏必填欄位:`{fieldName}`。", "apps.error.command.same_channel": "頻道重複的欄位 `{fieldName}`:`{option}`。", @@ -2848,7 +2846,6 @@ "login_mfa.token": "多重要素驗證 Token", "manage_channel_groups_modal.search_placeholder": "搜尋群組", "manage_team_groups_modal.search_placeholder": "搜尋群組", - "marketplace_list.count_total_page": "{total, number}位中{startCount, number} - {endCount, number}位使用者", "marketplace_modal.install_plugins": "安裝模組", "marketplace_modal.installing": "安裝中...", "marketplace_modal.list.configure": "設定", diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/timezone.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/timezone.ts index 655088a0fa..823a519af1 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/actions/timezone.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/timezone.ts @@ -2,15 +2,16 @@ // See LICENSE.txt for license information. import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; -import {makeGetUserTimezone} from 'mattermost-redux/selectors/entities/timezone'; +import {getCurrentTimezoneFull} from 'mattermost-redux/selectors/entities/timezone'; import {DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions'; import {updateMe} from './users'; + export function autoUpdateTimezone(deviceTimezone: string) { return async (dispatch: DispatchFunc, getState: GetStateFunc) => { const currentUser = getCurrentUser(getState()); - const currentTimezone = makeGetUserTimezone()(getState(), currentUser.id); + const currentTimezone = getCurrentTimezoneFull(getState()); const newTimezoneExists = currentTimezone.automaticTimezone !== deviceTimezone; if (currentTimezone.useAutomaticTimezone && newTimezoneExists) { diff --git a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/timezone.ts b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/timezone.ts index 51505d5de5..5ff93935cd 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/timezone.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/timezone.ts @@ -1,17 +1,17 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import timezones, {Timezone} from 'timezones.json'; - -import {getUser} from 'mattermost-redux/selectors/entities/users'; +import timezones from 'timezones.json'; import {GlobalState} from '@mattermost/types/store'; -import {UserProfile, UserTimezone} from '@mattermost/types/users'; +import {UserProfile} from '@mattermost/types/users'; import {createSelector} from 'reselect'; -import {getUserCurrentTimezone, getTimezoneLabel as getTimezoneLabelUtil} from 'mattermost-redux/utils/timezone_utils'; +import {getTimezoneLabel, getUserCurrentTimezone} from 'mattermost-redux/utils/timezone_utils'; -export function getTimezoneForUserProfile(profile: UserProfile) { +import {getCurrentUser} from './common'; + +function getTimezoneForUserProfile(profile: UserProfile) { if (profile && profile.timezone) { return { ...profile.timezone, @@ -31,23 +31,30 @@ export function isTimezoneEnabled(state: GlobalState) { return config.ExperimentalTimezone === 'true'; } -export const makeGetUserTimezone = () => createSelector( - 'makeGetUserTimezone', - (state: GlobalState, userId: string) => getUser(state, userId), - (user: UserProfile) => { - return getTimezoneForUserProfile(user); +export const getCurrentTimezoneFull = createSelector( + 'getCurrentTimezoneFull', + getCurrentUser, + (currentUser) => { + return getTimezoneForUserProfile(currentUser); }, ); -export const getTimezoneLabel: (state: GlobalState, userId: UserProfile['id']) => string = createSelector( - 'getTimezoneLabel', - () => timezones, - makeGetUserTimezone(), - (timezones: Timezone[], timezoneObject: UserTimezone) => { - const timezone = getUserCurrentTimezone(timezoneObject); +export const getCurrentTimezone = createSelector( + 'getCurrentTimezone', + getCurrentTimezoneFull, + (timezoneFull) => { + return getUserCurrentTimezone(timezoneFull); + }, +); + +export const getCurrentTimezoneLabel = createSelector( + 'getCurrentTimezoneLabel', + getCurrentTimezone, + (timezone) => { if (!timezone) { return ''; } - return getTimezoneLabelUtil(timezones, timezone); + + return getTimezoneLabel(timezones, timezone); }, ); diff --git a/webapp/channels/src/packages/mattermost-redux/src/utils/timezone_utils.ts b/webapp/channels/src/packages/mattermost-redux/src/utils/timezone_utils.ts index b50c3d230d..afcb08cfe5 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/utils/timezone_utils.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/utils/timezone_utils.ts @@ -5,9 +5,9 @@ import {Timezone} from 'timezones.json'; import {UserTimezone} from '@mattermost/types/users'; -export function getUserCurrentTimezone(userTimezone?: UserTimezone): string | undefined | null { +export function getUserCurrentTimezone(userTimezone?: UserTimezone): string { if (!userTimezone) { - return null; + return 'UTC'; } const { useAutomaticTimezone, diff --git a/webapp/channels/src/sass/base/_structure.scss b/webapp/channels/src/sass/base/_structure.scss index 0ec379a86d..7171f80757 100644 --- a/webapp/channels/src/sass/base/_structure.scss +++ b/webapp/channels/src/sass/base/_structure.scss @@ -160,7 +160,6 @@ body.app__body #root { } #SidebarContainer.move--right { position: relative; - left: 65px; } } } diff --git a/webapp/channels/src/sass/components/_post-right.scss b/webapp/channels/src/sass/components/_post-right.scss index ff9c7be945..c5784a917d 100644 --- a/webapp/channels/src/sass/components/_post-right.scss +++ b/webapp/channels/src/sass/components/_post-right.scss @@ -79,6 +79,8 @@ } &.post--compact { + padding-left: 1em; + .post__body { padding-left: 0; } @@ -102,7 +104,7 @@ &.same--user { .post__header { .col__name { - display: inline-block; + display: flex; } } } diff --git a/webapp/channels/src/sass/components/_post.scss b/webapp/channels/src/sass/components/_post.scss index f1aeca6b83..0a1dbb46d6 100644 --- a/webapp/channels/src/sass/components/_post.scss +++ b/webapp/channels/src/sass/components/_post.scss @@ -2395,8 +2395,7 @@ .post.current--user { &.other--root, - &.post--root, - &.post--comment { + &.post--root, { .post__header-set-custom-status { display: revert; padding: 0 6px; diff --git a/webapp/channels/src/sass/layout/_sidebar-left.scss b/webapp/channels/src/sass/layout/_sidebar-left.scss index c670182893..2f5aa4759a 100644 --- a/webapp/channels/src/sass/layout/_sidebar-left.scss +++ b/webapp/channels/src/sass/layout/_sidebar-left.scss @@ -575,6 +575,18 @@ $sidebarOpacityAnimationDuration: 0.15s; } } + @media screen and (min-width: 768px) { + .SidebarNavContainer { + .scrollbar--view { + max-width: 240px; + } + } + + #SidebarContainer .SidebarChannelGroup .SidebarChannelGroupHeader { + max-width: 240px; + } + } + .SidebarCategory_newLabel { display: flex; width: 32px; @@ -741,6 +753,7 @@ $sidebarOpacityAnimationDuration: 0.15s; } .AddChannelsCtaDropdown .dropdown-menu { + min-width: 210px !important; margin-left: 20px; } @@ -748,20 +761,13 @@ $sidebarOpacityAnimationDuration: 0.15s; #addChannelsCta { display: flex; width: 100%; + margin-top: -6px; &:hover { background-color: var(--sidebar-text-hover-bg); } } - #AddChannelCtaDropdown { - position: fixed; - - ul { - min-width: 232px !important; - } - } - .SidebarChannelNavigator_inviteUsersSticky { position: absolute; z-index: 2; @@ -1021,7 +1027,6 @@ $sidebarOpacityAnimationDuration: 0.15s; /* Channels */ .SidebarChannel { display: flex; - overflow: hidden; height: 32px; /* height required for transition animation */ diff --git a/webapp/channels/src/sass/responsive/_tablet.scss b/webapp/channels/src/sass/responsive/_tablet.scss index 31d24fbc3f..c9159f6966 100644 --- a/webapp/channels/src/sass/responsive/_tablet.scss +++ b/webapp/channels/src/sass/responsive/_tablet.scss @@ -292,6 +292,8 @@ .post__permalink { position: absolute; top: 1px; + left: -76px; + width: 60px; text-align: right; } @@ -355,6 +357,10 @@ .post__header { .post-menu { top: -34px; + + &.post-menu--position { + top: -12px; + } } } } diff --git a/webapp/channels/src/sass/routes/_signup.scss b/webapp/channels/src/sass/routes/_signup.scss index e0dc3d1ae1..ac4ac2b959 100644 --- a/webapp/channels/src/sass/routes/_signup.scss +++ b/webapp/channels/src/sass/routes/_signup.scss @@ -3,7 +3,7 @@ body { &.announcement-bar--fixed { .signup-header { - top: 24px; + top: 42px; } } } diff --git a/webapp/channels/src/sass/utils/_mixins.scss b/webapp/channels/src/sass/utils/_mixins.scss index 2d3f74df34..bb4ef5b0a0 100644 --- a/webapp/channels/src/sass/utils/_mixins.scss +++ b/webapp/channels/src/sass/utils/_mixins.scss @@ -114,9 +114,13 @@ display: flex; font-size: 18px; - &::before { + &:first-child::before { margin: 0 7px 0 0; } + + &:last-child::before { + margin: 0 0 0 7px; + } } } @@ -146,7 +150,7 @@ outline: none; } - &:disabled { + &:disabled:not(.always-show-enabled) { background: rgba(var(--center-channel-color-rgb), 0.08); color: rgba(var(--center-channel-color-rgb), 0.32); cursor: not-allowed; diff --git a/webapp/channels/src/selectors/general.ts b/webapp/channels/src/selectors/general.ts index 1d7f84d00e..e38fea69c8 100644 --- a/webapp/channels/src/selectors/general.ts +++ b/webapp/channels/src/selectors/general.ts @@ -1,13 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {createSelector} from 'reselect'; - -import {getCurrentUser} from 'mattermost-redux/selectors/entities/common'; import {getConfig} from 'mattermost-redux/selectors/entities/general'; -import {getTimezoneForUserProfile} from 'mattermost-redux/selectors/entities/timezone'; - -import * as UserAgent from 'utils/user_agent'; import type {GlobalState} from 'types/store'; @@ -17,15 +11,6 @@ declare global { } } -export function areTimezonesEnabledAndSupported(state: GlobalState) { - if (UserAgent.isInternetExplorer()) { - return false; - } - - const config = getConfig(state); - return config.ExperimentalTimezone === 'true'; -} - export function getBasePath(state: GlobalState) { const config = getConfig(state) || {}; @@ -36,21 +21,6 @@ export function getBasePath(state: GlobalState) { return window.basename || '/'; } -export const getCurrentUserTimezone = createSelector( - 'getCurrentUserTimezone', - getCurrentUser, - areTimezonesEnabledAndSupported, - (user, enabledTimezone) => { - let timezone; - if (enabledTimezone) { - const userTimezone = getTimezoneForUserProfile(user); - timezone = userTimezone.useAutomaticTimezone ? userTimezone.automaticTimezone : userTimezone.manualTimezone; - } - - return timezone; - }, -); - export function getConnectionId(state: GlobalState) { return state.websocket.connectionId; } diff --git a/webapp/channels/src/selectors/views/custom_status.test.ts b/webapp/channels/src/selectors/views/custom_status.test.ts index fdbf13af94..8642fbc0ed 100644 --- a/webapp/channels/src/selectors/views/custom_status.test.ts +++ b/webapp/channels/src/selectors/views/custom_status.test.ts @@ -11,6 +11,7 @@ import {makeGetCustomStatus, getRecentCustomStatuses, isCustomStatusEnabled, sho import {TestHelper} from 'utils/test_helper'; import {CustomStatusDuration} from '@mattermost/types/users'; +import {addTimeToTimestamp, TimeInformation} from 'utils/utils'; jest.mock('mattermost-redux/selectors/entities/users'); jest.mock('mattermost-redux/selectors/entities/general'); @@ -86,6 +87,7 @@ describe('isCustomStatusEnabled', () => { }); describe('showStatusDropdownPulsatingDot and showPostHeaderUpdateStatusButton', () => { + const user = TestHelper.getUserMock(); const preference = { myPreference: { value: '', @@ -104,4 +106,31 @@ describe('showStatusDropdownPulsatingDot and showPostHeaderUpdateStatusButton', (PreferenceSelectors.get as jest.Mock).mockReturnValue(preference.myPreference.value); expect(showPostHeaderUpdateStatusButton(store.getState())).toBeFalsy(); }); + + it('should return false if user was created less than seven days before from today', async () => { + const store = await configureStore(); + (PreferenceSelectors.get as jest.Mock).mockReturnValue(preference.myPreference.value); + const todayTimestamp = new Date().getTime(); + + // set the user create date to 6 days in the past from today + const todayMinusSixDays = addTimeToTimestamp(todayTimestamp, TimeInformation.DAYS, 6, TimeInformation.PAST); + const newUser = {...user, create_at: todayMinusSixDays}; + newUser.props.customStatus = JSON.stringify(customStatus); + (UserSelectors.getCurrentUser as jest.Mock).mockReturnValue(newUser); + expect(showStatusDropdownPulsatingDot(store.getState())).toBeFalsy(); + }); + + it('should return true if user was created more than seven days before from today', async () => { + const store = await configureStore(); + preference.myPreference.value = JSON.stringify({[Preferences.CUSTOM_STATUS_MODAL_VIEWED]: false}); + (PreferenceSelectors.get as jest.Mock).mockReturnValue(preference.myPreference.value); + const todayTimestamp = new Date().getTime(); + + // set the user create date to 8 days in the past from today + const todayMinusEightDays = addTimeToTimestamp(todayTimestamp, TimeInformation.DAYS, 8, TimeInformation.PAST); + const newUser = {...user, create_at: todayMinusEightDays}; + newUser.props.customStatus = JSON.stringify(customStatus); + (UserSelectors.getCurrentUser as jest.Mock).mockReturnValue(newUser); + expect(showStatusDropdownPulsatingDot(store.getState())).toBeTruthy(); + }); }); diff --git a/webapp/channels/src/selectors/views/custom_status.ts b/webapp/channels/src/selectors/views/custom_status.ts index 80b2a93215..cd536331a7 100644 --- a/webapp/channels/src/selectors/views/custom_status.ts +++ b/webapp/channels/src/selectors/views/custom_status.ts @@ -5,6 +5,7 @@ import moment from 'moment-timezone'; import {createSelector} from 'reselect'; +import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone'; import {getCurrentUser, getUser} from 'mattermost-redux/selectors/entities/users'; import {getConfig} from 'mattermost-redux/selectors/entities/general'; @@ -12,8 +13,9 @@ import {get} from 'mattermost-redux/selectors/entities/preferences'; import {Preferences} from 'mattermost-redux/constants'; import {CustomStatusDuration, UserCustomStatus} from '@mattermost/types/users'; +import {isDateWithinDaysRange, TimeInformation} from 'utils/utils'; + import {GlobalState} from 'types/store'; -import {getCurrentUserTimezone} from 'selectors/general'; import {getCurrentMomentForTimezone} from 'utils/timezone'; export function makeGetCustomStatus(): (state: GlobalState, userID?: string) => UserCustomStatus { @@ -37,7 +39,7 @@ export function isCustomStatusExpired(state: GlobalState, customStatus?: UserCus } const expiryTime = moment(customStatus.expires_at); - const timezone = getCurrentUserTimezone(state); + const timezone = getCurrentTimezone(state); const currentTime = getCurrentMomentForTimezone(timezone); return currentTime.isSameOrAfter(expiryTime); } @@ -56,9 +58,12 @@ export function isCustomStatusEnabled(state: GlobalState) { } function showCustomStatusPulsatingDotAndPostHeader(state: GlobalState) { + // only show this for users after the first seven days + const currentUser = getCurrentUser(state); + const hasUserCreationMoreThanSevenDays = isDateWithinDaysRange(currentUser?.create_at, 7, TimeInformation.FUTURE); const customStatusTutorialState = get(state, Preferences.CATEGORY_CUSTOM_STATUS, Preferences.NAME_CUSTOM_STATUS_TUTORIAL_STATE); const modalAlreadyViewed = customStatusTutorialState && JSON.parse(customStatusTutorialState)[Preferences.CUSTOM_STATUS_MODAL_VIEWED]; - return !modalAlreadyViewed; + return !modalAlreadyViewed && hasUserCreationMoreThanSevenDays; } export function showStatusDropdownPulsatingDot(state: GlobalState) { diff --git a/webapp/channels/src/utils/constants.tsx b/webapp/channels/src/utils/constants.tsx index 7ea14fc0fa..d7e4644af2 100644 --- a/webapp/channels/src/utils/constants.tsx +++ b/webapp/channels/src/utils/constants.tsx @@ -1077,6 +1077,7 @@ export const HostedCustomerLinks = { BILLING_DOCS: 'https://mattermost.com/pl/how-self-hosted-billing-works', SELF_HOSTED_BILLING: 'https://docs.mattermost.com/manage/self-hosted-billing.html', TERMS_AND_CONDITIONS: 'https://mattermost.com/enterprise-edition-terms/', + SECURITY_UPDATES: 'https://mattermost.com/security-updates/', }; export const DocLinks = { diff --git a/webapp/channels/src/utils/utils.tsx b/webapp/channels/src/utils/utils.tsx index 4cdc34e57c..9f71b304d5 100644 --- a/webapp/channels/src/utils/utils.tsx +++ b/webapp/channels/src/utils/utils.tsx @@ -90,6 +90,23 @@ const CLICKABLE_ELEMENTS = [ 'audio', 'video', ]; +const MS_PER_SECOND = 1000; +const MS_PER_MINUTE = 60 * MS_PER_SECOND; +const MS_PER_HOUR = 60 * MS_PER_MINUTE; +const MS_PER_DAY = 24 * MS_PER_HOUR; + +export enum TimeInformation { + MILLISECONDS = 'm', + SECONDS = 's', + MINUTES = 'x', + HOURS = 'h', + DAYS = 'd', + FUTURE = 'f', + PAST = 'p' +} + +export type TimeUnit = Exclude; +export type TimeDirection = TimeInformation.FUTURE | TimeInformation.PAST; export function isMac() { return navigator.platform.toUpperCase().indexOf('MAC') >= 0; @@ -256,7 +273,6 @@ export function getTimestamp(): number { } export function getRemainingDaysFromFutureTimestamp(timestamp?: number): number { - const MS_PER_DAY = 24 * 60 * 60 * 1000; const futureDate = new Date(timestamp as number); const utcFuture = Date.UTC(futureDate.getFullYear(), futureDate.getMonth(), futureDate.getDate()); const today = new Date(); @@ -265,6 +281,39 @@ export function getRemainingDaysFromFutureTimestamp(timestamp?: number): number return Math.floor((utcFuture - utcToday) / MS_PER_DAY); } +export function addTimeToTimestamp(timestamp: number, type: TimeUnit, diff: number, timeline: TimeDirection) { + let modifier = 1; + switch (type) { + case TimeInformation.SECONDS: + modifier = MS_PER_SECOND; + break; + case TimeInformation.MINUTES: + modifier = MS_PER_MINUTE; + break; + case TimeInformation.HOURS: + modifier = MS_PER_HOUR; + break; + case TimeInformation.DAYS: + modifier = MS_PER_DAY; + break; + } + + return timeline === TimeInformation.FUTURE ? timestamp + (diff * modifier) : timestamp - (diff * modifier); +} + +/** + * Verifies if a date is in a particular given range of days from today + * @param timestamp date you want to check is in the range of the provided number of days from today + * @param days number of days you want to check your date against to + * @param timeline 'f' represents future, 'p' represents past + * @returns boolean, true if your date is in the range of the provided number of days + */ +export function isDateWithinDaysRange(timestamp: number, days: number, timeline: TimeDirection): boolean { + const today = new Date().getTime(); + const daysSince = Math.round((today - timestamp) / MS_PER_DAY); + return timeline === TimeInformation.PAST ? daysSince <= days : daysSince >= days; +} + export function getLocaleDateFromUTC(timestamp: number, format = 'YYYY/MM/DD HH:mm:ss', userTimezone = '') { if (!timestamp) { return moment.now(); diff --git a/webapp/channels/tsconfig.json b/webapp/channels/tsconfig.json index f71364fdc0..85f5958590 100644 --- a/webapp/channels/tsconfig.json +++ b/webapp/channels/tsconfig.json @@ -28,8 +28,6 @@ "reselect": ["packages/reselect/src"], "@mui/styled-engine": ["./node_modules/@mui/styled-engine-sc"], "!!file-loader*": ["utils/empty-string"], - "@e2e-support/*": ["e2e/playwright/support/*"], - "@e2e-test.config": ["e2e/playwright/test.config.ts"] } }, "include": [ diff --git a/webapp/channels/webpack.config.js b/webapp/channels/webpack.config.js index 911550300f..d37815646c 100644 --- a/webapp/channels/webpack.config.js +++ b/webapp/channels/webpack.config.js @@ -80,7 +80,7 @@ var config = { type: 'javascript/auto', test: /\.json$/, include: [ - path.resolve(__dirname, 'i18n'), + path.resolve(__dirname, 'src/i18n'), ], exclude: [/en\.json$/], use: [ @@ -441,7 +441,9 @@ if (DEV) { config.devtool = 'source-map'; } -const env = {}; +const env = { + STRIPE_PUBLIC_KEY: JSON.stringify(process.env.STRIPE_PUBLIC_KEY || ''), +}; if (DEV) { env.PUBLIC_PATH = JSON.stringify(publicPath); env.RUDDER_KEY = JSON.stringify(process.env.RUDDER_KEY || ''); diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 923fdfd0e4..987b8de866 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -3905,7 +3905,6 @@ "enzyme-to-json": "3.6.2", "eslint": "7.32.0", "eslint-import-resolver-webpack": "0.13.2", - "eslint-plugin-cypress": "2.11.3", "eslint-plugin-formatjs": "4.3.4", "eslint-plugin-header": "3.1.1", "eslint-plugin-import": "2.23.4", @@ -17819,18 +17818,6 @@ "eslint": ">=4.0.0" } }, - "node_modules/eslint-plugin-cypress": { - "version": "2.11.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.11.3.tgz", - "integrity": "sha512-hOoAid+XNFtpvOzZSNWP5LDrQBEJwbZwjib4XJ1KcRYKjeVj0mAmPmucG4Egli4j/aruv+Ow/acacoloWWCl9Q==", - "dev": true, - "dependencies": { - "globals": "^11.12.0" - }, - "peerDependencies": { - "eslint": ">= 3.2.1" - } - }, "node_modules/eslint-plugin-formatjs": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/eslint-plugin-formatjs/-/eslint-plugin-formatjs-4.3.4.tgz", @@ -50096,15 +50083,6 @@ "eslint-rule-composer": "^0.3.0" } }, - "eslint-plugin-cypress": { - "version": "2.11.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.11.3.tgz", - "integrity": "sha512-hOoAid+XNFtpvOzZSNWP5LDrQBEJwbZwjib4XJ1KcRYKjeVj0mAmPmucG4Egli4j/aruv+Ow/acacoloWWCl9Q==", - "dev": true, - "requires": { - "globals": "^11.12.0" - } - }, "eslint-plugin-formatjs": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/eslint-plugin-formatjs/-/eslint-plugin-formatjs-4.3.4.tgz", @@ -55129,7 +55107,6 @@ "enzyme-to-json": "3.6.2", "eslint": "7.32.0", "eslint-import-resolver-webpack": "0.13.2", - "eslint-plugin-cypress": "2.11.3", "eslint-plugin-formatjs": "4.3.4", "eslint-plugin-header": "3.1.1", "eslint-plugin-import": "2.23.4", diff --git a/webapp/platform/client/src/client4.ts b/webapp/platform/client/src/client4.ts index 063c706732..a58b1655ba 100644 --- a/webapp/platform/client/src/client4.ts +++ b/webapp/platform/client/src/client4.ts @@ -26,6 +26,7 @@ import { CreateSubscriptionRequest, Feedback, WorkspaceDeletionRequest, + NewsletterRequestBody, } from '@mattermost/types/cloud'; import { SelfHostedSignupForm, @@ -3893,6 +3894,13 @@ export default class Client4 { ); }; + subscribeToNewsletter = (newletterRequestBody: NewsletterRequestBody) => { + return this.doFetch( + `${this.getHostedCustomerRoute()}/subscribe-newsletter`, + {method: 'post', body: JSON.stringify(newletterRequestBody)}, + ); + }; + createPaymentMethod = async () => { return this.doFetch( `${this.getCloudRoute()}/payment`, diff --git a/webapp/platform/components/src/generic_modal/footer_content/__snapshots__/footer_pagination.test.tsx.snap b/webapp/platform/components/src/generic_modal/footer_content/__snapshots__/footer_pagination.test.tsx.snap new file mode 100644 index 0000000000..29d49c0a2a --- /dev/null +++ b/webapp/platform/components/src/generic_modal/footer_content/__snapshots__/footer_pagination.test.tsx.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`components/GenericModal/FooterPagination should render default 1`] = ` +
    +
    +
    + + +
    +
    +`; diff --git a/webapp/platform/components/src/generic_modal/footer_content/footer_pagination.scss b/webapp/platform/components/src/generic_modal/footer_content/footer_pagination.scss new file mode 100644 index 0000000000..35594f8bc5 --- /dev/null +++ b/webapp/platform/components/src/generic_modal/footer_content/footer_pagination.scss @@ -0,0 +1,41 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +@import '../../../../../channels/src/sass/utils/mixins'; + +.footer-pagination { + display: flex; + flex: 1; + flex-direction: row; + align-items: center; + + &__legend { + display: flex; + flex: 1; + align-items: center; + justify-content: flex-start; + color: rgba(var(--center-channel-color-rgb), 0.64); + font-size: 12px; + font-weight: 600; + line-height: 16px; + } + + &__button-container { + display: flex; + align-items: center; + justify-content: center; + + &__button { + @include tertiary-button; + @include button-small; + + &:not(:first-child) { + margin-left: 8px; + } + + > :not(:first-child) { + margin-left: 5px; + } + } + } +} diff --git a/webapp/platform/components/src/generic_modal/footer_content/footer_pagination.test.tsx b/webapp/platform/components/src/generic_modal/footer_content/footer_pagination.test.tsx new file mode 100644 index 0000000000..9b80d2a2fe --- /dev/null +++ b/webapp/platform/components/src/generic_modal/footer_content/footer_pagination.test.tsx @@ -0,0 +1,88 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import {FooterPagination} from './'; + +describe('components/GenericModal/FooterPagination', () => { + const baseProps = { + page: 0, + total: 0, + itemsPerPage: 0, + onNextPage: jest.fn(), + onPreviousPage: jest.fn(), + }; + + test('should render default', () => { + const wrapper = shallow( + , + ); + + expect(wrapper).toMatchSnapshot(); + }); + + test('should render pagination legend', () => { + const wrapper = shallow( + , + ); + + const legend = wrapper.find('.footer-pagination__legend'); + + expect(legend.length).toEqual(1); + expect(legend.at(0).text()).toEqual('Showing 1-10 of 17'); + }); + + test('should render pagination buttons', () => { + const wrapper = shallow( + , + ); + + const buttons = wrapper.find('.footer-pagination__button-container__button'); + + expect(buttons.length).toEqual(2); + expect(buttons.at(0).text()).toEqual('Previous'); + expect(buttons.at(1).text()).toEqual('Next'); + }); + + test('should handle pagination buttons', async () => { + const onPreviousPage = jest.fn(); + const onNextPage = jest.fn(); + + const wrapper = shallow( + , + ); + + const buttons = wrapper.find('.footer-pagination__button-container__button'); + const prevButton = buttons.at(0); + const nextButton = buttons.at(1); + + expect(prevButton.hasClass('disabled')).toBeFalsy(); + expect(nextButton.hasClass('disabled')).toBeFalsy(); + + nextButton.simulate('click'); + + expect(onNextPage).toHaveBeenCalledTimes(1); + + prevButton.simulate('click'); + + expect(onPreviousPage).toHaveBeenCalledTimes(1); + }); +}); diff --git a/webapp/platform/components/src/generic_modal/footer_content/footer_pagination.tsx b/webapp/platform/components/src/generic_modal/footer_content/footer_pagination.tsx new file mode 100644 index 0000000000..3efd3c7a75 --- /dev/null +++ b/webapp/platform/components/src/generic_modal/footer_content/footer_pagination.tsx @@ -0,0 +1,93 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import classNames from 'classnames'; +import React from 'react'; +import {useIntl} from 'react-intl'; + +import {ChevronLeftIcon, ChevronRightIcon} from '@mattermost/compass-icons/components'; + +import './footer_pagination.scss'; + +const BUTTON_ICON_SIZE = 16; + +type FooterPaginationProps = { + page: number; + total: number; + itemsPerPage: number; + onNextPage: (event: React.MouseEvent) => void; + onPreviousPage: (event: React.MouseEvent) => void; +}; + +export const FooterPagination = ({ + page, + total, + itemsPerPage, + onNextPage, + onPreviousPage, +}: FooterPaginationProps) => { + const {formatMessage} = useIntl(); + + const startCount = page * itemsPerPage; + const endCount = Math.min(startCount + itemsPerPage, total); + const totalPages = Math.trunc((total - 1) / itemsPerPage); + + const prevDisabled = page <= 0; + const nextDisabled = page >= totalPages; + + return ( +
    +
    + {Boolean(total) && ( + formatMessage( + { + id: 'footer_pagination.count', + defaultMessage: 'Showing {startCount, number}-{endCount, number} of {total, number}', + }, + { + startCount: startCount + 1, + endCount, + total, + }, + ) + )} +
    +
    + + +
    +
    + ); +}; diff --git a/webapp/platform/components/src/generic_modal/footer_content/index.ts b/webapp/platform/components/src/generic_modal/footer_content/index.ts new file mode 100644 index 0000000000..6097345e39 --- /dev/null +++ b/webapp/platform/components/src/generic_modal/footer_content/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export * from './footer_pagination'; diff --git a/webapp/platform/components/src/generic_modal/generic_modal.tsx b/webapp/platform/components/src/generic_modal/generic_modal.tsx index 9f5ea7a357..68272eb8f4 100644 --- a/webapp/platform/components/src/generic_modal/generic_modal.tsx +++ b/webapp/platform/components/src/generic_modal/generic_modal.tsx @@ -34,13 +34,17 @@ export type Props = { enforceFocus?: boolean; container?: React.ReactNode | React.ReactNodeArray; ariaLabel?: string; - errorText?: string; + errorText?: string | React.ReactNode; compassDesign?: boolean; backdrop?: boolean; backdropClassName?: string; tabIndex?: number; children: React.ReactNode; keyboardEscape?: boolean; + headerInput?: React.ReactNode; + bodyPadding?: boolean; + footerContent?: React.ReactNode; + footerDivider?: boolean; }; type State = { @@ -56,6 +60,7 @@ export class GenericModal extends React.PureComponent { autoCloseOnConfirmButton: true, enforceFocus: true, keyboardEscape: true, + bodyPadding: true, }; constructor(props: Props) { @@ -195,7 +200,12 @@ export class GenericModal extends React.PureComponent { className='GenericModal__wrapper-enter-key-press-catcher' > - {this.props.compassDesign && headerText} + {this.props.compassDesign && ( + <> + {headerText} + {this.props.headerInput} + + )} {this.props.compassDesign ? ( @@ -208,14 +218,22 @@ export class GenericModal extends React.PureComponent { ) : ( headerText )} -
    +
    {this.props.children}
    - {(cancelButton || confirmButton) && - {cancelButton} - {confirmButton} - } + {(cancelButton || confirmButton || this.props.footerContent) && ( + + {(cancelButton || confirmButton) ? ( + <> + {cancelButton} + {confirmButton} + + ) : ( + this.props.footerContent + )} + + )}
    diff --git a/webapp/platform/components/src/index.tsx b/webapp/platform/components/src/index.tsx index f1615adf19..2ead4f1994 100644 --- a/webapp/platform/components/src/index.tsx +++ b/webapp/platform/components/src/index.tsx @@ -7,6 +7,7 @@ export type {CircleSkeletonLoaderProps, RectangleSkeletonLoaderProps} from './sk export type {Props as FocusTrapProps} from './focus_trap'; // components +export * from './generic_modal/footer_content'; export {GenericModal} from './generic_modal/generic_modal'; export {CircleSkeletonLoader, RectangleSkeletonLoader} from './skeleton_loader'; export * from './tour_tip'; diff --git a/webapp/platform/types/src/cloud.ts b/webapp/platform/types/src/cloud.ts index 36a586f7be..74b7195ab2 100644 --- a/webapp/platform/types/src/cloud.ts +++ b/webapp/platform/types/src/cloud.ts @@ -220,6 +220,11 @@ export interface CreateSubscriptionRequest { internal_purchase_order?: string; } +export interface NewsletterRequestBody { + email: string; + subscribed_content: string; +} + export const areShippingDetailsValid = (address: Address | null | undefined): boolean => { if (!address) { return false;