From 4faad30a855e7996bee29c56210b2fc3817d9a5d Mon Sep 17 00:00:00 2001 From: Lin Wang Date: Mon, 4 Dec 2023 16:57:32 +0800 Subject: [PATCH] Fix redirect new session when dock right (#36) * fix: change to new session after history deleted Signed-off-by: Lin Wang * test: add miss file and style mock Signed-off-by: Lin Wang * feat: add unit test for clear deleted old chat session data Signed-off-by: Lin Wang * refactor: change handleEditConversationConfirmModalClose to handleEditConversationModalClose Signed-off-by: Lin Wang * Utilize barrel file for cleaner imports Example of using index.ts and importing one line instead of two. Signed-off-by: Kawika Avilla * refactor: move hooks references to index.ts Signed-off-by: Lin Wang --------- Signed-off-by: Lin Wang Signed-off-by: Kawika Avilla Co-authored-by: Kawika Avilla --- public/chat_header_button.tsx | 2 +- .../components/chat_window_header_title.tsx | 6 +- public/contexts/index.ts | 7 ++ public/contexts/set_context.tsx | 2 +- public/hooks/index.ts | 8 ++ public/tabs/chat/chat_page.tsx | 7 +- public/tabs/chat/chat_page_content.tsx | 5 +- .../chat/controls/chat_input_controls.tsx | 6 +- .../__tests__/chat_history_page.test.tsx | 81 +++++++++++++++++++ public/tabs/history/chat_history_page.tsx | 22 +++-- .../tabs/history/chat_history_search_list.tsx | 8 +- test/__mocks__/fileMock.js | 6 ++ test/__mocks__/styleMock.js | 6 ++ 13 files changed, 139 insertions(+), 27 deletions(-) create mode 100644 public/contexts/index.ts create mode 100644 public/hooks/index.ts create mode 100644 public/tabs/history/__tests__/chat_history_page.test.tsx create mode 100644 test/__mocks__/fileMock.js create mode 100644 test/__mocks__/styleMock.js diff --git a/public/chat_header_button.tsx b/public/chat_header_button.tsx index 74d48cc3..b17de3bb 100644 --- a/public/chat_header_button.tsx +++ b/public/chat_header_button.tsx @@ -11,7 +11,7 @@ import { ApplicationStart } from '../../../src/core/public'; import { ChatFlyout } from './chat_flyout'; import { ChatContext, IChatContext } from './contexts/chat_context'; import { SetContext } from './contexts/set_context'; -import { ChatStateProvider } from './hooks/use_chat_state'; +import { ChatStateProvider } from './hooks'; import './index.scss'; import chatIcon from './assets/chat.svg'; import { ActionExecutor, AssistantActions, ContentRenderer, UserAccount, TabId } from './types'; diff --git a/public/components/chat_window_header_title.tsx b/public/components/chat_window_header_title.tsx index 7f39b952..5c0f5864 100644 --- a/public/components/chat_window_header_title.tsx +++ b/public/components/chat_window_header_title.tsx @@ -12,13 +12,11 @@ import { EuiButtonIcon, } from '@elastic/eui'; import React, { useCallback, useState } from 'react'; -import { useChatContext } from '../contexts/chat_context'; -import { useChatActions } from '../hooks/use_chat_actions'; +import { useChatContext } from '../contexts'; +import { useChatActions, useChatState, useSaveChat } from '../hooks'; import { NotebookNameModal } from './notebook/notebook_name_modal'; import { ChatExperimentalBadge } from './chat_experimental_badge'; import { useCore } from '../contexts/core_context'; -import { useChatState } from '../hooks/use_chat_state'; -import { useSaveChat } from '../hooks/use_save_chat'; import { EditConversationNameModal } from './edit_conversation_name_modal'; export const ChatWindowHeaderTitle = React.memo(() => { diff --git a/public/contexts/index.ts b/public/contexts/index.ts new file mode 100644 index 00000000..469c4a1c --- /dev/null +++ b/public/contexts/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { useChatContext } from './chat_context'; +export { useCore } from './core_context'; diff --git a/public/contexts/set_context.tsx b/public/contexts/set_context.tsx index 9b1fd6c5..423c3b09 100644 --- a/public/contexts/set_context.tsx +++ b/public/contexts/set_context.tsx @@ -4,7 +4,7 @@ */ import React from 'react'; -import { useChatActions } from '../hooks/use_chat_actions'; +import { useChatActions } from '../hooks'; import { AssistantActions } from '../types'; interface SetContextProps { diff --git a/public/hooks/index.ts b/public/hooks/index.ts new file mode 100644 index 00000000..05e7214c --- /dev/null +++ b/public/hooks/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { useSaveChat } from './use_save_chat'; +export { useChatState, ChatStateProvider } from './use_chat_state'; +export { useChatActions } from './use_chat_actions'; diff --git a/public/tabs/chat/chat_page.tsx b/public/tabs/chat/chat_page.tsx index 6ca8893f..59f20777 100644 --- a/public/tabs/chat/chat_page.tsx +++ b/public/tabs/chat/chat_page.tsx @@ -6,12 +6,11 @@ import { EuiFlyoutBody, EuiFlyoutFooter, EuiPage, EuiPageBody, EuiSpacer } from '@elastic/eui'; import React, { useCallback, useState } from 'react'; import cs from 'classnames'; -import { useChatContext } from '../../contexts/chat_context'; -import { useChatState } from '../../hooks/use_chat_state'; +import { useObservable } from 'react-use'; +import { useChatContext, useCore } from '../../contexts'; +import { useChatState } from '../../hooks'; import { ChatPageContent } from './chat_page_content'; import { ChatInputControls } from './controls/chat_input_controls'; -import { useObservable } from 'react-use'; -import { useCore } from '../../contexts/core_context'; interface ChatPageProps { className?: string; diff --git a/public/tabs/chat/chat_page_content.tsx b/public/tabs/chat/chat_page_content.tsx index 16ebc5b7..9311f143 100644 --- a/public/tabs/chat/chat_page_content.tsx +++ b/public/tabs/chat/chat_page_content.tsx @@ -16,13 +16,12 @@ import { import React, { useLayoutEffect, useRef } from 'react'; import { IMessage, ISuggestedAction } from '../../../common/types/chat_saved_object_attributes'; import { TermsAndConditions } from '../../components/terms_and_conditions'; -import { useChatContext } from '../../contexts/chat_context'; -import { useChatState } from '../../hooks/use_chat_state'; +import { useChatContext } from '../../contexts'; +import { useChatState, useChatActions } from '../../hooks'; import { ChatPageGreetings } from './chat_page_greetings'; import { MessageBubble } from './messages/message_bubble'; import { MessageContent } from './messages/message_content'; import { SuggestionBubble } from './suggestions/suggestion_bubble'; -import { useChatActions } from '../../hooks/use_chat_actions'; interface ChatPageContentProps { showGreetings: boolean; diff --git a/public/tabs/chat/controls/chat_input_controls.tsx b/public/tabs/chat/controls/chat_input_controls.tsx index ed7654f4..b8e010c9 100644 --- a/public/tabs/chat/controls/chat_input_controls.tsx +++ b/public/tabs/chat/controls/chat_input_controls.tsx @@ -3,13 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiButton, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiTextArea } from '@elastic/eui'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiTextArea } from '@elastic/eui'; import autosize from 'autosize'; import React, { useRef } from 'react'; import { useEffectOnce } from 'react-use'; import { IMessage } from '../../../../common/types/chat_saved_object_attributes'; -import { useChatContext } from '../../../contexts/chat_context'; -import { useChatActions } from '../../../hooks/use_chat_actions'; +import { useChatContext } from '../../../contexts'; +import { useChatActions } from '../../../hooks'; interface ChatInputControlsProps { disabled: boolean; diff --git a/public/tabs/history/__tests__/chat_history_page.test.tsx b/public/tabs/history/__tests__/chat_history_page.test.tsx new file mode 100644 index 00000000..7d7ef521 --- /dev/null +++ b/public/tabs/history/__tests__/chat_history_page.test.tsx @@ -0,0 +1,81 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { act, fireEvent, render } from '@testing-library/react'; +import { BehaviorSubject } from 'rxjs'; +import { I18nProvider } from '@osd/i18n/react'; + +import * as useChatStateExports from '../../../hooks'; +import * as contextsExports from '../../../contexts'; + +import { ChatHistoryPage } from '../chat_history_page'; + +const setup = () => { + const useCoreMock = { + services: { + sessions: { + sessions$: new BehaviorSubject({ + objects: [ + { + id: '1', + title: 'foo', + }, + ], + total: 1, + }), + status$: new BehaviorSubject('idle'), + load: jest.fn(), + }, + sessionLoad: {}, + }, + }; + const useChatStateMock = { + chatStateDispatch: jest.fn(), + }; + const useChatContextMock = { + sessionId: '1', + setSessionId: jest.fn(), + setTitle: jest.fn(), + }; + jest.spyOn(contextsExports, 'useCore').mockReturnValue(useCoreMock); + jest.spyOn(useChatStateExports, 'useChatState').mockReturnValue(useChatStateMock); + jest.spyOn(contextsExports, 'useChatContext').mockReturnValue(useChatContextMock); + + const renderResult = render( + + + + ); + + return { + useCoreMock, + useChatStateMock, + useChatContextMock, + renderResult, + }; +}; + +describe('', () => { + it('should clear old session data after current session deleted', async () => { + const { renderResult, useChatStateMock, useChatContextMock } = setup(); + + act(() => { + fireEvent.click(renderResult.getByLabelText('Delete conversation')); + }); + + expect(useChatContextMock.setSessionId).not.toHaveBeenCalled(); + expect(useChatContextMock.setTitle).not.toHaveBeenCalled(); + expect(useChatStateMock.chatStateDispatch).not.toHaveBeenCalled(); + + act(() => { + fireEvent.click(renderResult.getByTestId('confirmModalConfirmButton')); + }); + + expect(useChatContextMock.setSessionId).toHaveBeenLastCalledWith(undefined); + expect(useChatContextMock.setTitle).toHaveBeenLastCalledWith(undefined); + expect(useChatStateMock.chatStateDispatch).toHaveBeenLastCalledWith({ type: 'reset' }); + }); +}); diff --git a/public/tabs/history/chat_history_page.tsx b/public/tabs/history/chat_history_page.tsx index 3dc8fefd..ac309888 100644 --- a/public/tabs/history/chat_history_page.tsx +++ b/public/tabs/history/chat_history_page.tsx @@ -20,9 +20,8 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { FormattedMessage } from '@osd/i18n/react'; import { useDebounce, useObservable } from 'react-use'; import cs from 'classnames'; -import { useChatActions } from '../../hooks/use_chat_actions'; -import { useChatContext } from '../../contexts/chat_context'; -import { useCore } from '../../contexts/core_context'; +import { useChatActions, useChatState } from '../../hooks'; +import { useChatContext, useCore } from '../../contexts'; import { ChatHistorySearchList } from './chat_history_search_list'; interface ChatHistoryPageProps { @@ -33,7 +32,14 @@ interface ChatHistoryPageProps { export const ChatHistoryPage: React.FC = React.memo((props) => { const { services } = useCore(); const { loadChat } = useChatActions(); - const { setSelectedTabId, flyoutFullScreen, sessionId } = useChatContext(); + const { chatStateDispatch } = useChatState(); + const { + setSelectedTabId, + flyoutFullScreen, + sessionId, + setSessionId, + setTitle, + } = useChatContext(); const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(10); const [searchName, setSearchName] = useState(); @@ -70,11 +76,13 @@ export const ChatHistoryPage: React.FC = React.memo((props const handleHistoryDeleted = useCallback( (id: string) => { if (sessionId === id) { - // Switch to new conversation when current session be deleted - loadChat(); + // Clear old session chat states + setTitle(undefined); + setSessionId(undefined); + chatStateDispatch({ type: 'reset' }); } }, - [sessionId, loadChat] + [sessionId, setSessionId, setTitle, chatStateDispatch] ); useDebounce( diff --git a/public/tabs/history/chat_history_search_list.tsx b/public/tabs/history/chat_history_search_list.tsx index a14c7983..2f703bf3 100644 --- a/public/tabs/history/chat_history_search_list.tsx +++ b/public/tabs/history/chat_history_search_list.tsx @@ -51,7 +51,7 @@ export const ChatHistorySearchList = ({ } | null>(null); const [deletingConversation, setDeletingConversation] = useState<{ id: string } | null>(null); - const handleEditConversationCancel = useCallback( + const handleEditConversationModalClose = useCallback( (status: 'updated' | string) => { if (status === 'updated') { onRefresh(); @@ -61,7 +61,7 @@ export const ChatHistorySearchList = ({ [setEditingConversation, onRefresh] ); - const handleDeleteConversationCancel = useCallback( + const handleDeleteConversationConfirmModalClose = useCallback( (status: 'deleted' | string) => { if (status === 'deleted') { onRefresh(); @@ -108,7 +108,7 @@ export const ChatHistorySearchList = ({ /> {editingConversation && ( @@ -116,7 +116,7 @@ export const ChatHistorySearchList = ({ {deletingConversation && ( )} diff --git a/test/__mocks__/fileMock.js b/test/__mocks__/fileMock.js new file mode 100644 index 00000000..cac247b7 --- /dev/null +++ b/test/__mocks__/fileMock.js @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +module.exports = 'file-stub'; diff --git a/test/__mocks__/styleMock.js b/test/__mocks__/styleMock.js new file mode 100644 index 00000000..28de3c8b --- /dev/null +++ b/test/__mocks__/styleMock.js @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +module.exports = {};