Compare commits

...
Author SHA1 Message Date
Félix Malfait 512774bd36 Improvements 2026-02-13 13:40:41 +01:00
Félix MalfaitandGitHub b7434451aa Merge branch 'main' into acee3/add-mentions 2026-02-13 11:53:33 +01:00
Félix MalfaitandGitHub f91b558399 Merge branch 'main' into acee3/add-mentions 2026-02-12 23:18:42 +01:00
Alex Cheung 1855124c0e core: remove redundant code 2025-12-15 11:46:21 -05:00
Alex Cheung bc83f4ad63 fix: Added permissions check 2025-12-15 03:17:27 -05:00
Alex Cheung a0204fc6b9 Merge branch 'main' into acee3/add-mentions 2025-12-09 13:04:28 -05:00
Alex Cheung 80f59f80f8 Merge branch 'main' into acee3/add-mentions 2025-12-08 22:55:09 -05:00
Alex Cheung 89070fbf3c fix: Added image icon to the chip component and centered it 2025-12-08 22:19:23 -05:00
Alex Cheung df513d91ca feat: Added name of object as subtitle 2025-12-08 18:13:27 -05:00
Alex Cheung 70a2728552 chore: set width of menu to 240px 2025-12-08 17:37:59 -05:00
Alex Cheung 22979fbde4 fix: Fixed the padding issue of the scrollable dropdown by moving the scrollable container within the external container instead of inside 2025-12-08 17:22:52 -05:00
Alex Cheung b7aa32f034 refactor: remove duplicated code 2025-12-05 16:45:58 -05:00
Alex Cheung f68dd72da3 fix: Updated to remove race condition with async function 2025-12-05 16:37:39 -05:00
Alex Cheung fc01449921 fix: Add error handling to rendering of mentions 2025-12-05 14:55:12 -05:00
Alex Cheung 98951653ac fix: Make limit on retrieved items actually work by following similar pattern to SingleRecordPickerMenu 2025-12-05 14:55:01 -05:00
Alex Cheung 086267aea8 fix: Undid fix to change limit to first 2025-12-05 14:47:07 -05:00
Alex Cheung b7646fc6d4 fix: Changed wrong limit statement to first statement in graphql query 2025-12-05 04:30:48 -05:00
Alex Cheung ad66b21f78 fix: Made in-line styling of tags more consistent to not cut-off any text 2025-12-05 03:16:57 -05:00
Alex Cheung 5a39f3f1c9 feat: integrate mention menu into block editor with click-outside behavior 2025-12-05 00:36:35 -05:00
Alex Cheung b3147088f6 feat: add mention menu data hook 2025-12-05 00:36:06 -05:00
Alex Cheung 6581b9294c feat: add mention menu dropdown UI 2025-12-05 00:35:18 -05:00
Alex Cheung 1ae428ae24 feat: add mention inline content spec for notes mentions 2025-12-05 00:32:38 -05:00
19 changed files with 487 additions and 28 deletions
@@ -0,0 +1,95 @@
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 { useRecoilValue } from 'recoil';
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,
objectRecordId: recordId,
skip: !objectNameSingular || !recordId,
});
if (loading) {
return null;
}
if (!record) {
return (
<Chip
label={t`Deleted record`}
variant={ChipVariant.Transparent}
disabled
/>
);
}
if (!objectNameSingular) {
return (
<Chip
label={t`Unknown object`}
variant={ChipVariant.Transparent}
disabled
/>
);
}
return (
<StyledRecordChip
objectNameSingular={objectNameSingular}
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({
@@ -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,
@@ -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) => {
@@ -0,0 +1,2 @@
export const MENTION_MENU_DROPDOWN_CLICK_OUTSIDE_ID =
'mention-menu-dropdown-click-outside-id';
@@ -0,0 +1 @@
export const MENTION_MENU_LIST_ID = 'mention-menu-list-id';
@@ -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}
/>
<SuggestionMenuController
triggerCharacter="@"
getItems={async (query) => getMentionItems(query)}
suggestionMenuComponent={CustomMentionMenu}
/>
</BlockNoteView>
</StyledEditor>
);
@@ -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 (
<StyledContainer ref={refs.setReference}>
<CustomMentionMenuSelectedIndexSyncEffect
items={filteredItems}
selectedIndex={selectedIndex}
/>
<>
{createPortal(
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.1 }}
onClick={handleContainerClick}
>
<OverlayContainer
ref={refs.setFloating}
style={floatingStyles}
data-click-outside-id={MENTION_MENU_DROPDOWN_CLICK_OUTSIDE_ID}
>
<DropdownContent widthInPixels={MenuPixelWidth}>
<DropdownMenuItemsContainer hasMaxHeight>
<SelectableList
focusId={MENTION_MENU_DROPDOWN_CLICK_OUTSIDE_ID}
selectableListInstanceId={MENTION_MENU_LIST_ID}
selectableItemIdArray={filteredItems.map(
(item) => item.recordId!,
)}
>
{filteredItems.map((item) => (
<CustomMentionMenuListItem
key={item.recordId!}
recordId={item.recordId!}
onClick={() => onItemClick?.(item)}
objectNameSingular={item.objectNameSingular!}
/>
))}
</SelectableList>
</DropdownMenuItemsContainer>
</DropdownContent>
</OverlayContainer>
</motion.div>,
document.body,
)}
</>
</StyledContainer>
);
};
@@ -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 (
<SelectableListItem itemId={recordId} onEnter={handleClick}>
<MenuItemSuggestion
selected={isSelectedItem}
onClick={handleClick}
text={`${searchRecord.label}`}
contextualText={objectMetadataItem.labelSingular}
contextualTextPosition="left"
LeftIcon={() => (
<Avatar
placeholder={searchRecord.label}
placeholderColorSeed={recordId}
avatarUrl={searchRecord.imageUrl}
type={getAvatarType(objectNameSingular) ?? 'rounded'}
size="sm"
/>
)}
/>
</SelectableListItem>
);
};
@@ -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 <></>;
};
@@ -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 (
<StyledContainer ref={refs.setReference}>
<CustomSlashMenuSelectedIndexSyncEffect
items={items}
selectedIndex={selectedIndex}
/>
<>
{createPortal(
<motion.div
@@ -0,0 +1,29 @@
import { SLASH_MENU_LIST_ID } from '@/ui/input/constants/SlashMenuListId';
import { type SuggestionItem } 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 CustomSlashMenuSelectedIndexSyncEffectProps = {
items: SuggestionItem[];
selectedIndex: number | undefined;
};
export const CustomSlashMenuSelectedIndexSyncEffect = ({
items,
selectedIndex,
}: CustomSlashMenuSelectedIndexSyncEffectProps) => {
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 <></>;
};
@@ -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<SuggestionItem>;
export type MentionItem = DefaultReactSuggestionItem & {
recordId?: string;
objectNameSingular?: string;
objectMetadataId?: string;
};
export type CustomMentionMenuProps = SuggestionMenuProps<MentionItem>;
@@ -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<MentionItem[]> => {
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;
};
@@ -56,15 +56,15 @@ export const DropdownMenuItemsContainer = ({
scrollable?: boolean;
}) => {
return scrollable === true ? (
<StyledScrollableContainer
maxHeight={
hasMaxHeight ? DROPDOWN_MENU_ITEMS_CONTAINER_MAX_HEIGHT : undefined
}
>
<StyledExternalContainer role="listbox">
<StyledExternalContainer role="listbox">
<StyledScrollableContainer
maxHeight={
hasMaxHeight ? DROPDOWN_MENU_ITEMS_CONTAINER_MAX_HEIGHT : undefined
}
>
<StyledInternalContainer>{children}</StyledInternalContainer>
</StyledExternalContainer>
</StyledScrollableContainer>
</StyledScrollableContainer>
</StyledExternalContainer>
) : (
<StyledExternalContainer role="listbox">
<StyledInternalContainer>{children}</StyledInternalContainer>
@@ -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 },
},
@@ -26,6 +26,7 @@ const StyledLink = styled(Link)`
display: inline-flex;
text-decoration: none;
min-width: 0;
vertical-align: middle;
`;
export const LinkChip = ({
@@ -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<HTMLLIElement>) => 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 = ({
<MenuItemLeftContent
LeftIcon={LeftIcon ?? undefined}
text={text}
contextualText={contextualText}
contextualTextPosition={contextualTextPosition}
withIconContainer={withIconContainer}
/>
</StyledMenuItemLeftContent>