Introduce react-query

This commit is contained in:
Paulo Gustavo Veiga 2020-12-25 21:39:54 -08:00
parent 60fd57b094
commit fbb2b11175
12 changed files with 182 additions and 220 deletions

View File

@ -17,9 +17,6 @@
"@babel/preset-env": "^7.12.7", "@babel/preset-env": "^7.12.7",
"@babel/preset-react": "^7.12.7", "@babel/preset-react": "^7.12.7",
"@formatjs/cli": "^2.13.15", "@formatjs/cli": "^2.13.15",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-router-dom": "^5.1.6",
"@typescript-eslint/eslint-plugin": "^4.8.1", "@typescript-eslint/eslint-plugin": "^4.8.1",
"@typescript-eslint/parser": "^4.8.1", "@typescript-eslint/parser": "^4.8.1",
"babel-loader": "^8.2.2", "babel-loader": "^8.2.2",
@ -49,14 +46,15 @@
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.57", "@material-ui/lab": "^4.0.0-alpha.57",
"@reduxjs/toolkit": "^1.5.0", "@reduxjs/toolkit": "^1.5.0",
"@types/axios": "^0.14.0", "axios": "^0.14.0",
"@types/react-google-recaptcha": "^2.1.0", "react": "^17.0.0",
"react": "^17.0.1", "react-dom": "^17.0.0",
"react-dom": "^17.0.1",
"react-google-recaptcha": "^2.1.0", "react-google-recaptcha": "^2.1.0",
"react-intl": "^5.10.6", "react-intl": "^3.0.0",
"react-query": "^3.5.5",
"react-redux": "^7.2.2", "react-redux": "^7.2.2",
"react-router": "^5.1.8",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"styled-components": "^5.2.1" "styled-components": "^5.1.7"
} }
} }

View File

@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { IntlProvider } from 'react-intl' import { IntlProvider } from 'react-intl';
import { Route, Switch, Redirect, BrowserRouter as Router } from 'react-router-dom';
import { GlobalStyle } from './theme/global-style'; import { GlobalStyle } from './theme/global-style';
import RegistrationSuccessPage from './components/registration-success-page'; import RegistrationSuccessPage from './components/registration-success-page';
@ -7,14 +8,7 @@ import ForgotPasswordSuccessPage from './components/forgot-password-success-page
import RegistationPage from './components/registration-page'; import RegistationPage from './components/registration-page';
import LoginPage from './components/login-page'; import LoginPage from './components/login-page';
import MapsPage from './components/maps-page'; import MapsPage from './components/maps-page';
import store from "./store" import store from "./store";
import {
Route,
Switch,
Redirect,
BrowserRouter as Router,
} from 'react-router-dom';
import { ForgotPasswordPage } from './components/forgot-password-page'; import { ForgotPasswordPage } from './components/forgot-password-page';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';

View File

