diff --git a/apps/web/src/components/features/input/Mentions/ShortcutsSuggestions/ShortcutsSuggestions.tsx b/apps/web/src/components/features/input/Mentions/ShortcutsSuggestions/ShortcutsSuggestions.tsx index 58a90441c..6816bf76a 100644 --- a/apps/web/src/components/features/input/Mentions/ShortcutsSuggestions/ShortcutsSuggestions.tsx +++ b/apps/web/src/components/features/input/Mentions/ShortcutsSuggestions/ShortcutsSuggestions.tsx @@ -1,6 +1,6 @@ import type { ListShortcutsResponse, Shortcut } from '@buster/server-shared/shortcuts'; import { useNavigate } from '@tanstack/react-router'; -import { useMemo } from 'react'; +import { useMemo, useRef } from 'react'; import { useDeleteShortcut, useGetShortcut } from '@/api/buster_rest/shortcuts/queryRequests'; import { ErrorCard } from '@/components/ui/error/ErrorCard'; import { Trash } from '@/components/ui/icons'; @@ -16,7 +16,6 @@ import type { MentionInputSuggestionsProps, MentionInputSuggestionsRef, } from '@/components/ui/inputs/MentionInputSuggestions'; -import { CircleSpinnerLoader } from '@/components/ui/loaders'; import { ShortcutPopoverContent } from './ShortcutPopoverContent'; export const SHORTCUT_MENTION_TRIGGER = '/'; @@ -28,34 +27,42 @@ export const useCreateShortcutsMentionsSuggestions = ( const navigate = useNavigate(); const createShortcutForMention = useCreateShortcutForMention(); + const currentItemsRef = useRef(shortcuts); + + currentItemsRef.current = shortcuts; + return useMemo( () => createMentionSuggestionExtension({ trigger: SHORTCUT_MENTION_TRIGGER, - items: [ - ...shortcuts.map(createShortcutForMention), - { type: 'separator' as const }, - { - value: 'manageShortcuts', - label: 'Manage shortcuts', - icon: , - doNotAddPipeOnSelect: true, - onSelect: () => { - navigate({ - to: '/app/home/shortcuts', - }); + items: ({ defaultQueryMentionsFilter, query }) => { + const shortcuts = currentItemsRef.current; + const allItems = [ + ...shortcuts.map(createShortcutForMention), + { type: 'separator' as const }, + { + value: 'manageShortcuts', + label: 'Manage shortcuts', + icon: , + doNotAddPipeOnSelect: true, + onSelect: () => { + navigate({ + to: '/app/home/shortcuts', + }); + }, }, - }, - { - value: 'createShortcut', - label: 'Create shortcut', - icon: , - doNotAddPipeOnSelect: true, - onSelect: () => { - setOpenCreateShortcutModal(true); + { + value: 'createShortcut', + label: 'Create shortcut', + icon: , + doNotAddPipeOnSelect: true, + onSelect: () => { + setOpenCreateShortcutModal(true); + }, }, - }, - ], + ]; + return defaultQueryMentionsFilter(query, allItems); + }, popoverContent: ShortcutPopoverContent, onChangeTransform: (v) => { const foundShortcut = shortcuts.find((shortcut) => shortcut.name === v.label); @@ -97,8 +104,8 @@ export const useCreateShortcutForMention = () => { label: 'Delete', icon: , value: 'delete', - onClick: () => { - deleteShortcut({ id: shortcut.id }); + onClick: async () => { + await deleteShortcut({ id: shortcut.id }); }, }, ]} diff --git a/apps/web/src/components/ui/inputs/MentionInput/MentionInput.stories.tsx b/apps/web/src/components/ui/inputs/MentionInput/MentionInput.stories.tsx index b3afb878f..23e834762 100644 --- a/apps/web/src/components/ui/inputs/MentionInput/MentionInput.stories.tsx +++ b/apps/web/src/components/ui/inputs/MentionInput/MentionInput.stories.tsx @@ -3,7 +3,6 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { useMemo, useRef, useState } from 'react'; import { fn } from 'storybook/test'; import { createMentionSuggestionExtension } from './createMentionSuggestionOption'; -import { defaultQueryMentionsFilter } from './defaultQueryMentionsFilter'; import { MentionInput } from './MentionInput'; import type { MentionInputRef, MentionInputTriggerItem } from './MentionInput.types'; @@ -329,6 +328,10 @@ export const DynamicItems: Story = { { value: 'Initial Item 2', label: 'Initial Item 2' }, ]); const mentionInputRef = useRef(null); + const currentItemsRef = useRef(dynamicItems); + + // Always keep the ref updated with the latest items + currentItemsRef.current = dynamicItems; const addRandomItem = () => { const randomNames = [ @@ -384,11 +387,15 @@ export const DynamicItems: Story = { setDynamicItems([]); }; + // Create a stable function that always uses the current items from the ref const dynamicSuggestions = useMemo( () => createMentionSuggestionExtension({ trigger: '!', - items: dynamicItems, + items: ({ query, defaultQueryMentionsFilter }) => { + const latestItems = currentItemsRef.current; + return defaultQueryMentionsFilter(query, latestItems); + }, pillStyling: { className: () => { return 'bg-gradient-to-r from-purple-100 to-pink-100 border-purple-300 text-purple-700 hover:from-purple-200 hover:to-pink-200'; @@ -403,7 +410,7 @@ export const DynamicItems: Story = { ); }, }), - [dynamicItems] + [dynamicItems] // Empty dependency array since we're using the ref for fresh data ); return ( diff --git a/apps/web/src/components/ui/inputs/MentionInput/createMentionSuggestionOption.tsx b/apps/web/src/components/ui/inputs/MentionInput/createMentionSuggestionOption.tsx index 5f87a61aa..2a9bd0ebb 100644 --- a/apps/web/src/components/ui/inputs/MentionInput/createMentionSuggestionOption.tsx +++ b/apps/web/src/components/ui/inputs/MentionInput/createMentionSuggestionOption.tsx @@ -20,14 +20,24 @@ export const createMentionSuggestionExtension = ({ pillStyling, }: { trigger: string; - items: MentionInputTriggerItem[] | ((props: { query: string }) => MentionInputTriggerItem[]); //if no function is provided we will use a literal string match + items: + | MentionInputTriggerItem[] + | ((props: { + query: string; + defaultQueryMentionsFilter: typeof defaultQueryMentionsFilter; + }) => MentionInputTriggerItem[]); //if no function is provided we will use a literal string match popoverContent?: MentionPopoverContentCallback; pillStyling?: MentionStylePillProps; onChangeTransform?: MentionSuggestionExtension['onChangeTransform']; }): MentionSuggestionExtension => ({ char: trigger, items: - typeof items === 'function' ? items : ({ query }) => defaultQueryMentionsFilter(query, items), + //beware of stale closures here. We should use a ref to get the latest items + typeof items === 'function' + ? (props) => { + return items({ ...props, defaultQueryMentionsFilter }); + } + : ({ query }) => defaultQueryMentionsFilter(query, items), render: () => { let component: ReactRenderer>;