diff --git a/webapp/channels/src/components/channel_settings_modal/channel_settings_configuration_tab.test.tsx b/webapp/channels/src/components/channel_settings_modal/channel_settings_configuration_tab.test.tsx
index 80d593f7ce..8255b021f9 100644
--- a/webapp/channels/src/components/channel_settings_modal/channel_settings_configuration_tab.test.tsx
+++ b/webapp/channels/src/components/channel_settings_modal/channel_settings_configuration_tab.test.tsx
@@ -434,4 +434,48 @@ describe('ChannelSettingsConfigurationTab', () => {
// Check that the save changes panel is visible
expect(screen.getByRole('button', {name: 'Save'})).toBeInTheDocument();
});
+
+ it('should trim whitespace from banner text and color when saving', async () => {
+ const {patchChannel} = require('mattermost-redux/actions/channels');
+ patchChannel.mockReturnValue({type: 'MOCK_ACTION', data: {}});
+
+ renderWithContext();
+
+ // Add whitespace to the banner text
+ await act(async () => {
+ const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
+ await userEvent.clear(textInput);
+ await userEvent.type(textInput, ' Banner text with whitespace ');
+ });
+
+ // Add whitespace to the banner color
+ await act(async () => {
+ const colorInput = screen.getByTestId('color-inputColorValue');
+ await userEvent.clear(colorInput);
+ await userEvent.type(colorInput, ' #00FF00 ');
+ });
+
+ // Click the Save button
+ await act(async () => {
+ await userEvent.click(screen.getByRole('button', {name: 'Save'}));
+ });
+
+ // Verify patchChannel was called with the trimmed values
+ expect(patchChannel).toHaveBeenCalledWith('channel1', {
+ ...mockChannelWithBanner,
+ banner_info: {
+ enabled: true,
+ text: 'Banner text with whitespace', // Whitespace should be trimmed
+ background_color: expect.any(String), // The exact color might be normalized by the component
+ },
+ });
+
+ // Verify that the local state is updated with trimmed values
+ // Wait for the component to update after the save
+ await new Promise((resolve) => setTimeout(resolve, 0));
+
+ // The text input should now have the trimmed value
+ const textInput = screen.getByTestId('channel_banner_banner_text_textbox');
+ expect(textInput).toHaveValue('Banner text with whitespace');
+ });
});
diff --git a/webapp/channels/src/components/channel_settings_modal/channel_settings_configuration_tab.tsx b/webapp/channels/src/components/channel_settings_modal/channel_settings_configuration_tab.tsx
index e7b35966e9..32e1facc55 100644
--- a/webapp/channels/src/components/channel_settings_modal/channel_settings_configuration_tab.tsx
+++ b/webapp/channels/src/components/channel_settings_modal/channel_settings_configuration_tab.tsx
@@ -75,18 +75,19 @@ function ChannelSettingsConfigurationTab({channel, setAreThereUnsavedChanges, sh
}, []);
const handleTextChange = useCallback((e: React.ChangeEvent) => {
- setUpdatedChannelBanner({
- ...updatedChannelBanner,
- text: e.target.value,
- });
+ const newValue = e.target.value;
+ setUpdatedChannelBanner((prev) => ({
+ ...prev,
+ text: newValue,
+ }));
- if (e.target.value.trim().length > CHANNEL_BANNER_MAX_CHARACTER_LIMIT) {
+ if (newValue.trim().length > CHANNEL_BANNER_MAX_CHARACTER_LIMIT) {
setFormError(formatMessage({
id: 'channel_settings.save_changes_panel.standard_error',
defaultMessage: 'There are errors in the form above',
}));
setCharacterLimitExceeded(true);
- } else if (e.target.value.trim().length <= CHANNEL_BANNER_MIN_CHARACTER_LIMIT) {
+ } else if (newValue.trim().length <= CHANNEL_BANNER_MIN_CHARACTER_LIMIT) {
setFormError(formatMessage({
id: 'channel_settings.save_changes_panel.banner_text.required_error',
defaultMessage: 'Channel banner text cannot be empty when enabled',
@@ -96,24 +97,24 @@ function ChannelSettingsConfigurationTab({channel, setAreThereUnsavedChanges, sh
resetFormErrors();
setCharacterLimitExceeded(false);
}
- }, [formatMessage, resetFormErrors, updatedChannelBanner]);
+ }, [formatMessage, resetFormErrors]);
const handleColorChange = useCallback((color: string) => {
- setUpdatedChannelBanner({
- ...updatedChannelBanner,
+ setUpdatedChannelBanner((prev) => ({
+ ...prev,
background_color: color,
- });
+ }));
- if (color) {
+ if (color.trim()) {
resetFormErrors();
}
- }, [resetFormErrors, updatedChannelBanner]);
+ }, [resetFormErrors]);
const toggleTextPreview = useCallback(() => setShowBannerTextPreview((show) => !show), []);
const hasUnsavedChanges = useCallback(() => {
- return updatedChannelBanner.text !== initialBannerInfo?.text ||
- updatedChannelBanner.background_color !== initialBannerInfo?.background_color ||
+ return (updatedChannelBanner.text?.trim() || '') !== (initialBannerInfo?.text?.trim() || '') ||
+ (updatedChannelBanner.background_color?.trim() || '') !== (initialBannerInfo?.background_color?.trim() || '') ||
updatedChannelBanner.enabled !== initialBannerInfo?.enabled;
}, [initialBannerInfo, updatedChannelBanner]);
@@ -154,8 +155,8 @@ function ChannelSettingsConfigurationTab({channel, setAreThereUnsavedChanges, sh
};
updated.banner_info = {
- text: updatedChannelBanner.text,
- background_color: updatedChannelBanner.background_color,
+ text: updatedChannelBanner.text?.trim() || '',
+ background_color: updatedChannelBanner.background_color?.trim() || '',
enabled: updatedChannelBanner.enabled,
};
@@ -175,6 +176,13 @@ function ChannelSettingsConfigurationTab({channel, setAreThereUnsavedChanges, sh
return;
}
+ // Update local state with trimmed values after successful save
+ setUpdatedChannelBanner((prev) => ({
+ ...prev,
+ text: prev.text?.trim() || '',
+ background_color: prev.background_color?.trim() || '',
+ }));
+
resetFormErrors();
setSaveChangesPanelState('saved');
}, [handleSave, resetFormErrors]);
diff --git a/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.test.tsx b/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.test.tsx
index 2fc8809a40..c0850e65ad 100644
--- a/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.test.tsx
+++ b/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.test.tsx
@@ -213,6 +213,89 @@ describe('ChannelSettingsInfoTab', () => {
});
});
+ it('should trim whitespace from channel fields when saving', async () => {
+ const {patchChannel} = require('mattermost-redux/actions/channels');
+ patchChannel.mockReturnValue({type: 'MOCK_ACTION', data: {}});
+
+ renderWithContext();
+
+ // Add whitespace to the channel fields
+ await act(async () => {
+ // Change the channel name with whitespace
+ const nameInput = screen.getByRole('textbox', {name: 'Channel name'});
+ await userEvent.clear(nameInput);
+ await userEvent.type(nameInput, ' Channel Name With Whitespace ');
+
+ // Change the channel purpose with whitespace
+ const purposeInput = screen.getByTestId('channel_settings_purpose_textbox');
+ await userEvent.clear(purposeInput);
+ await userEvent.type(purposeInput, ' Purpose with whitespace ');
+
+ // Change the channel header with whitespace
+ const headerInput = screen.getByTestId('channel_settings_header_textbox');
+ await userEvent.clear(headerInput);
+ await userEvent.type(headerInput, ' Header with whitespace ');
+ });
+
+ // Add a small delay to ensure all state updates are processed
+ await new Promise((resolve) => setTimeout(resolve, 0));
+
+ // Click the Save button
+ await act(async () => {
+ await userEvent.click(screen.getByRole('button', {name: 'Save'}));
+ });
+
+ // Verify patchChannel was called with the trimmed values
+ expect(patchChannel).toHaveBeenCalledWith('channel1', {
+ ...mockChannel,
+ display_name: 'Channel Name With Whitespace', // Whitespace should be trimmed
+ name: 'channel-name-with-whitespace', // URL is generated from display name and should be trimmed
+ purpose: 'Purpose with whitespace', // Whitespace should be trimmed
+ header: 'Header with whitespace', // Whitespace should be trimmed
+ });
+
+ // Verify that the local state is updated with trimmed values
+ // Wait for the component to update after the save
+ await new Promise((resolve) => setTimeout(resolve, 0));
+
+ // The inputs should now have the trimmed values
+ expect(screen.getByRole('textbox', {name: 'Channel name'})).toHaveValue('Channel Name With Whitespace');
+ expect(screen.getByTestId('channel_settings_purpose_textbox')).toHaveValue('Purpose with whitespace');
+ expect(screen.getByTestId('channel_settings_header_textbox')).toHaveValue('Header with whitespace');
+ });
+
+ it('should hide SaveChangesPanel after successful save', async () => {
+ // Mock the patchChannel function to return a successful response
+ const {patchChannel} = require('mattermost-redux/actions/channels');
+ patchChannel.mockReturnValue({type: 'MOCK_ACTION', data: {}});
+
+ renderWithContext();
+
+ // Initially, SaveChangesPanel should not be visible
+ expect(screen.queryByRole('button', {name: 'Save'})).not.toBeInTheDocument();
+
+ // Make changes to the channel name
+ await act(async () => {
+ const nameInput = screen.getByRole('textbox', {name: 'Channel name'});
+ await userEvent.clear(nameInput);
+ await userEvent.type(nameInput, 'Updated Channel Name');
+ });
+
+ // SaveChangesPanel should now be visible
+ expect(screen.getByRole('button', {name: 'Save'})).toBeInTheDocument();
+
+ // Click the Save button
+ await act(async () => {
+ await userEvent.click(screen.getByRole('button', {name: 'Save'}));
+ });
+
+ // Add a small delay to ensure all state updates are processed
+ await new Promise((resolve) => setTimeout(resolve, 0));
+
+ // SaveChangesPanel should now be hidden after the successful save
+ expect(screen.queryByRole('button', {name: 'Save'})).not.toBeInTheDocument();
+ });
+
it('should reset form when Reset button is clicked', async () => {
renderWithContext();
diff --git a/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.tsx b/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.tsx
index ba4bc92115..0f30db0922 100644
--- a/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.tsx
+++ b/webapp/channels/src/components/channel_settings_modal/channel_settings_info_tab.tsx
@@ -106,10 +106,10 @@ function ChannelSettingsInfoTab({
useEffect(() => {
// Calculate unsaved changes directly
const unsavedChanges = channel ? (
- displayName !== channel.display_name ||
- channelUrl !== channel.name ||
- channelPurpose !== channel.purpose ||
- channelHeader !== channel.header ||
+ displayName.trim() !== channel.display_name ||
+ channelUrl.trim() !== channel.name ||
+ channelPurpose.trim() !== channel.purpose ||
+ channelHeader.trim() !== channel.header ||
channelType !== channel.type
) : false;
@@ -122,7 +122,7 @@ function ChannelSettingsInfoTab({
setSaveChangesPanelState(undefined);
setUrlError('');
}
- setChannelURL(newURL);
+ setChannelURL(newURL.trim());
}, [internalUrlError]);
const togglePurposePreview = useCallback(() => {
@@ -155,7 +155,7 @@ function ChannelSettingsInfoTab({
setChannelHeader(newValue);
// Check for character limit
- if (newValue.length > HEADER_MAX_LENGTH) {
+ if (newValue.trim().length > HEADER_MAX_LENGTH) {
setFormError(formatMessage({
id: 'edit_channel_header_modal.error',
defaultMessage: 'The text entered exceeds the character limit. The channel header is limited to {maxLength} characters.',
@@ -176,7 +176,7 @@ function ChannelSettingsInfoTab({
setChannelPurpose(newValue);
// Check for character limit
- if (newValue.length > Constants.MAX_CHANNELPURPOSE_LENGTH) {
+ if (newValue.trim().length > Constants.MAX_CHANNELPURPOSE_LENGTH) {
setFormError(formatMessage({
id: 'channel_settings.error_purpose_length',
defaultMessage: 'The text entered exceeds the character limit. The channel purpose is limited to {maxLength} characters.',
@@ -243,6 +243,12 @@ function ChannelSettingsInfoTab({
return false;
}
+ // After every successful save, update local state to match the saved values
+ // with this, we make sure that the unsavedChanges check will return false after saving
+ setDisplayName(updated.display_name);
+ setChannelURL(updated.name);
+ setChannelPurpose(updated.purpose);
+ setChannelHeader(updated.header);
return true;
}, [channel, displayName, channelUrl, channelPurpose, channelHeader, channelType, setFormError, handleServerError]);
@@ -308,10 +314,10 @@ function ChannelSettingsInfoTab({
// Memoize the calculation for whether to show the save changes panel
const shouldShowPanel = useMemo(() => {
const unsavedChanges = channel ? (
- displayName !== channel.display_name ||
- channelUrl !== channel.name ||
- channelPurpose !== channel.purpose ||
- channelHeader !== channel.header ||
+ displayName.trim() !== channel.display_name ||
+ channelUrl.trim() !== channel.name ||
+ channelPurpose.trim() !== channel.purpose ||
+ channelHeader.trim() !== channel.header ||
channelType !== channel.type
) : false;
@@ -445,7 +451,7 @@ function ChannelSettingsInfoTab({
/>
{/* SaveChangesPanel for unsaved changes */}
- {shouldShowPanel && (
+ {(canManageChannelProperties && shouldShowPanel) && (