mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 05:37:20 +08:00
refactor(frontend): migration of initial-query-slice.ts to zustand (#11020)
This commit is contained in:
@@ -1,20 +1,24 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import store from "../src/store";
|
||||
import {
|
||||
setInitialPrompt,
|
||||
clearInitialPrompt,
|
||||
} from "../src/state/initial-query-slice";
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { useInitialQueryStore } from "../src/stores/initial-query-store";
|
||||
|
||||
describe("Initial Query Behavior", () => {
|
||||
it("should clear initial query when clearInitialPrompt is dispatched", () => {
|
||||
beforeEach(() => {
|
||||
// Reset the store before each test
|
||||
useInitialQueryStore.getState().reset();
|
||||
});
|
||||
|
||||
it("should clear initial query when clearInitialPrompt is called", () => {
|
||||
const { setInitialPrompt, clearInitialPrompt, initialPrompt } =
|
||||
useInitialQueryStore.getState();
|
||||
|
||||
// Set up initial query in the store
|
||||
store.dispatch(setInitialPrompt("test query"));
|
||||
expect(store.getState().initialQuery.initialPrompt).toBe("test query");
|
||||
setInitialPrompt("test query");
|
||||
expect(useInitialQueryStore.getState().initialPrompt).toBe("test query");
|
||||
|
||||
// Clear the initial query
|
||||
store.dispatch(clearInitialPrompt());
|
||||
clearInitialPrompt();
|
||||
|
||||
// Verify initial query is cleared
|
||||
expect(store.getState().initialQuery.initialPrompt).toBeNull();
|
||||
expect(useInitialQueryStore.getState().initialPrompt).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
32
frontend/package-lock.json
generated
32
frontend/package-lock.json
generated
@@ -59,7 +59,8 @@
|
||||
"tailwind-scrollbar": "^4.0.2",
|
||||
"vite": "^7.1.4",
|
||||
"web-vitals": "^5.1.0",
|
||||
"ws": "^8.18.2"
|
||||
"ws": "^8.18.2",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/parser": "^7.28.3",
|
||||
@@ -18326,6 +18327,35 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz",
|
||||
"integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18.0.0",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=18.0.0",
|
||||
"use-sync-external-store": ">=1.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"use-sync-external-store": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/zwitch": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
||||
|
||||
@@ -58,7 +58,8 @@
|
||||
"tailwind-scrollbar": "^4.0.2",
|
||||
"vite": "^7.1.4",
|
||||
"web-vitals": "^5.1.0",
|
||||
"ws": "^8.18.2"
|
||||
"ws": "^8.18.2",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "npm run make-i18n && cross-env VITE_MOCK_API=false react-router dev",
|
||||
|
||||
@@ -18,6 +18,7 @@ import { useWsClient } from "#/context/ws-client-provider";
|
||||
import { Messages } from "./messages";
|
||||
import { ChatSuggestions } from "./chat-suggestions";
|
||||
import { ScrollProvider } from "#/context/scroll-context";
|
||||
import { useInitialQueryStore } from "#/stores/initial-query-store";
|
||||
|
||||
import { ScrollToBottomButton } from "#/components/shared/buttons/scroll-to-bottom-button";
|
||||
import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
@@ -67,9 +68,7 @@ export function ChatInterface() {
|
||||
"positive" | "negative"
|
||||
>("positive");
|
||||
const [feedbackModalIsOpen, setFeedbackModalIsOpen] = React.useState(false);
|
||||
const { selectedRepository, replayJson } = useSelector(
|
||||
(state: RootState) => state.initialQuery,
|
||||
);
|
||||
const { selectedRepository, replayJson } = useInitialQueryStore();
|
||||
const params = useParams();
|
||||
const { mutateAsync: uploadFiles } = useUploadFiles();
|
||||
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { GitRepository } from "#/types/git";
|
||||
|
||||
type SliceState = {
|
||||
files: string[]; // base64 encoded images
|
||||
initialPrompt: string | null;
|
||||
selectedRepository: GitRepository | null;
|
||||
selectedRepositoryProvider: Provider | null;
|
||||
replayJson: string | null;
|
||||
};
|
||||
|
||||
const initialState: SliceState = {
|
||||
files: [],
|
||||
initialPrompt: null,
|
||||
selectedRepository: null,
|
||||
selectedRepositoryProvider: null,
|
||||
replayJson: null,
|
||||
};
|
||||
|
||||
export const selectedFilesSlice = createSlice({
|
||||
name: "initialQuery",
|
||||
initialState,
|
||||
reducers: {
|
||||
addFile(state, action: PayloadAction<string>) {
|
||||
state.files.push(action.payload);
|
||||
},
|
||||
removeFile(state, action: PayloadAction<number>) {
|
||||
state.files.splice(action.payload, 1);
|
||||
},
|
||||
clearFiles(state) {
|
||||
state.files = [];
|
||||
},
|
||||
setInitialPrompt(state, action: PayloadAction<string>) {
|
||||
state.initialPrompt = action.payload;
|
||||
},
|
||||
clearInitialPrompt(state) {
|
||||
state.initialPrompt = null;
|
||||
},
|
||||
setSelectedRepository(state, action: PayloadAction<GitRepository | null>) {
|
||||
state.selectedRepository = action.payload;
|
||||
},
|
||||
clearSelectedRepository(state) {
|
||||
state.selectedRepository = null;
|
||||
},
|
||||
setReplayJson(state, action: PayloadAction<string | null>) {
|
||||
state.replayJson = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
addFile,
|
||||
removeFile,
|
||||
clearFiles,
|
||||
setInitialPrompt,
|
||||
clearInitialPrompt,
|
||||
setSelectedRepository,
|
||||
clearSelectedRepository,
|
||||
setReplayJson,
|
||||
} = selectedFilesSlice.actions;
|
||||
export default selectedFilesSlice.reducer;
|
||||
@@ -2,7 +2,6 @@ import { combineReducers, configureStore } from "@reduxjs/toolkit";
|
||||
import agentReducer from "./state/agent-slice";
|
||||
import browserReducer from "./state/browser-slice";
|
||||
import fileStateReducer from "./state/file-state-slice";
|
||||
import initialQueryReducer from "./state/initial-query-slice";
|
||||
import commandReducer from "./state/command-slice";
|
||||
import { jupyterReducer } from "./state/jupyter-slice";
|
||||
import securityAnalyzerReducer from "./state/security-analyzer-slice";
|
||||
@@ -14,7 +13,6 @@ import eventMessageReducer from "./state/event-message-slice";
|
||||
|
||||
export const rootReducer = combineReducers({
|
||||
fileState: fileStateReducer,
|
||||
initialQuery: initialQueryReducer,
|
||||
browser: browserReducer,
|
||||
cmd: commandReducer,
|
||||
agent: agentReducer,
|
||||
|
||||
85
frontend/src/stores/initial-query-store.ts
Normal file
85
frontend/src/stores/initial-query-store.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { create } from "zustand";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { GitRepository } from "#/types/git";
|
||||
|
||||
interface InitialQueryState {
|
||||
files: string[]; // base64 encoded images
|
||||
initialPrompt: string | null;
|
||||
selectedRepository: GitRepository | null;
|
||||
selectedRepositoryProvider: Provider | null;
|
||||
replayJson: string | null;
|
||||
}
|
||||
|
||||
interface InitialQueryActions {
|
||||
addFile: (file: string) => void;
|
||||
removeFile: (index: number) => void;
|
||||
clearFiles: () => void;
|
||||
setInitialPrompt: (prompt: string) => void;
|
||||
clearInitialPrompt: () => void;
|
||||
setSelectedRepository: (repository: GitRepository | null) => void;
|
||||
clearSelectedRepository: () => void;
|
||||
setSelectedRepositoryProvider: (provider: Provider | null) => void;
|
||||
setReplayJson: (replayJson: string | null) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
type InitialQueryStore = InitialQueryState & InitialQueryActions;
|
||||
|
||||
const initialState: InitialQueryState = {
|
||||
files: [],
|
||||
initialPrompt: null,
|
||||
selectedRepository: null,
|
||||
selectedRepositoryProvider: null,
|
||||
replayJson: null,
|
||||
};
|
||||
|
||||
export const useInitialQueryStore = create<InitialQueryStore>((set) => ({
|
||||
...initialState,
|
||||
|
||||
addFile: (file: string) =>
|
||||
set((state) => ({
|
||||
files: [...state.files, file],
|
||||
})),
|
||||
|
||||
removeFile: (index: number) =>
|
||||
set((state) => ({
|
||||
files: state.files.filter((_, i) => i !== index),
|
||||
})),
|
||||
|
||||
clearFiles: () =>
|
||||
set(() => ({
|
||||
files: [],
|
||||
})),
|
||||
|
||||
setInitialPrompt: (prompt: string) =>
|
||||
set(() => ({
|
||||
initialPrompt: prompt,
|
||||
})),
|
||||
|
||||
clearInitialPrompt: () =>
|
||||
set(() => ({
|
||||
initialPrompt: null,
|
||||
})),
|
||||
|
||||
setSelectedRepository: (repository: GitRepository | null) =>
|
||||
set(() => ({
|
||||
selectedRepository: repository,
|
||||
})),
|
||||
|
||||
clearSelectedRepository: () =>
|
||||
set(() => ({
|
||||
selectedRepository: null,
|
||||
})),
|
||||
|
||||
setSelectedRepositoryProvider: (provider: Provider | null) =>
|
||||
set(() => ({
|
||||
selectedRepositoryProvider: provider,
|
||||
})),
|
||||
|
||||
setReplayJson: (replayJson: string | null) =>
|
||||
set(() => ({
|
||||
replayJson,
|
||||
})),
|
||||
|
||||
reset: () => set(() => initialState),
|
||||
}));
|
||||
Reference in New Issue
Block a user