diff --git a/packages/dify-ui/src/autocomplete/index.stories.tsx b/packages/dify-ui/src/autocomplete/index.stories.tsx index 6de6b16766..5fa35015c7 100644 --- a/packages/dify-ui/src/autocomplete/index.stories.tsx +++ b/packages/dify-ui/src/autocomplete/index.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite' import type { Virtualizer } from '@tanstack/react-virtual' import type { RefObject } from 'react' import { useVirtualizer } from '@tanstack/react-virtual' -import { useEffect, useMemo, useRef, useState } from 'react' +import { useEffect, useMemo, useRef, useState, useTransition } from 'react' import { Autocomplete, AutocompleteClear, @@ -159,6 +159,29 @@ const virtualizedSuggestions: Suggestion[] = Array.from({ length: 1000 }, (_, in const getSuggestionLabel = (item: Suggestion) => item.label +async function searchSuggestions( + suggestions: Suggestion[], + query: string, + filter: (item: string, query: string) => boolean, +): Promise<{ items: Suggestion[], error: string | null }> { + await new Promise(resolve => window.setTimeout(resolve, 500)) + + if (query === 'will_error') { + return { + items: [], + error: 'Failed to load suggestions. Please try again.', + } + } + + return { + items: suggestions.filter(item => ( + filter(item.label, query) + || (item.description ? filter(item.description, query) : false) + )), + error: null, + } +} + const SuggestionItem = ({ item, dense, @@ -227,6 +250,7 @@ const BasicTagAutocomplete = ({ @@ -311,32 +335,64 @@ const LimitedStatus = ({ } const AsyncSearchDemo = () => { - const [value, setValue] = useState('agent') - const [loading, setLoading] = useState(false) - const [items, setItems] = useState(remoteSuggestions) + const [searchValue, setSearchValue] = useState('') + const [searchResults, setSearchResults] = useState([]) + const [error, setError] = useState(null) + const [isPending, startTransition] = useTransition() + const { contains } = useAutocompleteFilter() + const abortControllerRef = useRef(null) - useEffect(() => { - setLoading(true) - const timeout = window.setTimeout(() => { - setItems( - value.trim() - ? remoteSuggestions.filter(item => item.label.toLowerCase().includes(value.trim().toLowerCase())) - : remoteSuggestions, - ) - setLoading(false) - }, 500) + const status = (() => { + if (isPending) + return 'Searching remote suggestions…' - return () => window.clearTimeout(timeout) - }, [value]) + if (error) + return error + + if (searchValue === '') + return null + + if (searchResults.length === 0) + return `No remote suggestion matches "${searchValue}".` + + return `${searchResults.length} remote suggestion${searchResults.length === 1 ? '' : 's'} found` + })() return (
{ + setSearchValue(nextSearchValue) + + const controller = new AbortController() + abortControllerRef.current?.abort() + abortControllerRef.current = controller + + if (nextSearchValue === '') { + setSearchResults([]) + setError(null) + return + } + + startTransition(async () => { + setError(null) + + const result = await searchSuggestions(remoteSuggestions, nextSearchValue, contains) + + if (controller.signal.aborted) + return + + startTransition(() => { + setSearchResults(result.items) + setError(result.error) + }) + }) + }} itemToStringValue={getSuggestionLabel} - openOnInputClick + filter={null} + mode="list" > - + - {loading ? 'Loading suggestions…' : `${items.length} remote suggestions`} + {status} {(item: Suggestion) => ( )} - No remote suggestion. Keep the typed query.
@@ -467,6 +522,7 @@ const FuzzyMatchingDemo = () => { onValueChange={setValue} filter={contains} itemToStringValue={getSuggestionLabel} + mode="list" openOnInputClick > @@ -567,6 +623,7 @@ export const GroupedSuggestions: Story = { @@ -595,6 +652,7 @@ export const LimitResults: Story = { items={workflowSuggestions} itemToStringValue={getSuggestionLabel} limit={5} + mode="list" openOnInputClick > @@ -627,6 +685,7 @@ export const CommandPalette: Story = { inline items={commandGroups} itemToStringValue={getSuggestionLabel} + mode="list" autoHighlight="always" keepHighlight > @@ -649,6 +708,7 @@ const VirtualizedLongSuggestionsDemo = () => { { @@ -686,6 +746,7 @@ export const Empty: Story = { items={tagSuggestions} itemToStringValue={getSuggestionLabel} defaultValue="private-release-note" + mode="list" openOnInputClick > @@ -710,7 +771,7 @@ export const Empty: Story = { export const DisabledAndReadOnly: Story = { render: () => (
- + @@ -724,7 +785,7 @@ export const DisabledAndReadOnly: Story = { - + diff --git a/packages/dify-ui/src/combobox/index.stories.tsx b/packages/dify-ui/src/combobox/index.stories.tsx index c788490268..8cf40951af 100644 --- a/packages/dify-ui/src/combobox/index.stories.tsx +++ b/packages/dify-ui/src/combobox/index.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite' import type { Virtualizer } from '@tanstack/react-virtual' import type { RefObject } from 'react' import { useVirtualizer } from '@tanstack/react-virtual' -import { useEffect, useRef, useState } from 'react' +import { useEffect, useMemo, useRef, useState, useTransition } from 'react' import { Combobox, ComboboxChip, @@ -26,6 +26,7 @@ import { ComboboxStatus, ComboboxTrigger, ComboboxValue, + useComboboxFilter, useComboboxFilteredItems, } from '.' import { cn } from '../cn' @@ -178,8 +179,34 @@ const defaultPopupDataSource = dataSourceOptions[1]! const readOnlyDataSource = dataSourceOptions[2]! const defaultTool = toolGroups[0]!.items[0]! const defaultReviewers = [reviewerOptions[0]!, reviewerOptions[1]!] +const defaultAsyncReviewers = [reviewerOptions[1]!] const defaultTag = tagOptions[2]! +const getOptionLabel = (option: Option) => option.label + +async function searchOptions( + options: Option[], + query: string, + filter: (item: string, query: string) => boolean, +): Promise<{ items: Option[], error: string | null }> { + await new Promise(resolve => window.setTimeout(resolve, 450)) + + if (query === 'will_error') { + return { + items: [], + error: 'Failed to fetch matches. Please try again.', + } + } + + return { + items: options.filter(option => ( + filter(option.label, query) + || (option.meta ? filter(option.meta, query) : false) + )), + error: null, + } +} + const renderOptionItem = (option: Option) => ( @@ -348,35 +375,88 @@ const VirtualizedLongListDemo = () => { } const AsyncDirectoryDemo = () => { - const [inputValue, setInputValue] = useState('ma') - const [value, setValue] = useState