From 06765b1f44b8317d6d012b8faadf7fbaed85f9fb Mon Sep 17 00:00:00 2001
From: Harshil Sharma <18575143+harshilsharma63@users.noreply.github.com>
Date: Tue, 10 Oct 2023 17:41:49 +0530
Subject: [PATCH] Displayed error message for already used channel name
(#24860)
* WIP
* Displayed existing channel name error
* refactoring
---
server/channels/app/channel.go | 3 ++
.../channel_name_form_field.tsx | 3 +-
.../convert_gm_to_channel_modal.test.tsx | 39 +++++++++++++++++++
.../convert_gm_to_channel_modal.tsx | 22 ++++++++++-
.../new_channel_modal/new_channel_modal.scss | 4 --
.../new_channel_modal/new_channel_modal.tsx | 8 +++-
.../widgets/inputs/url_input/url_input.scss | 1 +
7 files changed, 72 insertions(+), 8 deletions(-)
diff --git a/server/channels/app/channel.go b/server/channels/app/channel.go
index 2a5b05bae8d..15b6a5033e9 100644
--- a/server/channels/app/channel.go
+++ b/server/channels/app/channel.go
@@ -697,6 +697,9 @@ func (a *App) UpdateChannel(c request.CTX, channel *model.Channel) (*model.Chann
var invErr *store.ErrInvalidInput
switch {
case errors.As(err, &invErr):
+ if invErr.Entity == "Channel" && invErr.Field == "Name" {
+ return nil, model.NewAppError("UpdateChannel", store.ChannelExistsError, nil, "", http.StatusBadRequest).Wrap(err)
+ }
return nil, model.NewAppError("UpdateChannel", "app.channel.update.bad_id", nil, "", http.StatusBadRequest).Wrap(err)
case errors.As(err, &appErr):
return nil, appErr
diff --git a/webapp/channels/src/components/channel_name_form_field/channel_name_form_field.tsx b/webapp/channels/src/components/channel_name_form_field/channel_name_form_field.tsx
index 508dd40f071..ae9d9f7fe9b 100644
--- a/webapp/channels/src/components/channel_name_form_field/channel_name_form_field.tsx
+++ b/webapp/channels/src/components/channel_name_form_field/channel_name_form_field.tsx
@@ -26,6 +26,7 @@ export type Props = {
autoFocus?: boolean;
onErrorStateChange?: (isError: boolean) => void;
team?: Team;
+ urlError?: string;
}
import './channel_name_form_field.scss';
@@ -139,7 +140,7 @@ const ChannelNameFormField = (props: Props): JSX.Element => {
pathInfo={url}
limit={Constants.MAX_CHANNELNAME_LENGTH}
shortenLength={Constants.DEFAULT_CHANNELURL_SHORTEN_LENGTH}
- error={urlError}
+ error={urlError || props.urlError}
onChange={handleOnURLChange}
/>
diff --git a/webapp/channels/src/components/convert_gm_to_channel_modal/convert_gm_to_channel_modal.test.tsx b/webapp/channels/src/components/convert_gm_to_channel_modal/convert_gm_to_channel_modal.test.tsx
index 4f4d6809098..024452c0667 100644
--- a/webapp/channels/src/components/convert_gm_to_channel_modal/convert_gm_to_channel_modal.test.tsx
+++ b/webapp/channels/src/components/convert_gm_to_channel_modal/convert_gm_to_channel_modal.test.tsx
@@ -175,4 +175,43 @@ describe('component/ConvertGmToChannelModal', () => {
fireEvent.click(confirmButton!);
});
});
+
+ test('duplicate channel names should npt be allowed', async () => {
+ TestHelper.initBasic(Client4);
+
+ nock(Client4.getBaseRoute()).
+ get('/channels/channel_id_1/common_teams').
+ reply(200, [
+ {id: 'team_id_1', display_name: 'Team 1', name: 'team_1'},
+ ]);
+
+ baseProps.actions.convertGroupMessageToPrivateChannel.mockResolvedValueOnce({
+ error: {
+ server_error_id: 'store.sql_channel.save_channel.exists.app_error',
+ },
+ });
+
+ renderWithFullContext(
+ ,
+ baseState,
+ );
+
+ await waitFor(
+ () => expect(screen.queryByText('Conversation history will be visible to any channel members')).toBeInTheDocument(),
+ {timeout: 1500},
+ );
+
+ const channelNameInput = screen.queryByPlaceholderText('Channel name');
+ expect(channelNameInput).toBeInTheDocument();
+ fireEvent.change(channelNameInput!, {target: {value: 'Channel'}});
+
+ const confirmButton = screen.queryByText('Convert to private channel');
+ expect(channelNameInput).toBeInTheDocument();
+
+ await act(async () => {
+ fireEvent.click(confirmButton!);
+ });
+
+ expect(screen.queryByText('A channel with that URL already exists')).toBeInTheDocument();
+ });
});
diff --git a/webapp/channels/src/components/convert_gm_to_channel_modal/convert_gm_to_channel_modal.tsx b/webapp/channels/src/components/convert_gm_to_channel_modal/convert_gm_to_channel_modal.tsx
index 8a53832e4e9..774c7465133 100644
--- a/webapp/channels/src/components/convert_gm_to_channel_modal/convert_gm_to_channel_modal.tsx
+++ b/webapp/channels/src/components/convert_gm_to_channel_modal/convert_gm_to_channel_modal.tsx
@@ -28,6 +28,10 @@ import TeamSelector from 'components/convert_gm_to_channel_modal/team_selector/t
import WarningTextSection from 'components/convert_gm_to_channel_modal/warning_text_section/warning_text_section';
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
+const enum ServerErrorId {
+ CHANNEL_NAME_EXISTS = 'store.sql_channel.save_channel.exists.app_error',
+}
+
export type Props = {
onExited: () => void;
channel: Channel;
@@ -43,8 +47,11 @@ const ConvertGmToChannelModal = (props: Props) => {
const [channelName, setChannelName] = useState('');
const channelURL = useRef('');
+
+ const [urlError, setURLError] = useState('');
const handleChannelURLChange = useCallback((newURL: string) => {
channelURL.current = newURL;
+ setURLError('');
}, []);
const [channelMemberNames, setChannelMemberNames] = useState([]);
@@ -112,7 +119,17 @@ const ConvertGmToChannelModal = (props: Props) => {
const {error} = await props.actions.convertGroupMessageToPrivateChannel(props.channel.id, selectedTeamId, channelName.trim(), channelURL.current.trim());
if (error) {
- setConversionError(error.message);
+ if (error.server_error_id === ServerErrorId.CHANNEL_NAME_EXISTS) {
+ setURLError(
+ formatMessage({
+ id: 'channel_modal.alreadyExist',
+ defaultMessage: 'A channel with that URL already exists',
+ }),
+ );
+ } else {
+ setConversionError(error.message);
+ }
+
return;
}
@@ -122,7 +139,7 @@ const ConvertGmToChannelModal = (props: Props) => {
}, [selectedTeamId, props.channel.id, channelName, channelURL.current, props.actions.moveChannelsInSidebar]);
const showLoader = !commonTeamsFetched || !loadingAnimationTimeout;
- const canCreate = selectedTeamId !== undefined && channelName !== '' && !nameError;
+ const canCreate = selectedTeamId !== undefined && channelName !== '' && !nameError && !urlError;
const modalProps: Partial> = {};
let modalBody;
@@ -171,6 +188,7 @@ const ConvertGmToChannelModal = (props: Props) => {
onURLChange={handleChannelURLChange}
onErrorStateChange={setNameError}
team={selectedTeamId ? commonTeamsById[selectedTeamId] : undefined}
+ urlError={urlError}
/>
{
diff --git a/webapp/channels/src/components/new_channel_modal/new_channel_modal.scss b/webapp/channels/src/components/new_channel_modal/new_channel_modal.scss
index bbc08073272..2f276fe004d 100644
--- a/webapp/channels/src/components/new_channel_modal/new_channel_modal.scss
+++ b/webapp/channels/src/components/new_channel_modal/new_channel_modal.scss
@@ -81,7 +81,3 @@
font-weight: 500;
}
}
-
-.new-channel-modal__url {
- margin-top: 4px;
-}
diff --git a/webapp/channels/src/components/new_channel_modal/new_channel_modal.tsx b/webapp/channels/src/components/new_channel_modal/new_channel_modal.tsx
index 1e93b35662b..0b5966943d1 100644
--- a/webapp/channels/src/components/new_channel_modal/new_channel_modal.tsx
+++ b/webapp/channels/src/components/new_channel_modal/new_channel_modal.tsx
@@ -83,6 +83,11 @@ const NewChannelModal = () => {
const [canCreateFromPluggable, setCanCreateFromPluggable] = useState(true);
const [actionFromPluggable, setActionFromPluggable] = useState<((currentTeamId: string, channelId: string) => Promise) | undefined>(undefined);
+ const handleURLChange = useCallback((newURL: string) => {
+ setURL(newURL);
+ setURLError('');
+ }, []);
+
const handleOnModalConfirm = async () => {
if (!canCreate) {
return;
@@ -265,8 +270,9 @@ const NewChannelModal = () => {
name='new-channel-modal-name'
placeholder={formatMessage({id: 'channel_modal.name.placeholder', defaultMessage: 'Enter a name for your new channel'})}
onDisplayNameChange={setDisplayName}
- onURLChange={setURL}
+ onURLChange={handleURLChange}
onErrorStateChange={setChannelInputError}
+ urlError={urlError}
/>