Displayed error message for already used channel name (#24860)

* WIP

* Displayed existing channel name error

* refactoring
This commit is contained in:
Harshil Sharma 2023-10-10 17:41:49 +05:30 committed by GitHub
parent f18002a8d6
commit 06765b1f44
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 72 additions and 8 deletions

View file

@ -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

View file

@ -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}
/>
</React.Fragment>

View file

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

View file

@ -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<string>('');
const channelURL = useRef<string>('');
const [urlError, setURLError] = useState('');
const handleChannelURLChange = useCallback((newURL: string) => {
channelURL.current = newURL;
setURLError('');
}, []);
const [channelMemberNames, setChannelMemberNames] = useState<string[]>([]);
@ -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<ComponentProps<typeof GenericModal>> = {};
let modalBody;
@ -171,6 +188,7 @@ const ConvertGmToChannelModal = (props: Props) => {
onURLChange={handleChannelURLChange}
onErrorStateChange={setNameError}
team={selectedTeamId ? commonTeamsById[selectedTeamId] : undefined}
urlError={urlError}
/>
{

View file

@ -81,7 +81,3 @@
font-weight: 500;
}
}
.new-channel-modal__url {
margin-top: 4px;
}

View file

@ -83,6 +83,11 @@ const NewChannelModal = () => {
const [canCreateFromPluggable, setCanCreateFromPluggable] = useState(true);
const [actionFromPluggable, setActionFromPluggable] = useState<((currentTeamId: string, channelId: string) => Promise<Board>) | 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}
/>
<PublicPrivateSelector
className='new-channel-modal-type-selector'

View file

@ -11,6 +11,7 @@
display: flex;
min-height: 32px;
align-items: center;
margin-top: 4px;
.url-input-label {
flex: none;