Skip to content

Commit 964bdfb

Browse files
Register chat as the global search command (opensearch-project#10824)
* Remove action filter and add comments Signed-off-by: Lin Wang <[email protected]> * Register chat to global search command Signed-off-by: Lin Wang <[email protected]> * Changeset file for PR opensearch-project#10824 created/updated * Changeset file for PR opensearch-project#10824 created/updated * Fix unit tests of chat header button Signed-off-by: Lin Wang <[email protected]> * Remove comments Signed-off-by: Lin Wang <[email protected]> --------- Signed-off-by: Lin Wang <[email protected]> Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
1 parent 88c406e commit 964bdfb

File tree

9 files changed

+467
-141
lines changed

9 files changed

+467
-141
lines changed

changelogs/fragments/10824.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
feat:
2+
- Register chat as the global search command ([#10824](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/10824))

src/core/public/chrome/ui/header/header_search_bar.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -153,32 +153,42 @@ export const HeaderSearchBar = ({ globalSearchCommands, panel, onSearchResultCli
153153
const abortController = new AbortController();
154154
ongoingAbortControllersRef.current.push({ controller: abortController, query: value });
155155
if (enterKeyDownRef.current) {
156-
globalSearchCommands
157-
.filter((item) => !!item.action)
158-
.forEach((command) => {
159-
command.action?.({
160-
content: value,
161-
});
156+
globalSearchCommands.forEach((command) => {
157+
command.action?.({
158+
content: value,
162159
});
160+
});
163161
enterKeyDownRef.current = false;
164162
setIsPopoverOpen(false);
165163
setSearchValue('');
166164
searchBarInputRef.current?.blur();
167165
return;
168166
}
167+
// Separate ACTIONS commands from other command types for special handling
168+
// ACTIONS commands should always be executed and rendered at the end
169169
const commandsWithoutActions = globalSearchCommands.filter(
170170
(command) => command.type !== 'ACTIONS'
171171
);
172+
173+
// Filter commands that have an alias and match the search input prefix
174+
// For example, if a command has alias "@" and user types "@something", it will be included
172175
const filteredCommands = commandsWithoutActions.filter((command) => {
173176
const alias = SearchCommandTypes[command.type].alias;
174177
return alias && value.startsWith(alias);
175178
});
179+
180+
// Get commands without aliases as default search commands
176181
const defaultSearchCommands = commandsWithoutActions.filter((command) => {
177182
return !SearchCommandTypes[command.type].alias;
178183
});
184+
185+
// If no alias-based commands matched, use default search commands instead
179186
if (filteredCommands.length === 0) {
180187
filteredCommands.push(...defaultSearchCommands);
181188
}
189+
190+
// Always append ACTIONS commands at the end to ensure they are executed and rendered last
191+
// This ensures action commands are available regardless of whether user used an alias or not
182192
filteredCommands.push(
183193
...globalSearchCommands.filter((command) => command.type === 'ACTIONS')
184194
);
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import React from 'react';
7+
import { render, waitFor } from '@testing-library/react';
8+
9+
import { ChatHeaderButton, ChatHeaderButtonInstance } from './chat_header_button';
10+
import { ChatService } from '../services/chat_service';
11+
import { coreMock } from '../../../../core/public/mocks';
12+
13+
// Mock dependencies
14+
15+
jest.mock('./chat_window', () => {
16+
const ActualReact = jest.requireActual('react');
17+
return {
18+
ChatWindow: ActualReact.forwardRef((props: any, ref: any) => {
19+
ActualReact.useImperativeHandle(ref, () => ({
20+
startNewChat: jest.fn(),
21+
sendMessage: jest.fn().mockResolvedValue(undefined),
22+
}));
23+
return null;
24+
}),
25+
};
26+
});
27+
28+
describe('ChatHeaderButton', () => {
29+
let mockCore: ReturnType<typeof coreMock.createStart>;
30+
let mockChatService: jest.Mocked<ChatService>;
31+
let mockContextProvider: any;
32+
33+
beforeEach(() => {
34+
jest.clearAllMocks();
35+
mockCore = coreMock.createStart();
36+
mockChatService = {
37+
sendMessage: jest.fn(),
38+
newThread: jest.fn(),
39+
} as any;
40+
mockContextProvider = {};
41+
42+
// Mock sidecar
43+
mockCore.overlays.sidecar.open = jest.fn().mockReturnValue({
44+
close: jest.fn(),
45+
});
46+
mockCore.overlays.sidecar.setSidecarConfig = jest.fn();
47+
});
48+
49+
describe('ref functionality', () => {
50+
it('should expose startNewConversation method via ref', async () => {
51+
const ref = React.createRef<ChatHeaderButtonInstance>();
52+
53+
render(
54+
<ChatHeaderButton
55+
core={mockCore}
56+
chatService={mockChatService}
57+
contextProvider={mockContextProvider}
58+
ref={ref}
59+
/>
60+
);
61+
62+
expect(ref.current).toBeDefined();
63+
expect(ref.current?.startNewConversation).toBeDefined();
64+
expect(typeof ref.current?.startNewConversation).toBe('function');
65+
});
66+
67+
it('should call startNewChat and sendMessage when startNewConversation is invoked', async () => {
68+
const ref = React.createRef<ChatHeaderButtonInstance>();
69+
70+
render(
71+
<ChatHeaderButton
72+
core={mockCore}
73+
chatService={mockChatService}
74+
contextProvider={mockContextProvider}
75+
ref={ref}
76+
/>
77+
);
78+
79+
// Call startNewConversation
80+
await ref.current?.startNewConversation({ content: 'test message' });
81+
82+
// Verify sidecar was opened
83+
await waitFor(() => {
84+
expect(mockCore.overlays.sidecar.open).toHaveBeenCalled();
85+
});
86+
});
87+
});
88+
});

0 commit comments

Comments
 (0)