Migrating from Redux to RTK-query

This commit is contained in:
2023-10-20 11:54:07 -04:00
parent 206c2eeb4b
commit 4ea9086e3f
15 changed files with 204 additions and 201 deletions

View File

@@ -20,6 +20,7 @@
"@dnd-kit/utilities": "^3.2.1",
"@fortawesome/fontawesome-free": "^6.3.0",
"@redux-devtools/extension": "^3.2.5",
"@reduxjs/toolkit": "^1.9.7",
"@rollup/plugin-node-resolve": "^15.0.1",
"@tanstack/react-table": "^8.9.3",
"@types/mime-types": "^2.1.0",

View File

@@ -4,7 +4,7 @@ import {
SETTINGS_CALL_IN_PROGRESS,
SETTINGS_DB_FLUSH_SUCCESS,
SETTINGS_QBITTORRENT_TORRENTS_LIST_FETCHED,
} from "../constants/action-types";
} from "../reducers/settings.reducer";
import {
LIBRARY_SERVICE_BASE_URI,
SETTINGS_SERVICE_BASE_URI,
@@ -72,20 +72,19 @@ export const flushDb = () => async (dispatch) => {
};
export const getQBitTorrentClientInfo = (hostInfo) => async (dispatch) => {
const foo = await axios.request({
await axios.request({
url: `${QBITTORRENT_SERVICE_BASE_URI}/connect`,
method: "POST",
data: hostInfo,
});
const bar = await axios.request({
const qBittorrentClientInfo = await axios.request({
url: `${QBITTORRENT_SERVICE_BASE_URI}/getClientInfo`,
method: "GET",
});
console.log(bar);
dispatch({
type: SETTINGS_QBITTORRENT_TORRENTS_LIST_FETCHED,
data: bar.data,
data: qBittorrentClientInfo.data,
});
};

View File

@@ -119,7 +119,6 @@ export const App = (): ReactElement => {
<AirDCPPSocketContextProvider>
<div>
<AirDCPPSocketComponent />
<Navbar />
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/import" element={<Import path={"./comics"} />} />

View File

@@ -16,110 +16,50 @@ import Header from "../shared/Header";
export const Dashboard = (): ReactElement => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchVolumeGroups());
dispatch(
getComicBooks({
paginationOptions: {
page: 0,
limit: 5,
sort: { updatedAt: "-1" },
},
predicate: { "acquisition.source.wanted": false },
comicStatus: "recent",
}),
);
dispatch(
getComicBooks({
paginationOptions: {
page: 0,
limit: 5,
sort: { updatedAt: "-1" },
},
predicate: { "acquisition.source.wanted": true },
comicStatus: "wanted",
}),
);
dispatch(getLibraryStatistics());
}, []);
const recentComics = useSelector(
(state: RootState) => state.fileOps.recentComics,
);
const wantedComics = useSelector(
(state: RootState) => state.fileOps.wantedComics,
);
const volumeGroups = useSelector(
(state: RootState) => state.fileOps.comicVolumeGroups,
);
const libraryStatistics = useSelector(
(state: RootState) => state.comicInfo.libraryStatistics,
);
// useEffect(() => {
// dispatch(fetchVolumeGroups());
// dispatch(
// getComicBooks({
// paginationOptions: {
// page: 0,
// limit: 5,
// sort: { updatedAt: "-1" },
// },
// predicate: { "acquisition.source.wanted": false },
// comicStatus: "recent",
// }),
// );
// dispatch(
// getComicBooks({
// paginationOptions: {
// page: 0,
// limit: 5,
// sort: { updatedAt: "-1" },
// },
// predicate: { "acquisition.source.wanted": true },
// comicStatus: "wanted",
// }),
// );
// dispatch(getLibraryStatistics());
// }, []);
//
// const recentComics = useSelector(
// (state: RootState) => state.fileOps.recentComics,
// );
// const wantedComics = useSelector(
// (state: RootState) => state.fileOps.wantedComics,
// );
// const volumeGroups = useSelector(
// (state: RootState) => state.fileOps.comicVolumeGroups,
// );
//
// const libraryStatistics = useSelector(
// (state: RootState) => state.comicInfo.libraryStatistics,
// );
return (
<div className="container">
<section className="section">
<h1 className="title">Dashboard</h1>
{!isEmpty(recentComics) ? (
<>
{/* Pull List */}
<PullList issues={recentComics} />
<>
<div className="content mt-6">
<Header
headerContent="Import Activity"
subHeaderContent="Results aggregated from the last import"
iconClassNames="fa-solid fa-file-invoice mr-2"
/>
</div>
<table className="table">
<thead>
<tr>
<th>
<abbr title="Position">Pos</abbr>
</th>
<th>Team</th>
<th>
<abbr title="Played">Pld</abbr>
</th>
</tr>
</thead>
<tbody>
<tr>
<th>1</th>
<td>38</td>
</tr>
</tbody>
</table>
</>
{/* Stats */}
{!isEmpty(libraryStatistics) && (
<LibraryStatistics stats={libraryStatistics} />
)}
{/* Wanted comics */}
{!isEmpty(wantedComics) && (
<WantedComicsList comics={wantedComics} />
)}
{/* Recent imports */}
<RecentlyImported comicBookCovers={recentComics} />
{/* Volumes */}
{!isEmpty(volumeGroups) && (
<VolumeGroups volumeGroups={volumeGroups} />
)}
</>
) : (
<ZeroState
header={"Set the source directory"}
message={
"No comics were found! Please point ThreeTwo! to a directory..."
}
/>
)}
</section>
</div>
);

View File

@@ -34,7 +34,11 @@ export const AirDCPPSettingsForm = (): ReactElement => {
return (
<>
<ConnectionForm initialData={initFormData} submitHandler={onSubmit} />
<ConnectionForm
initialData={initFormData}
submitHandler={onSubmit}
formHeading={"Configure AirDC++"}
/>
{!isEmpty(airDCPPSettings.airDCPPState.socketConnectionInformation) ? (
<AirDCPPSettingsConfirmation

View File

@@ -1,27 +1,14 @@
import React, { ReactElement, useCallback, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useAppDispatch, useAppSelector } from "../../../hooks/store";
import { getQBitTorrentClientInfo } from "../../../actions/settings.actions";
import { saveSettings } from "../../../actions/settings.actions";
import { ConnectionForm } from "../../shared/ConnectionForm/ConnectionForm";
import { useGetAllSettingsQuery } from "../../../services/settings.api";
import { isUndefined } from "lodash";
import { useConnectToQBittorrentClientQuery } from "../../../services/torrents.api";
export const QbittorrentConnectionForm = (): ReactElement => {
const dispatch = useDispatch();
const torrents = useSelector(
(state: RootState) => state.settings.torrentsList,
);
const qBittorrentSettings = useSelector((state: RootState) => {
if (!isUndefined(state.settings.data.bittorrent)) {
return state.settings.data.bittorrent.client.host;
}
});
useEffect(() => {
if (!isUndefined(qBittorrentSettings)) {
dispatch(getQBitTorrentClientInfo(qBittorrentSettings));
}
}, []);
const dispatch = useAppDispatch();
const onSubmit = useCallback(async (values) => {
try {
@@ -31,15 +18,7 @@ export const QbittorrentConnectionForm = (): ReactElement => {
}
}, []);
return (
<>
<ConnectionForm
initialData={qBittorrentSettings}
submitHandler={onSubmit}
/>
<pre> {torrents && JSON.stringify(torrents, null, 4)} </pre>
</>
);
return <></>;
};
export default QbittorrentConnectionForm;

View File

@@ -6,6 +6,7 @@ import { isEmpty } from "lodash";
export const ConnectionForm = ({
initialData,
submitHandler,
formHeading,
}): ReactElement => {
return (
<>
@@ -14,8 +15,8 @@ export const ConnectionForm = ({
initialValues={initialData}
render={({ handleSubmit }) => (
<form onSubmit={handleSubmit}>
<h2>Configure AirDC++</h2>
<label className="label">AirDC++ Hostname</label>
<h2>{formHeading}</h2>
<label className="label">Hostname</label>
<div className="field has-addons">
<p className="control">
<span className="select">

View File

@@ -0,0 +1,6 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { RootState, AppDispatch } from "../store";
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

View File

@@ -1,11 +1,11 @@
import comicinfoReducer from "../reducers/comicinfo.reducer";
import fileOpsReducer from "../reducers/fileops.reducer";
import airdcppReducer from "../reducers/airdcpp.reducer";
import settingsReducer from "../reducers/settings.reducer";
// import settingsReducer from "../reducers/settings.reducer";
export const reducers = {
comicInfo: comicinfoReducer,
fileOps: fileOpsReducer,
airdcpp: airdcppReducer,
settings: settingsReducer,
// settings: settingsReducer,
};

View File

@@ -1,57 +1,67 @@
import {
SETTINGS_CALL_FAILED,
SETTINGS_OBJECT_FETCHED,
SETTINGS_OBJECT_DELETED,
SETTINGS_CALL_IN_PROGRESS,
SETTINGS_DB_FLUSH_SUCCESS,
SETTINGS_QBITTORRENT_TORRENTS_LIST_FETCHED,
} from "../constants/action-types";
const initialState = {
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import { RootState } from "../store";
import { isUndefined } from "lodash";
import { SETTINGS_SERVICE_BASE_URI } from "../constants/endpoints";
export interface InitialState {
data: object;
inProgress: boolean;
dbFlushed: boolean;
torrentsList: Array<any>;
}
const initialState: InitialState = {
data: {},
inProgress: false,
DbFlushed: false,
dbFlushed: false,
torrentsList: [],
};
function settingsReducer(state = initialState, action) {
switch (action.type) {
case SETTINGS_CALL_IN_PROGRESS:
return {
...state,
inProgress: true,
};
export const settingsSlice = createSlice({
name: "settings",
initialState,
reducers: {
SETTINGS_CALL_IN_PROGRESS: (state) => {
state.inProgress = true;
},
case SETTINGS_OBJECT_FETCHED:
return {
...state,
data: action.data,
inProgress: false,
};
SETTINGS_OBJECT_FETCHED: (state, action) => {
state.data = action.payload;
state.inProgress = false;
},
case SETTINGS_OBJECT_DELETED:
return {
...state,
data: action.data,
inProgress: false,
};
SETTINGS_OBJECT_DELETED: (state, action) => {
state.data = action.payload;
state.inProgress = false;
},
case SETTINGS_DB_FLUSH_SUCCESS:
SETTINGS_DB_FLUSH_SUCCESS: (state, action) => {
state.dbFlushed = action.payload;
state.inProgress = false;
},
SETTINGS_QBITTORRENT_TORRENTS_LIST_FETCHED: (state, action) => {
console.log(state);
return {
...state,
DbFlushed: action.data,
inProgress: false,
};
case SETTINGS_QBITTORRENT_TORRENTS_LIST_FETCHED:
return {
...state,
torrentsList: action.data,
}
console.log(action);
state.torrentsList = action.payload;
},
},
});
default:
return { ...state };
export const {
SETTINGS_CALL_IN_PROGRESS,
SETTINGS_OBJECT_FETCHED,
SETTINGS_OBJECT_DELETED,
SETTINGS_DB_FLUSH_SUCCESS,
SETTINGS_QBITTORRENT_TORRENTS_LIST_FETCHED,
} = settingsSlice.actions;
// Other code such as selectors can use the imported `RootState` type
export const torrentsList = (state: RootState) => state.settings.torrentsList;
export const qBittorrentSettings = (state: RootState) => {
console.log(state);
if (!isUndefined(state.settings?.data?.bittorrent)) {
return state.settings?.data?.bittorrent.client.host;
}
}
export default settingsReducer;
};
export default settingsSlice.reducer;

View File

View File

@@ -0,0 +1,7 @@
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
// initialize an empty api service that we'll inject endpoints into later as needed
export const emptySplitApi = createApi({
baseQuery: fetchBaseQuery({ baseUrl: "http://" }),
endpoints: () => ({}),
});

View File

@@ -0,0 +1,19 @@
import { emptySplitApi } from "./empty.api";
import { useConnectToQBittorrentClientQuery } from "./torrents.api";
export const settingsApi = emptySplitApi.injectEndpoints({
endpoints: (builder) => ({
getAllSettings: builder.query({
query: () => "localhost:3000/api/settings/getAllSettings",
}),
getQBitTorrentClientInfo: builder.query({
queryFn: async () => {
try {
} catch (err) {}
},
}),
}),
overrideExisting: false,
});
export const { useGetAllSettingsQuery } = settingsApi;

View File

@@ -0,0 +1,27 @@
import { emptySplitApi } from "./empty.api";
export const torrentsApi = emptySplitApi.injectEndpoints({
endpoints: (builder) => ({
connectToQBittorrentClient: builder.query({
queryFn: async (_arg, _queryApi, _extraOptions, fetchWithBQ) => {
const qBittorrentHostInfo = await fetchWithBQ(
"localhost:3000/api/settings/getAllSettings",
);
await fetchWithBQ({
url: "localhost:3060/api/qbittorrent/connect",
method: "POST",
data: qBittorrentHostInfo,
});
console.log(qBittorrentHostInfo);
return {
url: "",
method: "GET",
data: qBittorrentHostInfo,
};
},
}),
}),
overrideExisting: false,
});
export const { useConnectToQBittorrentClientQuery } = torrentsApi;

View File

@@ -1,12 +1,12 @@
import { createStore, combineReducers, applyMiddleware } from "redux";
import { createHashHistory } from "history";
import { composeWithDevTools } from "@redux-devtools/extension";
import thunk from "redux-thunk";
import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit";
import { createReduxHistoryContext } from "redux-first-history";
import { reducers } from "../reducers/index";
import socketIoMiddleware from "redux-socket.io-middleware";
import socketIOMiddleware from "../shared/middleware/SocketIOMiddleware";
import socketIOConnectionInstance from "../shared/socket.io/instance";
import settingsReducer from "../reducers/settings.reducer";
import { settingsApi } from "../services/settings.api";
const customSocketIOMiddleware = socketIOMiddleware(socketIOConnectionInstance);
@@ -15,19 +15,30 @@ const { createReduxHistory, routerMiddleware, routerReducer } =
history: createHashHistory(),
});
export const store = createStore(
combineReducers({
router: routerReducer,
...reducers,
}),
composeWithDevTools(
applyMiddleware(
socketIoMiddleware(socketIOConnectionInstance),
customSocketIOMiddleware,
thunk,
routerMiddleware,
),
// window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
),
);
const rootReducer = (history) => ({
settings: settingsReducer,
[settingsApi.reducerPath]: settingsApi.reducer,
router: routerReducer,
});
const preloadedState = {};
export const store = configureStore({
middleware: [
socketIoMiddleware(socketIOConnectionInstance),
customSocketIOMiddleware,
thunk,
routerMiddleware,
settingsApi.middleware,
],
reducer: rootReducer(createHashHistory()),
preloadedState,
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;
export const history = createReduxHistory(store);