feat: Add `link` action to composer toolbar (#31679)

Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com>
pull/31901/head
gabriellsh 2 years ago committed by GitHub
parent 43d1b0f835
commit 939a6fa35f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      .changeset/khaki-oranges-wink.md
  2. 65
      apps/meteor/app/ui-message/client/messageBox/AddLinkComposerActionModal.tsx
  3. 64
      apps/meteor/app/ui-message/client/messageBox/messageBoxFormatting.ts
  4. 5
      apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/FormattingToolbarDropdown.tsx
  5. 9
      apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/MessageBoxFormattingToolbar.tsx
  6. 22
      apps/meteor/tests/e2e/message-composer.spec.ts
  7. 2
      packages/i18n/src/locales/en.i18n.json

@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/i18n": minor
---
Added a new formatter shortcut to add hyperlinks to a message

@ -0,0 +1,65 @@
import { Field, FieldGroup, TextInput, FieldLabel, FieldRow, Box } from '@rocket.chat/fuselage';
import { useUniqueId } from '@rocket.chat/fuselage-hooks';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React, { useEffect } from 'react';
import { useForm, Controller } from 'react-hook-form';
import GenericModal from '../../../../client/components/GenericModal';
type AddLinkComposerActionModalProps = {
selectedText?: string;
onConfirm: (url: string, text: string) => void;
onClose: () => void;
};
const AddLinkComposerActionModal = ({ selectedText, onClose, onConfirm }: AddLinkComposerActionModalProps) => {
const t = useTranslation();
const textField = useUniqueId();
const urlField = useUniqueId();
const { handleSubmit, setFocus, control } = useForm({
mode: 'onBlur',
defaultValues: {
text: selectedText || '',
url: '',
},
});
useEffect(() => {
setFocus(selectedText ? 'url' : 'text');
}, [selectedText, setFocus]);
const onClickConfirm = ({ url, text }: { url: string; text: string }) => {
onConfirm(url, text);
};
const submit = handleSubmit(onClickConfirm);
return (
<GenericModal
variant='warning'
icon={null}
confirmText={t('Add')}
onCancel={onClose}
wrapperFunction={(props) => <Box is='form' onSubmit={(e) => void submit(e)} {...props} />}
title={t('Add_link')}
>
<FieldGroup>
<Field>
<FieldLabel htmlFor={textField}>{t('Text')}</FieldLabel>
<FieldRow>
<Controller control={control} name='text' render={({ field }) => <TextInput autoComplete='off' id={textField} {...field} />} />
</FieldRow>
</Field>
<Field>
<FieldLabel htmlFor={urlField}>{t('URL')}</FieldLabel>
<FieldRow>
<Controller control={control} name='url' render={({ field }) => <TextInput autoComplete='off' id={urlField} {...field} />} />
</FieldRow>
</Field>
</FieldGroup>
</GenericModal>
);
};
export default AddLinkComposerActionModal;

@ -1,24 +1,34 @@
import type { Keys as IconName } from '@rocket.chat/icons';
import type { TranslationKey } from '@rocket.chat/ui-contexts';
import type { ComposerAPI } from '../../../../client/lib/chats/ChatAPI';
import { imperativeModal } from '../../../../client/lib/imperativeModal';
import { settings } from '../../../settings/client';
import AddLinkComposerActionModal from './AddLinkComposerActionModal';
export type FormattingButton =
| {
label: TranslationKey;
icon: IconName;
pattern: string;
// text?: () => string | undefined;
command?: string;
link?: string;
condition?: () => boolean;
}
| {
label: TranslationKey;
text: () => string | undefined;
link: string;
condition?: () => boolean;
};
type FormattingButtonDefault = { label: TranslationKey; condition?: () => boolean };
type TextButton = {
text: () => string | undefined;
link: string;
} & FormattingButtonDefault;
type PatternButton = {
icon: IconName;
pattern: string;
// text?: () => string | undefined;
command?: string;
link?: string;
} & FormattingButtonDefault;
type PromptButton = {
prompt: (composer: ComposerAPI) => void;
icon: IconName;
} & FormattingButtonDefault;
export type FormattingButton = PatternButton | PromptButton | TextButton;
export const isPromptButton = (button: FormattingButton): button is PromptButton => 'prompt' in button;
export const formattingButtons: ReadonlyArray<FormattingButton> = [
{
@ -48,6 +58,28 @@ export const formattingButtons: ReadonlyArray<FormattingButton> = [
icon: 'multiline',
pattern: '```\n{{text}}\n``` ',
},
{
label: 'Link',
icon: 'link',
prompt: (composerApi: ComposerAPI) => {
const { selection } = composerApi;
const selectedText = composerApi.substring(selection.start, selection.end);
const onClose = () => {
imperativeModal.close();
composerApi.focus();
};
const onConfirm = (url: string, text: string) => {
onClose();
composerApi.replaceText(`[${text}](${url})`, selection);
composerApi.setCursorToEnd();
};
imperativeModal.open({ component: AddLinkComposerActionModal, props: { onConfirm, selectedText, onClose } });
},
},
{
label: 'KaTeX' as TranslationKey,
icon: 'katex',

@ -1,7 +1,7 @@
import { useTranslation } from '@rocket.chat/ui-contexts';
import React from 'react';
import type { FormattingButton } from '../../../../../../app/ui-message/client/messageBox/messageBoxFormatting';
import { isPromptButton, type FormattingButton } from '../../../../../../app/ui-message/client/messageBox/messageBoxFormatting';
import GenericMenu from '../../../../../components/GenericMenu/GenericMenu';
import type { GenericMenuItemProps } from '../../../../../components/GenericMenu/GenericMenuItem';
import type { ComposerAPI } from '../../../../../lib/chats/ChatAPI';
@ -21,6 +21,9 @@ const FormattingToolbarDropdown = ({ composer, items, disabled }: FormattingTool
window.open(formatter.link, '_blank', 'rel=noreferrer noopener');
return;
}
if (isPromptButton(formatter)) {
return formatter.prompt(composer);
}
composer.wrapSelection(formatter.pattern);
};

@ -3,6 +3,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts';
import React, { memo } from 'react';
import type { FormattingButton } from '../../../../../../app/ui-message/client/messageBox/messageBoxFormatting';
import { isPromptButton } from '../../../../../../app/ui-message/client/messageBox/messageBoxFormatting';
import type { ComposerAPI } from '../../../../../lib/chats/ChatAPI';
import FormattingToolbarDropdown from './FormattingToolbarDropdown';
@ -24,7 +25,9 @@ const MessageBoxFormattingToolbar = ({ items, variant = 'large', composer, disab
<>
{'icon' in featuredFormatter && (
<MessageComposerAction
onClick={() => composer.wrapSelection(featuredFormatter.pattern)}
onClick={() =>
isPromptButton(featuredFormatter) ? featuredFormatter.prompt(composer) : composer.wrapSelection(featuredFormatter.pattern)
}
icon={featuredFormatter.icon}
disabled={disabled}
/>
@ -45,6 +48,10 @@ const MessageBoxFormattingToolbar = ({ items, variant = 'large', composer, disab
data-id={formatter.label}
title={t(formatter.label)}
onClick={(): void => {
if (isPromptButton(formatter)) {
formatter.prompt(composer);
return;
}
if ('link' in formatter) {
window.open(formatter.link, '_blank', 'rel=noreferrer noopener');
return;

@ -1,3 +1,5 @@
import { faker } from '@faker-js/faker';
import { Users } from './fixtures/userStates';
import { HomeChannel } from './page-objects';
import { createTargetChannel } from './utils';
@ -23,21 +25,18 @@ test.describe.serial('message-composer', () => {
await poHomeChannel.sidenav.openChat(targetChannel);
await poHomeChannel.content.sendMessage('hello composer');
await expect(poHomeChannel.composerToolbarActions).toHaveCount(11);
await expect(poHomeChannel.composerToolbarActions).toHaveCount(12);
});
test('should have only the main formatter and the main action', async ({ page }) => {
await page.setViewportSize({ width: 768, height: 600 });
await poHomeChannel.sidenav.openChat(targetChannel);
await poHomeChannel.content.sendMessage('hello composer');
await expect(poHomeChannel.composerToolbarActions).toHaveCount(5);
});
test('should navigate on toolbar using arrow keys', async ({ page }) => {
await poHomeChannel.sidenav.openChat(targetChannel);
await poHomeChannel.content.sendMessage('hello composer');
await page.keyboard.press('Tab');
await page.keyboard.press('ArrowRight');
@ -50,11 +49,24 @@ test.describe.serial('message-composer', () => {
test('should move the focus away from toolbar using tab key', async ({ page }) => {
await poHomeChannel.sidenav.openChat(targetChannel);
await poHomeChannel.content.sendMessage('hello composer');
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
await expect(poHomeChannel.composerToolbar.getByRole('button', { name: 'Emoji' })).not.toBeFocused();
});
test('should add a link to the selected text', async ({ page }) => {
const url = faker.internet.url();
await poHomeChannel.sidenav.openChat(targetChannel);
await page.keyboard.type('hello composer');
await page.keyboard.press('Control+A'); // on Windows and Linux
await page.keyboard.press('Meta+A'); // on macOS
await poHomeChannel.composerToolbar.getByRole('button', { name: 'Link' }).click()
await page.keyboard.type(url);
await page.keyboard.press('Enter');
await expect(poHomeChannel.composer).toHaveValue(`[hello composer](${url})`);
});
});

@ -312,6 +312,7 @@
"Add_files_from": "Add files from",
"Add_manager": "Add manager",
"Add_monitor": "Add monitor",
"Add_link": "Add link",
"Add_Reaction": "Add reaction",
"Add_Role": "Add Role",
"Add_Sender_To_ReplyTo": "Add Sender to Reply-To",
@ -5108,6 +5109,7 @@
"test-push-notifications": "Test push notifications",
"test-push-notifications_description": "Permission to test push notifications",
"Texts": "Texts",
"Text": "Text",
"Thank_you_for_your_feedback": "Thank you for your feedback",
"The_application_name_is_required": "The application name is required",
"The_application_will_be_able_to": "<1>{{appName}}</1> will be able to:",

Loading…
Cancel
Save