diff --git a/packages/twenty-front/src/modules/activities/blocks/components/MentionInlineContent.tsx b/packages/twenty-front/src/modules/activities/blocks/components/MentionInlineContent.tsx
new file mode 100644
index 00000000000..dff8a6be18f
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/blocks/components/MentionInlineContent.tsx
@@ -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 (
+
+ );
+ }
+
+ if (!isDefined(record)) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+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 (
+
+ );
+ },
+ },
+);
diff --git a/packages/twenty-front/src/modules/activities/blocks/constants/Schema.ts b/packages/twenty-front/src/modules/activities/blocks/constants/Schema.ts
index 745584f28be..c2eb65b990d 100644
--- a/packages/twenty-front/src/modules/activities/blocks/constants/Schema.ts
+++ b/packages/twenty-front/src/modules/activities/blocks/constants/Schema.ts
@@ -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,
+ },
});
diff --git a/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx
index 7091c248c91..cc2a4530363 100644
--- a/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx
+++ b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx
@@ -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({
diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuOpenContainer.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuOpenContainer.tsx
index 8af08175b34..7d6cda2879f 100644
--- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuOpenContainer.tsx
+++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuOpenContainer.tsx
@@ -5,6 +5,7 @@ import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { isSidePanelAnimatingState } from '@/command-menu/states/isSidePanelAnimatingState';
import { type CommandMenuAnimationVariant } from '@/command-menu/types/CommandMenuAnimationVariant';
import { RECORD_CHIP_CLICK_OUTSIDE_ID } from '@/object-record/record-table/constants/RecordChipClickOutsideId';
+import { MENTION_MENU_DROPDOWN_CLICK_OUTSIDE_ID } from '@/ui/input/constants/MentionMenuDropdownClickOutsideId';
import { SLASH_MENU_DROPDOWN_CLICK_OUTSIDE_ID } from '@/ui/input/constants/SlashMenuDropdownClickOutsideId';
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
import { PAGE_HEADER_COMMAND_MENU_BUTTON_CLICK_OUTSIDE_ID } from '@/ui/layout/page-header/constants/PageHeaderCommandMenuButtonClickOutsideId';
@@ -79,6 +80,7 @@ export const CommandMenuOpenContainer = ({
LINK_CHIP_CLICK_OUTSIDE_ID,
RECORD_CHIP_CLICK_OUTSIDE_ID,
SLASH_MENU_DROPDOWN_CLICK_OUTSIDE_ID,
+ MENTION_MENU_DROPDOWN_CLICK_OUTSIDE_ID,
WORKFLOW_DIAGRAM_STEP_NODE_BASE_CLICK_OUTSIDE_ID,
WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID,
WORKFLOW_DIAGRAM_CREATE_STEP_NODE_CLICK_OUTSIDE_ID,
diff --git a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextEditorContent.tsx b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextEditorContent.tsx
index 3660755caca..2056fc792c4 100644
--- a/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextEditorContent.tsx
+++ b/packages/twenty-front/src/modules/page-layout/widgets/standalone-rich-text/components/StandaloneRichTextEditorContent.tsx
@@ -18,6 +18,7 @@ import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePush
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
+import { t } from '@lingui/core/macro';
import '@blocknote/core/fonts/inter.css';
import '@blocknote/mantine/style.css';
import { useCreateBlockNote } from '@blocknote/react';
@@ -95,6 +96,9 @@ export const StandaloneRichTextEditorContent = ({
schema: BLOCK_SCHEMA,
uploadFile: handleEditorBuiltInUploadFile,
sideMenuDetection: 'editor',
+ placeholders: {
+ default: t`Type '/' for commands, '@' for mentions`,
+ },
});
const handlePersistBody = useDebouncedCallback((blocknote: string) => {
diff --git a/packages/twenty-front/src/modules/ui/input/constants/MentionMenuDropdownClickOutsideId.ts b/packages/twenty-front/src/modules/ui/input/constants/MentionMenuDropdownClickOutsideId.ts
new file mode 100644
index 00000000000..8a94cb4b43a
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/input/constants/MentionMenuDropdownClickOutsideId.ts
@@ -0,0 +1,2 @@
+export const MENTION_MENU_DROPDOWN_CLICK_OUTSIDE_ID =
+ 'mention-menu-dropdown-click-outside-id';
diff --git a/packages/twenty-front/src/modules/ui/input/constants/MentionMenuListId.ts b/packages/twenty-front/src/modules/ui/input/constants/MentionMenuListId.ts
new file mode 100644
index 00000000000..39b97203292
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/input/constants/MentionMenuListId.ts
@@ -0,0 +1 @@
+export const MENTION_MENU_LIST_ID = 'mention-menu-list-id';
diff --git a/packages/twenty-front/src/modules/ui/input/editor/components/BlockEditor.tsx b/packages/twenty-front/src/modules/ui/input/editor/components/BlockEditor.tsx
index 075df001297..ce25728a7f9 100644
--- a/packages/twenty-front/src/modules/ui/input/editor/components/BlockEditor.tsx
+++ b/packages/twenty-front/src/modules/ui/input/editor/components/BlockEditor.tsx
@@ -7,11 +7,13 @@ import { type ClipboardEvent } from 'react';
import { type BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema';
import { getSlashMenu } from '@/activities/blocks/utils/getSlashMenu';
+import { CustomMentionMenu } from '@/ui/input/editor/components/CustomMentionMenu';
import { CustomSideMenu } from '@/ui/input/editor/components/CustomSideMenu';
import {
CustomSlashMenu,
type SuggestionItem,
} from '@/ui/input/editor/components/CustomSlashMenu';
+import { useMentionMenu } from '@/ui/input/editor/hooks/useMentionMenu';
interface BlockEditorProps {
editor: typeof BLOCK_SCHEMA.BlockNoteEditor;
@@ -141,6 +143,7 @@ export const BlockEditor = ({
}: BlockEditorProps) => {
const theme = useTheme();
const blockNoteTheme = theme.name === 'light' ? 'light' : 'dark';
+ const getMentionItems = useMentionMenu(editor);
const handleFocus = () => {
onFocus?.();
@@ -179,6 +182,11 @@ export const BlockEditor = ({
}
suggestionMenuComponent={CustomSlashMenu}
/>
+ getMentionItems(query)}
+ suggestionMenuComponent={CustomMentionMenu}
+ />
);
diff --git a/packages/twenty-front/src/modules/ui/input/editor/components/CustomMentionMenu.tsx b/packages/twenty-front/src/modules/ui/input/editor/components/CustomMentionMenu.tsx
new file mode 100644
index 00000000000..9881797e75b
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/input/editor/components/CustomMentionMenu.tsx
@@ -0,0 +1,101 @@
+import styled from '@emotion/styled';
+import { autoUpdate, useFloating } from '@floating-ui/react';
+import { motion } from 'framer-motion';
+import { type MouseEvent as ReactMouseEvent } from 'react';
+import { createPortal } from 'react-dom';
+
+import { MENTION_MENU_DROPDOWN_CLICK_OUTSIDE_ID } from '@/ui/input/constants/MentionMenuDropdownClickOutsideId';
+import { MENTION_MENU_LIST_ID } from '@/ui/input/constants/MentionMenuListId';
+import { CustomMentionMenuListItem } from '@/ui/input/editor/components/CustomMentionMenuListItem';
+import { CustomMentionMenuSelectedIndexSyncEffect } from '@/ui/input/editor/components/CustomMentionMenuSelectedIndexSyncEffect';
+import {
+ type CustomMentionMenuProps,
+ type MentionItem,
+} from '@/ui/input/editor/components/types';
+import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
+import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
+import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
+import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
+import { isDefined } from 'twenty-shared/utils';
+
+export type { MentionItem };
+
+const MenuPixelWidth = 240;
+
+const StyledContainer = styled.div`
+ height: 1px;
+ width: 1px;
+`;
+
+export const CustomMentionMenu = ({
+ items,
+ selectedIndex,
+ onItemClick,
+}: CustomMentionMenuProps) => {
+ const { refs, floatingStyles } = useFloating({
+ placement: 'bottom-start',
+ whileElementsMounted: autoUpdate,
+ });
+
+ const handleContainerClick = (e: ReactMouseEvent) => {
+ e.stopPropagation();
+ };
+
+ if (!isDefined(items) || items.length === 0) {
+ return null;
+ }
+
+ const filteredItems = items.filter(
+ (item) =>
+ isDefined(item.recordId) &&
+ isDefined(item.objectNameSingular) &&
+ isDefined(item.objectMetadataId),
+ );
+
+ return (
+
+
+ <>
+ {createPortal(
+
+
+
+
+ item.recordId!,
+ )}
+ >
+ {filteredItems.map((item) => (
+ onItemClick?.(item)}
+ objectNameSingular={item.objectNameSingular!}
+ />
+ ))}
+
+
+
+
+ ,
+ document.body,
+ )}
+ >
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/ui/input/editor/components/CustomMentionMenuListItem.tsx b/packages/twenty-front/src/modules/ui/input/editor/components/CustomMentionMenuListItem.tsx
new file mode 100644
index 00000000000..290689c0c9f
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/input/editor/components/CustomMentionMenuListItem.tsx
@@ -0,0 +1,69 @@
+import { type MouseEvent } from 'react';
+
+import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
+import { getAvatarType } from '@/object-metadata/utils/getAvatarType';
+import { searchRecordStoreFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState';
+import { MENTION_MENU_LIST_ID } from '@/ui/input/constants/MentionMenuListId';
+import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
+import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
+import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector';
+import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue';
+import { useRecoilValue } from 'recoil';
+import { isDefined } from 'twenty-shared/utils';
+import { Avatar } from 'twenty-ui/display';
+import { MenuItemSuggestion } from 'twenty-ui/navigation';
+
+type CustomMentionMenuListItemProps = {
+ recordId: string;
+ onClick: () => void;
+ objectNameSingular: string;
+};
+
+export const CustomMentionMenuListItem = ({
+ recordId,
+ onClick,
+ objectNameSingular,
+}: CustomMentionMenuListItemProps) => {
+ const { resetSelectedItem } = useSelectableList(MENTION_MENU_LIST_ID);
+
+ const isSelectedItem = useRecoilComponentFamilyValue(
+ isSelectedItemIdComponentFamilySelector,
+ recordId,
+ );
+
+ const searchRecord = useRecoilValue(searchRecordStoreFamilyState(recordId));
+
+ const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
+
+ const handleClick = (event?: MouseEvent) => {
+ event?.preventDefault();
+ event?.stopPropagation();
+ resetSelectedItem();
+ onClick();
+ };
+
+ if (!isDefined(searchRecord)) {
+ return null;
+ }
+
+ return (
+
+ (
+
+ )}
+ />
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/ui/input/editor/components/CustomMentionMenuSelectedIndexSyncEffect.tsx b/packages/twenty-front/src/modules/ui/input/editor/components/CustomMentionMenuSelectedIndexSyncEffect.tsx
new file mode 100644
index 00000000000..077bfd870df
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/input/editor/components/CustomMentionMenuSelectedIndexSyncEffect.tsx
@@ -0,0 +1,29 @@
+import { MENTION_MENU_LIST_ID } from '@/ui/input/constants/MentionMenuListId';
+import { type MentionItem } from '@/ui/input/editor/components/types';
+import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
+import { useEffect } from 'react';
+import { isDefined } from 'twenty-shared/utils';
+
+type CustomMentionMenuSelectedIndexSyncEffectProps = {
+ items: MentionItem[];
+ selectedIndex: number | undefined;
+};
+
+export const CustomMentionMenuSelectedIndexSyncEffect = ({
+ items,
+ selectedIndex,
+}: CustomMentionMenuSelectedIndexSyncEffectProps) => {
+ const { setSelectedItemId } = useSelectableList(MENTION_MENU_LIST_ID);
+
+ useEffect(() => {
+ if (!isDefined(selectedIndex) || !isDefined(items)) return;
+
+ const selectedItem = items[selectedIndex];
+
+ if (isDefined(selectedItem) && isDefined(selectedItem.recordId)) {
+ setSelectedItemId(selectedItem.recordId);
+ }
+ }, [items, selectedIndex, setSelectedItemId]);
+
+ return <>>;
+};
diff --git a/packages/twenty-front/src/modules/ui/input/editor/components/CustomSlashMenu.tsx b/packages/twenty-front/src/modules/ui/input/editor/components/CustomSlashMenu.tsx
index 093c08f6b40..299a86ce2fa 100644
--- a/packages/twenty-front/src/modules/ui/input/editor/components/CustomSlashMenu.tsx
+++ b/packages/twenty-front/src/modules/ui/input/editor/components/CustomSlashMenu.tsx
@@ -2,12 +2,12 @@ import { useBlockNoteEditor } from '@blocknote/react';
import styled from '@emotion/styled';
import { autoUpdate, flip, offset, useFloating } from '@floating-ui/react';
import { motion } from 'framer-motion';
-import { useEffect } from 'react';
import { createPortal } from 'react-dom';
import { SLASH_MENU_DROPDOWN_CLICK_OUTSIDE_ID } from '@/ui/input/constants/SlashMenuDropdownClickOutsideId';
import { SLASH_MENU_LIST_ID } from '@/ui/input/constants/SlashMenuListId';
import { CustomSlashMenuListItem } from '@/ui/input/editor/components/CustomSlashMenuListItem';
+import { CustomSlashMenuSelectedIndexSyncEffect } from '@/ui/input/editor/components/CustomSlashMenuSelectedIndexSyncEffect';
import type {
CustomSlashMenuProps,
SuggestionItem,
@@ -16,8 +16,6 @@ import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
-import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
-import { isDefined } from 'twenty-shared/utils';
export type { SuggestionItem };
@@ -58,20 +56,12 @@ export const CustomSlashMenu = ({
middleware: [flip(), offset(({ placement }) => getOffsetValue(placement))],
});
- const { setSelectedItemId } = useSelectableList(SLASH_MENU_LIST_ID);
-
- useEffect(() => {
- if (!isDefined(selectedIndex)) return;
-
- const selectedItem = items[selectedIndex];
-
- if (isDefined(selectedItem)) {
- setSelectedItemId(selectedItem.title);
- }
- }, [items, selectedIndex, setSelectedItemId]);
-
return (
+
<>
{createPortal(
{
+ const { setSelectedItemId } = useSelectableList(SLASH_MENU_LIST_ID);
+
+ useEffect(() => {
+ if (!isDefined(selectedIndex)) return;
+
+ const selectedItem = items[selectedIndex];
+
+ if (isDefined(selectedItem)) {
+ setSelectedItemId(selectedItem.title);
+ }
+ }, [items, selectedIndex, setSelectedItemId]);
+
+ return <>>;
+};
diff --git a/packages/twenty-front/src/modules/ui/input/editor/components/types.ts b/packages/twenty-front/src/modules/ui/input/editor/components/types.ts
index 26354233cc0..7d10aa8eef9 100644
--- a/packages/twenty-front/src/modules/ui/input/editor/components/types.ts
+++ b/packages/twenty-front/src/modules/ui/input/editor/components/types.ts
@@ -1,11 +1,20 @@
-import type { SuggestionMenuProps } from '@blocknote/react';
+import type {
+ DefaultReactSuggestionItem,
+ SuggestionMenuProps,
+} from '@blocknote/react';
import { type IconComponent } from 'twenty-ui/display';
-export type SuggestionItem = {
- title: string;
- onItemClick: () => void;
+export type SuggestionItem = DefaultReactSuggestionItem & {
aliases?: string[];
Icon?: IconComponent;
};
export type CustomSlashMenuProps = SuggestionMenuProps;
+
+export type MentionItem = DefaultReactSuggestionItem & {
+ recordId?: string;
+ objectNameSingular?: string;
+ objectMetadataId?: string;
+};
+
+export type CustomMentionMenuProps = SuggestionMenuProps;
diff --git a/packages/twenty-front/src/modules/ui/input/editor/hooks/useMentionMenu.ts b/packages/twenty-front/src/modules/ui/input/editor/hooks/useMentionMenu.ts
new file mode 100644
index 00000000000..fe7d01c4204
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/input/editor/hooks/useMentionMenu.ts
@@ -0,0 +1,96 @@
+import { type BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema';
+import { SEARCH_QUERY } from '@/command-menu/graphql/queries/search';
+import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
+import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
+import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
+import { searchRecordStoreFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState';
+import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
+import { type MentionItem } from '@/ui/input/editor/components/types';
+import { useMemo } from 'react';
+import { useRecoilCallback } from 'recoil';
+import {
+ type SearchQuery,
+ type SearchQueryVariables,
+} from '~/generated/graphql';
+
+const MENTION_SEARCH_LIMIT = 50;
+
+export const useMentionMenu = (editor: typeof BLOCK_SCHEMA.BlockNoteEditor) => {
+ const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
+ const apolloCoreClient = useApolloCoreClient();
+
+ const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
+
+ const searchableObjectMetadataItems = useMemo(
+ () =>
+ activeObjectMetadataItems.filter(
+ (item) =>
+ !item.isSystem &&
+ item.isSearchable &&
+ getObjectPermissionsFromMapByObjectMetadataId({
+ objectPermissionsByObjectMetadataId,
+ objectMetadataId: item.id,
+ }).canReadObjectRecords === true,
+ ),
+ [activeObjectMetadataItems, objectPermissionsByObjectMetadataId],
+ );
+
+ const objectsToSearch = useMemo(
+ () => searchableObjectMetadataItems.map(({ nameSingular }) => nameSingular),
+ [searchableObjectMetadataItems],
+ );
+
+ const getMentionItems = useRecoilCallback(
+ ({ set }) =>
+ async (query: string): Promise => {
+ const { data } = await apolloCoreClient.query<
+ SearchQuery,
+ SearchQueryVariables
+ >({
+ query: SEARCH_QUERY,
+ variables: {
+ searchInput: query,
+ limit: MENTION_SEARCH_LIMIT,
+ includedObjectNameSingulars: objectsToSearch,
+ },
+ });
+
+ const searchRecords = data?.search.edges.map((edge) => edge.node) || [];
+
+ searchRecords.forEach((searchRecord) => {
+ set(searchRecordStoreFamilyState(searchRecord.recordId), {
+ ...searchRecord,
+ record: undefined,
+ });
+ });
+
+ return searchRecords.map((searchRecord) => {
+ const objectMetadataItem = searchableObjectMetadataItems.find(
+ (item) => item.nameSingular === searchRecord.objectNameSingular,
+ );
+
+ return {
+ title: searchRecord.label,
+ recordId: searchRecord.recordId,
+ objectNameSingular: searchRecord.objectNameSingular,
+ objectMetadataId: objectMetadataItem?.id,
+ onItemClick: () => {
+ editor.insertInlineContent([
+ {
+ type: 'mention',
+ props: {
+ recordId: searchRecord.recordId,
+ objectMetadataId: objectMetadataItem?.id ?? '',
+ },
+ },
+ ' ',
+ ]);
+ },
+ };
+ });
+ },
+ [apolloCoreClient, editor, objectsToSearch, searchableObjectMetadataItems],
+ );
+
+ return getMentionItems;
+};
diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuItemsContainer.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuItemsContainer.tsx
index 7181312f5b6..dc6c12fc10f 100644
--- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuItemsContainer.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuItemsContainer.tsx
@@ -56,15 +56,15 @@ export const DropdownMenuItemsContainer = ({
scrollable?: boolean;
}) => {
return scrollable === true ? (
-
-
+
+
{children}
-
-
+
+
) : (
{children}
diff --git a/packages/twenty-sdk/src/front-component/remote/generated/remote-elements.ts b/packages/twenty-sdk/src/front-component/remote/generated/remote-elements.ts
index 2361bad102c..0fd91e4cae4 100644
--- a/packages/twenty-sdk/src/front-component/remote/generated/remote-elements.ts
+++ b/packages/twenty-sdk/src/front-component/remote/generated/remote-elements.ts
@@ -6950,6 +6950,8 @@ export const TwentyUiMenuItemSelectTagElement = createRemoteElement<
export type TwentyUiMenuItemSuggestionProperties = {
withIconContainer?: boolean;
text: string;
+ contextualText?: string;
+ contextualTextPosition?: string;
selected?: boolean;
className?: string;
};
@@ -6964,6 +6966,8 @@ export const TwentyUiMenuItemSuggestionElement = createRemoteElement<
properties: {
withIconContainer: { type: Boolean },
text: { type: String },
+ contextualText: { type: String },
+ contextualTextPosition: { type: String },
selected: { type: Boolean },
className: { type: String },
},
diff --git a/packages/twenty-ui/src/components/chip/LinkChip.tsx b/packages/twenty-ui/src/components/chip/LinkChip.tsx
index 33771e42fb7..6e0c42ecbbb 100644
--- a/packages/twenty-ui/src/components/chip/LinkChip.tsx
+++ b/packages/twenty-ui/src/components/chip/LinkChip.tsx
@@ -26,6 +26,7 @@ const StyledLink = styled(Link)`
display: inline-flex;
text-decoration: none;
min-width: 0;
+ vertical-align: middle;
`;
export const LinkChip = ({
diff --git a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSuggestion.tsx b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSuggestion.tsx
index 9ef2f8f2a52..82d2d8764bb 100644
--- a/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSuggestion.tsx
+++ b/packages/twenty-ui/src/navigation/menu/menu-item/components/MenuItemSuggestion.tsx
@@ -10,6 +10,8 @@ export type MenuItemSuggestionProps = {
LeftIcon?: IconComponent | null;
withIconContainer?: boolean;
text: string;
+ contextualText?: string;
+ contextualTextPosition?: 'left' | 'right';
selected?: boolean;
className?: string;
onClick?: (event: MouseEvent) => void;
@@ -55,6 +57,8 @@ export const MenuItemSuggestion = ({
LeftIcon,
withIconContainer = false,
text,
+ contextualText = undefined,
+ contextualTextPosition = 'left',
className,
selected,
onClick,
@@ -77,6 +81,8 @@ export const MenuItemSuggestion = ({