MM-63966 - save pannel not dissapearing after save (#30993)

* MM-63966 - save pannel not dissapearing after save

* adjust unit tests

* trim only on direct change

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Pablo Vélez
2025-05-19 12:26:00 +02:00
коммит произвёл GitHub
родитель 15df76600b
Коммит 04ec01d312
4 изменённых файлов: 169 добавлений и 28 удалений

Просмотреть файл

@@ -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(<ChannelSettingsConfigurationTab {...{...baseProps, channel: mockChannelWithBanner}}/>);
// 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');
});
});

Просмотреть файл

@@ -75,18 +75,19 @@ function ChannelSettingsConfigurationTab({channel, setAreThereUnsavedChanges, sh
}, []);
const handleTextChange = useCallback((e: React.ChangeEvent<TextboxElement>) => {
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]);

Просмотреть файл

@@ -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(<ChannelSettingsInfoTab {...baseProps}/>);
// 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(<ChannelSettingsInfoTab {...baseProps}/>);
// 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(<ChannelSettingsInfoTab {...baseProps}/>);

Просмотреть файл

@@ -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) && (
<SaveChangesPanel
handleSubmit={handleSaveChanges}
handleCancel={handleCancel}