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>
Этот коммит содержится в:
@@ -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}
|
||||
|
||||
Ссылка в новой задаче
Block a user