Add mentions feature to objects in notes (#16373)
Issue #15755 ### Context As shown in the issue, users should be able to @ objects and have it be linked to their associated page. ### Changes Made - Updated the BlockNote schema to include the newly created MentionInlineContent that displays an object using a RecordChip - Added the UI for the menu that appears to select the requested object (similar to this example: https://www.blocknotejs.org/examples/ui-components/suggestion-menus-slash-menu-component) - Added a hook to retrieve the data for the UI - Added the @ trigger to the block editor ### Result https://github.com/user-attachments/assets/87dae6a2-ad7a-4642-80b2-8b1751feae24 --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+97
@@ -0,0 +1,97 @@
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { createReactInlineContentSpec } from '@blocknote/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Chip, ChipVariant } from 'twenty-ui/components';
|
||||
|
||||
const StyledRecordChip = styled(RecordChip)`
|
||||
height: auto;
|
||||
margin: 0;
|
||||
padding: ${({ theme }) => `0 ${theme.spacing(1)}`};
|
||||
`;
|
||||
|
||||
const MentionInlineContentRenderer = ({
|
||||
recordId,
|
||||
objectMetadataId,
|
||||
}: {
|
||||
recordId: string;
|
||||
objectMetadataId: string;
|
||||
}) => {
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === objectMetadataId,
|
||||
);
|
||||
|
||||
const objectNameSingular = objectMetadataItem?.nameSingular;
|
||||
|
||||
const { record, loading } = useFindOneRecord({
|
||||
objectNameSingular: objectNameSingular ?? '',
|
||||
objectRecordId: recordId,
|
||||
skip: !isNonEmptyString(objectNameSingular) || !isNonEmptyString(recordId),
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return (
|
||||
<Chip
|
||||
label={t`Unknown object`}
|
||||
variant={ChipVariant.Transparent}
|
||||
disabled
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDefined(record)) {
|
||||
return (
|
||||
<Chip
|
||||
label={t`Deleted record`}
|
||||
variant={ChipVariant.Transparent}
|
||||
disabled
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledRecordChip
|
||||
objectNameSingular={objectMetadataItem.nameSingular}
|
||||
record={record}
|
||||
forceDisableClick={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const MentionInlineContent = createReactInlineContentSpec(
|
||||
{
|
||||
type: 'mention' as const,
|
||||
propSchema: {
|
||||
recordId: {
|
||||
default: '' as const,
|
||||
},
|
||||
objectMetadataId: {
|
||||
default: '' as const,
|
||||
},
|
||||
},
|
||||
content: 'none',
|
||||
},
|
||||
{
|
||||
render: (props) => {
|
||||
const { recordId, objectMetadataId } = props.inlineContent.props;
|
||||
|
||||
return (
|
||||
<MentionInlineContentRenderer
|
||||
recordId={recordId}
|
||||
objectMetadataId={objectMetadataId}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -1,10 +1,19 @@
|
||||
import { BlockNoteSchema, defaultBlockSpecs } from '@blocknote/core';
|
||||
import {
|
||||
BlockNoteSchema,
|
||||
defaultBlockSpecs,
|
||||
defaultInlineContentSpecs,
|
||||
} from '@blocknote/core';
|
||||
|
||||
import { FileBlock } from '@/activities/blocks/components/FileBlock';
|
||||
import { MentionInlineContent } from '@/activities/blocks/components/MentionInlineContent';
|
||||
|
||||
export const BLOCK_SCHEMA = BlockNoteSchema.create({
|
||||
blockSpecs: {
|
||||
...defaultBlockSpecs,
|
||||
file: FileBlock,
|
||||
},
|
||||
inlineContentSpecs: {
|
||||
...defaultInlineContentSpecs,
|
||||
mention: MentionInlineContent,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -37,6 +37,7 @@ import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import '@blocknote/core/fonts/inter.css';
|
||||
import '@blocknote/mantine/style.css';
|
||||
import { useCreateBlockNote } from '@blocknote/react';
|
||||
@@ -223,6 +224,9 @@ export const ActivityRichTextEditor = ({
|
||||
domAttributes: { editor: { class: 'editor' } },
|
||||
schema: BLOCK_SCHEMA,
|
||||
uploadFile: handleEditorBuiltInUploadFile,
|
||||
placeholders: {
|
||||
default: t`Type '/' for commands, '@' for mentions`,
|
||||
},
|
||||
});
|
||||
|
||||
useHotkeysOnFocusedElement({
|
||||
|
||||
Reference in New Issue
Block a user