@ -2,7 +2,7 @@ import React, { useEffect } from 'react'
import { FormattedMessage, useIntl } from 'react-intl' import { FormattedMessage, useIntl } from 'react-intl'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import {PageContent} from '../../theme/global-style'; import { PageContent } from '../../theme/global-style';
import FormErrorDialog from '../form-error-dialog' import FormErrorDialog from '../form-error-dialog'
@ -13,7 +13,7 @@ import SubmitButton from '../submit-button'
const ConfigStatusMessage = (props: any) => { const ConfigStatusMessage = (props: any) => {
const enabled = props.enabled const enabled = props.enabled
let result = null; let result;
if (enabled === true) { if (enabled === true) {
result = (<div className="db-warn-msg"> result = (<div className="db-warn-msg">
<p> <p>
@ -21,7 +21,7 @@ const ConfigStatusMessage = (props: any) => {
</p> </p>
</div>); </div>);
} }
return result; return result ? result : null;
} }
const LoginError = () => { const LoginError = () => {

View File

@ -1,4 +1,4 @@
import React, { useEffect } from 'react'; import React from 'react';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog'; import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions'; import DialogActions from '@material-ui/core/DialogActions';
@ -9,13 +9,11 @@ import { FormattedMessage, useIntl } from 'react-intl';
import { ErrorInfo, MapInfo, Service } from '../../services/Service'; import { ErrorInfo, MapInfo, Service } from '../../services/Service';
import { FormControl, TextField } from '@material-ui/core'; import { FormControl, TextField } from '@material-ui/core';
import { Alert, AlertTitle } from '@material-ui/lab'; import { Alert, AlertTitle } from '@material-ui/lab';
import { useDispatch, useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { import {
allMaps, activeInstance,
remove, } from '../../reducers/serviceSlice'
rename import { useMutation, useQuery, useQueryClient } from 'react-query';
} from '../../reducers/mapsListSlice'
import { Description } from '@material-ui/icons';
type DialogProps = { type DialogProps = {
@ -30,17 +28,32 @@ export type BasicMapInfo = {
} }
function DeleteDialog(props: DialogProps) { function DeleteDialog(props: DialogProps) {
const dispatch = useDispatch() const service: Service = useSelector(activeInstance);
const queryClient = useQueryClient();
const mapId = props.mapId; const mapId = props.mapId;
const mapInfo: MapInfo | undefined = useSelector(allMaps). const mutation = useMutation((id: number) => service.deleteMap(id),
find(m => m.id == mapId); {
onSuccess: () => {
queryClient.invalidateQueries()
props.onClose();
}
}
);
const { isLoading, error, data } = useQuery<unknown, ErrorInfo, MapInfo[]>('maps', () => {
return service.fetchAllMaps();
});
let mapInfo: MapInfo | undefined = undefined;
if (data) {
mapInfo = data.find((m) => m.id == mapId);
}
const handleOnClose = (action: 'accept' | undefined): void => { const handleOnClose = (action: 'accept' | undefined): void => {
if (action == 'accept' && mapInfo) { if (action == 'accept' && mapInfo) {
dispatch(remove({ id: mapId })) mutation.mutate(mapId);
} }
props.onClose();
}; };
return ( return (
@ -78,22 +91,21 @@ function RenameDialog(props: DialogProps) {
const defaultModel: RenameModel = { name: '', description: '', id: -1 }; const defaultModel: RenameModel = { name: '', description: '', id: -1 };
const [model, setModel] = React.useState<RenameModel>(defaultModel); const [model, setModel] = React.useState<RenameModel>(defaultModel);
const [errorInfo, setErroInfo] = React.useState<ErrorInfo>(); const [errorInfo, setErroInfo] = React.useState<ErrorInfo>();
const dispatch = useDispatch()
const intl = useIntl(); const intl = useIntl();
useEffect(() => { // useEffect(() => {
const mapId: number = props.mapId; // const mapId: number = props.mapId;
if (mapId != -1) { // if (mapId != -1) {
const mapInfo: MapInfo | undefined = useSelector(allMaps) // const mapInfo: MapInfo | undefined = useSelector(activeInstance)
.find(m => m.id == props.mapId); // .find(m => m.id == props.mapId);
if (!mapInfo) { // if (!mapInfo) {
throw "Please, reflesh the page."; // throw "Please, reflesh the page.";
} // }
setModel({ ...mapInfo }); // setModel({ ...mapInfo });
} // }
}, []); // }, []);
const handleOnClose = (): void => { const handleOnClose = (): void => {
// Clean Up ... // Clean Up ...
@ -110,7 +122,7 @@ function RenameDialog(props: DialogProps) {
// Fire rename ... // Fire rename ...
const mapId: number = props.mapId; const mapId: number = props.mapId;
try { try {
dispatch(rename({ id: mapId, name: model.name, description: model.description })) // dispatch(rename({ id: mapId, name: model.name, description: model.description }))
handleOnClose(); handleOnClose();
} catch (errorInfo) { } catch (errorInfo) {

View File

@ -24,7 +24,11 @@ import { CSSProperties } from 'react';
import MapActionMenu, { ActionType } from './MapActionMenu'; import MapActionMenu, { ActionType } from './MapActionMenu';
import ActionDialog, { DialogType } from './ActionDialog'; import ActionDialog, { DialogType } from './ActionDialog';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { allMaps, MapInfo } from '../../reducers/mapsListSlice'; import { activeInstance } from '../../reducers/serviceSlice';
import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
import { ErrorInfo, MapInfo, Service } from '../../services/Service';
function descendingComparator<T>(a: T, b: T, orderBy: keyof T) { function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
if (b[orderBy] < a[orderBy]) { if (b[orderBy] < a[orderBy]) {
@ -41,7 +45,7 @@ type Order = 'asc' | 'desc';
function getComparator<Key extends keyof any>( function getComparator<Key extends keyof any>(
order: Order, order: Order,
orderBy: Key, orderBy: Key,
): (a: { [key in Key]: number | string | boolean | string[] }, b: { [key in Key]: number | string | string[] | boolean }) => number { ): (a: { [key in Key]: number | string | boolean | string[] | undefined }, b: { [key in Key]: number | string | string[] | boolean }) => number {
return order === 'desc' return order === 'desc'
? (a, b) => descendingComparator(a, b, orderBy) ? (a, b) => descendingComparator(a, b, orderBy)
: (a, b) => -descendingComparator(a, b, orderBy); : (a, b) => -descendingComparator(a, b, orderBy);
@ -211,14 +215,22 @@ type ActionPanelState = {
mapId: number mapId: number
} }
function EnhancedTable() { const EnhancedTable = () => {
const classes = useStyles(); const classes = useStyles();
const [order, setOrder] = React.useState<Order>('asc'); const [order, setOrder] = React.useState<Order>('asc');
const [orderBy, setOrderBy] = React.useState<keyof MapInfo>('modified'); const [orderBy, setOrderBy] = React.useState<keyof MapInfo>('modified');
const [selected, setSelected] = React.useState<number[]>([]); const [selected, setSelected] = React.useState<number[]>([]);
const [page, setPage] = React.useState(0); const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(5); const [rowsPerPage, setRowsPerPage] = React.useState(5);
const mapsInfo: MapInfo[] = useSelector(allMaps); const service: Service = useSelector(activeInstance);
const { isLoading, error, data } = useQuery<unknown, ErrorInfo, MapInfo[]>('maps', async () => {
const result = await service.fetchAllMaps();
return result;
});
const mapsInfo: MapInfo[] = data ? data : [];
const [activeRowAction, setActiveRowAction] = React.useState<ActionPanelState | undefined>(undefined); const [activeRowAction, setActiveRowAction] = React.useState<ActionPanelState | undefined>(undefined);
type ActiveDialog = { type ActiveDialog = {
@ -322,7 +334,7 @@ function EnhancedTable() {
rowCount={mapsInfo.length} rowCount={mapsInfo.length}
/> />
<TableBody> <TableBody>
{stableSort(mapsInfo, getComparator(order, orderBy)) {isLoading ? (<TableRow></TableRow>) : stableSort(mapsInfo, getComparator(order, orderBy))
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
.map((row: MapInfo) => { .map((row: MapInfo) => {
const isItemSelected = isSelected(row.id); const isItemSelected = isSelected(row.id);
@ -391,11 +403,14 @@ function EnhancedTable() {
</Paper> </Paper>
{/* Action Dialog */} {/* Action Dialog */}
<ActionDialog action={activeDialog?.actionType} onClose={() => setActiveDialog(undefined)} mapId={activeDialog ? activeDialog.mapId : -1}/> <ActionDialog action={activeDialog?.actionType} onClose={() => setActiveDialog(undefined)} mapId={activeDialog ? activeDialog.mapId : -1} />
</div> </div>
); );
} }
const queryClient = new QueryClient();
const MapsPage = () => { const MapsPage = () => {
useEffect(() => { useEffect(() => {
@ -403,20 +418,21 @@ const MapsPage = () => {
}, []); }, []);
return ( return (
<PageContainer> <QueryClientProvider client={queryClient}>
<HeaderArea> <PageContainer>
<h2>Header</h2> <HeaderArea>
</HeaderArea> <h2>Header</h2>
<NavArea> </HeaderArea>
<h1> Nav </h1> <NavArea>
</NavArea> <h1> Nav </h1>
<MapsListArea> </NavArea>
<EnhancedTable/> <MapsListArea>
</MapsListArea> <EnhancedTable />
</PageContainer> </MapsListArea>
</PageContainer>
</QueryClientProvider>
); );
} }
export default MapsPage; export default MapsPage;

View File

@ -1,17 +1,13 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import App from './app'; import App from './app';
import { BrowserRouter as Router } from 'react-router-dom';
import axios from 'axios';
async function bootstrapApplication() { async function bootstrapApplication() {
ReactDOM.render( ReactDOM.render(
<Router> <App />,
<App/>
</Router>,
document.getElementById('root') as HTMLElement document.getElementById('root') as HTMLElement
) )
} }
bootstrapApplication() bootstrapApplication()

View File

@ -1,113 +0,0 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import axios from 'axios';
import { RestService, Service } from '../services/Service';
function createMapInfo(
id: number,
starred: boolean,
name: string,
labels: [string],
creator: string,
modified: number,
description: string
): MapInfo {
return { id, name, labels, creator, modified, starred, description };
}
const maps = [
createMapInfo(1, true, "El Mapa", [""], "Paulo", 67, ""),
createMapInfo(2, false, "El Mapa2", [""], "Paulo2", 67, ""),
createMapInfo(3, false, "El Mapa3", [""], "Paulo3", 67, "")
];
export type MapInfo = {
id: number;
starred: boolean;
name: string;
labels: [string];
creator: string;
modified: number;
description: string
}
interface MapsListState {
maps: MapInfo[]
}
type RutimeConfig = {
apiBaseUrl: string;
}
async function loadRuntimeConfig() {
let result: RutimeConfig | undefined;
await axios.get("runtime-config.json"
).then(response => {
// All was ok, let's sent to success page ...
result = response.data as RutimeConfig;
console.log("Dynamic configuration->" + response.data);
}).catch(e => {
console.log(e)
});
if (!result) {
// Ok, try to create a default configuration relative to the current path ...
console.log("Configuration could not be loaded, falback to default config.")
const location = window.location;
const basePath = location.protocol + "//" + location.host + "/" + location.pathname.split('/')[1]
result = {
apiBaseUrl: basePath
}
}
return result;
}
const initialState: MapsListState = { maps: maps };
const service: Service = new RestService("", () => { console.log("401 error") });
type RemovePayload = {
id: number;
}
type RenamePayload = {
id: number;
name: string;
description: string | undefined;
}
export const mapsListSlice = createSlice({
name: 'maps',
initialState: initialState,
reducers: {
remove(state, action: PayloadAction<RemovePayload>) {
const maps: MapInfo[] = state.maps as MapInfo[];
const payload = action.payload;
state.maps = maps.filter(map => map.id != payload.id);
},
rename(state, action: PayloadAction<RenamePayload>) {
let maps: MapInfo[] = state.maps as MapInfo[];
const payload = action.payload;
const mapInfo = maps.find(m => m.id == payload.id);
if (mapInfo) {
mapInfo.name = payload.name;
mapInfo.description = payload.description ? payload.description: "";
// Remove and add the new map.
maps = maps.filter(map => map.id != payload.id);
maps.push(mapInfo);
state.maps = maps;
}
}
},
});
export const allMaps = (state: any): MapInfo[] => state.mapsList.maps;
export const { remove, rename } = mapsListSlice.actions
export default mapsListSlice.reducer

View File

@ -0,0 +1,58 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import axios from 'axios';
import { ErrorInfo } from 'react';
import { RestService, Service } from '../services/Service';
type RutimeConfig = {
apiBaseUrl: string;
}
async function loadRuntimeConfig() {
let result: RutimeConfig | undefined;
await axios.get("runtime-config.json"
).then(response => {
// All was ok, let's sent to success page ...
result = response.data as RutimeConfig;
console.log("Dynamic configuration->" + response.data);
}).catch(e => {
console.log(e)
});
if (!result) {
// Ok, try to create a default configuration relative to the current path ...
console.log("Configuration could not be loaded, falback to default config.")
const location = window.location;
const basePath = location.protocol + "//" + location.host + "/" + location.pathname.split('/')[1]
result = {
apiBaseUrl: basePath
}
}
return result;
}
interface ServiceState {
instance: Service
}
const initialState: ServiceState = {
instance: new RestService("", () => { console.log("401 error") })
};
export const serviceSlice = createSlice({
name: "service",
initialState: initialState,
reducers: {
initialize(state, action: PayloadAction<void[]>) {
state.instance = new RestService("", () => { console.log("401 error") });
}
},
});
export const activeInstance = (state: any): Service => {
return state.service.instance;
}
export default serviceSlice.reducer

View File

@ -1,4 +1,3 @@
import { Description } from '@material-ui/icons'
import axios from 'axios' import axios from 'axios'
export type NewUser = { export type NewUser = {
@ -47,11 +46,31 @@ interface Service {
class RestService implements Service { class RestService implements Service {
private baseUrl: string; private baseUrl: string;
private authFailed: () => void private authFailed: () => void
private maps: MapInfo[] = [];
constructor(baseUrl: string, authFailed: () => void) { constructor(baseUrl: string, authFailed: () => void) {
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
// Remove, just for develop ....
function createMapInfo(
id: number,
starred: boolean,
name: string,
labels: [string],
creator: string,
modified: number,
description: string
): MapInfo {
return { id, name, labels, creator, modified, starred, description };
}
this.maps = [
createMapInfo(1, true, "El Mapa", [""], "Paulo", 67, ""),
createMapInfo(2, false, "El Mapa2", [""], "Paulo2", 67, ""),
createMapInfo(3, false, "El Mapa3", [""], "Paulo3", 67, "")
];
} }
loadMapInfo(id: number): Promise<BasicMapInfo> { loadMapInfo(id: number): Promise<BasicMapInfo> {
return Promise.resolve({ name: 'My Map', description: 'My Description' }); return Promise.resolve({ name: 'My Map', description: 'My Description' });
} }
@ -66,7 +85,8 @@ class RestService implements Service {
}); });
} }
async deleteMap(id: number): Promise<void> { deleteMap(id: number): Promise<void> {
this.maps = this.maps.filter(m => m.id != id);
return Promise.resolve(); return Promise.resolve();
} }
@ -87,27 +107,8 @@ class RestService implements Service {
return new Promise(handler); return new Promise(handler);
} }
async fetchAllMaps(): Promise<MapInfo[]> { fetchAllMaps(): Promise<MapInfo[]> {
return Promise.resolve(this.maps);
function createMapInfo(
id: number,
starred: boolean,
name: string,
labels: [string],
creator: string,
modified: number,
description: string
): MapInfo {
return { id, name, labels, creator, modified, starred, description};
}
const maps = [
createMapInfo(1, true, "El Mapa", [""], "Paulo", 67,""),
createMapInfo(2, false, "El Mapa2", [""], "Paulo2", 67,""),
createMapInfo(3, false, "El Mapa3", [""], "Paulo3", 67,"")
];
return Promise.resolve(maps);
} }
resetPassword(email: string): Promise<void> { resetPassword(email: string): Promise<void> {

View File

@ -1,12 +1,12 @@
import { configureStore } from '@reduxjs/toolkit'; import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
import mapsListReducer from './reducers/mapsListSlice'; import serviceReducer from './reducers/serviceSlice';
// Create Service object... // Create Service object...
const store = configureStore({ const store = configureStore({
reducer: { reducer: {
mapsList: mapsListReducer service: serviceReducer
} }
}); });
export default store; export default store;

View File

@ -2,7 +2,7 @@
"compilerOptions": { "compilerOptions": {
"outDir": "./dist/", "outDir": "./dist/",
"sourceMap": true, "sourceMap": true,
"noImplicitAny": true, "noImplicitAny": false,
"module": "commonjs", "module": "commonjs",
"target": "es5", "target": "es5",
"jsx": "react", "jsx": "react",

View File

@ -2,7 +2,7 @@
"compilerOptions": { "compilerOptions": {
"outDir": "./dist/", "outDir": "./dist/",
"sourceMap": true, "sourceMap": true,
"noImplicitAny": true, "noImplicitAny": false,
"module": "commonjs", "module": "commonjs",
"target": "es5", "target": "es5",
"jsx": "react", "jsx": "react",