diff --git a/packages/dify-ui/docs/selection.md b/packages/dify-ui/docs/selection.md index acdae46dcd6..2edcabc3ca1 100644 --- a/packages/dify-ui/docs/selection.md +++ b/packages/dify-ui/docs/selection.md @@ -80,6 +80,11 @@ single-or-multiple union. Prefer the Base UI `items` collection pattern so the root, value display, and item list share one runtime source of truth. Convert values to strings only at real serialization boundaries. +Treat collection values exposed for rendering as read-only views. In multiple mode, `SelectValue` +may receive a shared frozen empty array, and `useAutocompleteFilteredItems` exposes internal +filtered state. Use non-mutating transforms such as `map`, `filter`, or `toSorted`; create an +explicit copy only when a mutable array is required. + ### Combobox source items and selected values Combobox has separate types for the selected business value and the source record rendered by the diff --git a/packages/dify-ui/src/autocomplete/__tests__/index.spec.tsx b/packages/dify-ui/src/autocomplete/__tests__/index.spec.tsx index 111da03ebb3..024c3984bcf 100644 --- a/packages/dify-ui/src/autocomplete/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/autocomplete/__tests__/index.spec.tsx @@ -19,8 +19,20 @@ import { AutocompleteSeparator, AutocompleteStatus, AutocompleteTrigger, + useAutocompleteFilteredItems, } from '../index' +function AutocompleteTypeExamples() { + const filteredItems = useAutocompleteFilteredItems() + + // @ts-expect-error internally filtered items are read-only + filteredItems.push('workflow') + + return null +} + +void AutocompleteTypeExamples + const renderWithSafeViewport = (ui: React.ReactNode) => render(
{ui}
) diff --git a/packages/dify-ui/src/autocomplete/index.tsx b/packages/dify-ui/src/autocomplete/index.tsx index 42bd7ca5786..e42c71f5484 100644 --- a/packages/dify-ui/src/autocomplete/index.tsx +++ b/packages/dify-ui/src/autocomplete/index.tsx @@ -38,7 +38,8 @@ function Autocomplete(props: AutocompleteProps): React.JSX.Element { const AutocompleteValue = BaseAutocomplete.Value const AutocompleteRow = BaseAutocomplete.Row const useAutocompleteFilter = BaseAutocomplete.useFilter -const useAutocompleteFilteredItems = BaseAutocomplete.useFilteredItems +const useAutocompleteFilteredItems: () => readonly Value[] = + BaseAutocomplete.useFilteredItems type AutocompleteValueProps = BaseAutocomplete.Value.Props type AutocompleteRowProps = BaseAutocomplete.Row.Props diff --git a/packages/dify-ui/src/select/__tests__/index.spec.tsx b/packages/dify-ui/src/select/__tests__/index.spec.tsx index 36cac087387..7f8e030dbc6 100644 --- a/packages/dify-ui/src/select/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/select/__tests__/index.spec.tsx @@ -18,6 +18,22 @@ import { SelectValue, } from '../index' +function SelectTypeExamples() { + return ( + multiple> + > + {(selectedValue) => { + // @ts-expect-error multiple value render state is read-only + selectedValue?.push('seattle') + return selectedValue?.join(', ') ?? '' + }} + + + ) +} + +void SelectTypeExamples + const renderWithSafeViewport = (ui: React.ReactNode) => render(
{ui}
) diff --git a/packages/dify-ui/src/select/index.tsx b/packages/dify-ui/src/select/index.tsx index ed5ffd66de5..029879ad55a 100644 --- a/packages/dify-ui/src/select/index.tsx +++ b/packages/dify-ui/src/select/index.tsx @@ -31,7 +31,7 @@ function Select( const SelectGroup = BaseSelect.Group type SelectSelectedValue = - | (Multiple extends true ? Value[] : Value) + | (Multiple extends true ? readonly Value[] : Value) | null type SelectValueState = Omit< BaseSelect.Value.State, diff --git a/web/app/components/goto-anything/README.md b/web/app/components/goto-anything/README.md index 370869ef182..c2bdb2b40a4 100644 --- a/web/app/components/goto-anything/README.md +++ b/web/app/components/goto-anything/README.md @@ -8,6 +8,7 @@ Global command palette that coordinates detached dialog triggers, typed search, - The dialog component owns the transient search input and selected plugin installer state. - TanStack Query owns remote search lifecycle and cache state for each generated query contract. - Autocomplete owns option registration, highlighting, keyboard navigation, and item activation. -- ScrollArea Viewport is the only results scroll container; Autocomplete List remains the listbox. +- ScrollArea Viewport is the only results scroll container; Autocomplete List is a listbox for + search results and a named grid for command cards. Search actions adapt application, knowledge, plugin, workflow, and RAG owners into palette results. They must not duplicate those features' authorization, navigation, or query contracts. diff --git a/web/app/components/goto-anything/__tests__/index.spec.tsx b/web/app/components/goto-anything/__tests__/index.spec.tsx index fd39b7713cf..d15209e931f 100644 --- a/web/app/components/goto-anything/__tests__/index.spec.tsx +++ b/web/app/components/goto-anything/__tests__/index.spec.tsx @@ -1037,7 +1037,9 @@ describe('GotoAnything', () => { const input = screen.getByRole('combobox', { name: 'app.gotoAnything.searchTitle' }) expect(input).toHaveAttribute('aria-haspopup', 'grid') - expect(screen.getByRole('grid')).toHaveAttribute('id', input.getAttribute('aria-controls')) + expect( + screen.getByRole('grid', { name: 'app.gotoAnything.groups.commands' }), + ).toHaveAttribute('id', input.getAttribute('aria-controls')) expect(screen.getByRole('rowgroup')).toBeInTheDocument() for (const cell of screen.getAllByRole('gridcell')) expect(cell.parentElement).toHaveAttribute('role', 'row') diff --git a/web/app/components/goto-anything/index.tsx b/web/app/components/goto-anything/index.tsx index 475bbbb39c2..aa31771d5ed 100644 --- a/web/app/components/goto-anything/index.tsx +++ b/web/app/components/goto-anything/index.tsx @@ -585,9 +585,16 @@ function GotoAnythingDialog() { /> )} - + $['gotoAnything.groups.commands'], { ns: 'app' }) + : undefined + } + className="max-h-none overflow-visible p-0" + > {!isLoading && !isError && isCommandsMode && autocompleteResultCount > 0 && ( - + {isSlashMode ? t(($) => $['gotoAnything.groups.commands'], { ns: 'app' })