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-react": "^7.12.7",
"@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/parser": "^4.8.1",
"babel-loader": "^8.2.2",
@ -49,14 +46,15 @@
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.57",
"@reduxjs/toolkit": "^1.5.0",
"@types/axios": "^0.14.0",
"@types/react-google-recaptcha": "^2.1.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"axios": "^0.14.0",
"react": "^17.0.0",
"react-dom": "^17.0.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-router": "^5.1.8",
"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 { 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 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 LoginPage from './components/login-page';
import MapsPage from './components/maps-page';
import store from "./store"
import {
Route,
Switch,
Redirect,
BrowserRouter as Router,
} from 'react-router-dom';
import store from "./store";
import { ForgotPasswordPage } from './components/forgot-password-page';
import { Provider } from 'react-redux';

View File

@ -2,7 +2,7 @@ import React, { useEffect } from 'react'
import { FormattedMessage, useIntl } from 'react-intl'
import { Link } from 'react-router-dom'
import {PageContent} from '../../theme/global-style';
import { PageContent } from '../../theme/global-style';
import FormErrorDialog from '../form-error-dialog'
@ -13,7 +13,7 @@ import SubmitButton from '../submit-button'
const ConfigStatusMessage = (props: any) => {
const enabled = props.enabled
let result = null;
let result;
if (enabled === true) {
result = (<div className="db-warn-msg">
<p>
@ -21,7 +21,7 @@ const ConfigStatusMessage = (props: any) => {
</p>
</div>);
}
return result;
return result ? result : null;
}
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 Dialog from '@material-ui/core/Dialog';
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 { FormControl, TextField } from '@material-ui/core';
import { Alert, AlertTitle } from '@material-ui/lab';
import { useDispatch, useSelector } from 'react-redux';
import { useSelector } from 'react-redux';
import {
allMaps,
remove,
rename
} from '../../reducers/mapsListSlice'
import { Description } from '@material-ui/icons';
activeInstance,
} from '../../reducers/serviceSlice'
import { useMutation, useQuery, useQueryClient } from 'react-query';
type DialogProps = {
@ -30,17 +28,32 @@ export type BasicMapInfo = {
}
function DeleteDialog(props: DialogProps) {
const dispatch = useDispatch()
const service: Service = useSelector(activeInstance);
const queryClient = useQueryClient();
const mapId = props.mapId;
const mapInfo: MapInfo | undefined = useSelector(allMaps).
find(m => m.id == mapId);
const mutation = useMutation((id: number) => service.deleteMap(id),
{
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 => {
if (action == 'accept' && mapInfo) {
dispatch(remove({ id: mapId }))
mutation.mutate(mapId);
}
props.onClose();
};
return (
@ -78,22 +91,21 @@ function RenameDialog(props: DialogProps) {
const defaultModel: RenameModel = { name: '', description: '', id: -1 };
const [model, setModel] = React.useState<RenameModel>(defaultModel);
const [errorInfo, setErroInfo] = React.useState<ErrorInfo>();
const dispatch = useDispatch()
const intl = useIntl();
useEffect(() => {
const mapId: number = props.mapId;
if (mapId != -1) {
const mapInfo: MapInfo | undefined = useSelector(allMaps)
.find(m => m.id == props.mapId);
// useEffect(() => {
// const mapId: number = props.mapId;
// if (mapId != -1) {
// const mapInfo: MapInfo | undefined = useSelector(activeInstance)
// .find(m => m.id == props.mapId);
if (!mapInfo) {
throw "Please, reflesh the page.";
}
// if (!mapInfo) {
// throw "Please, reflesh the page.";
// }
setModel({ ...mapInfo });
}
}, []);
// setModel({ ...mapInfo });
// }
// }, []);
const handleOnClose = (): void => {
// Clean Up ...
@ -110,7 +122,7 @@ function RenameDialog(props: DialogProps) {
// Fire rename ...
const mapId: number = props.mapId;
try {
dispatch(rename({ id: mapId, name: model.name, description: model.description }))
// dispatch(rename({ id: mapId, name: model.name, description: model.description }))
handleOnClose();
} catch (errorInfo) {

View File

@ -24,7 +24,11 @@ import { CSSProperties } from 'react';
import MapActionMenu, { ActionType } from './MapActionMenu';
import ActionDialog, { DialogType } from './ActionDialog';
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) {
if (b[orderBy] < a[orderBy]) {
@ -41,7 +45,7 @@ type Order = 'asc' | 'desc';
function getComparator<Key extends keyof any>(
order: Order,
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'
? (a, b) => descendingComparator(a, b, orderBy)
: (a, b) => -descendingComparator(a, b, orderBy);
@ -211,14 +215,22 @@ type ActionPanelState = {
mapId: number
}
function EnhancedTable() {
const EnhancedTable = () => {
const classes = useStyles();
const [order, setOrder] = React.useState<Order>('asc');
const [orderBy, setOrderBy] = React.useState<keyof MapInfo>('modified');
const [selected, setSelected] = React.useState<number[]>([]);
const [page, setPage] = React.useState(0);
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);
type ActiveDialog = {
@ -322,7 +334,7 @@ function EnhancedTable() {
rowCount={mapsInfo.length}
/>
<TableBody>
{stableSort(mapsInfo, getComparator(order, orderBy))
{isLoading ? (<TableRow></TableRow>) : stableSort(mapsInfo, getComparator(order, orderBy))
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
.map((row: MapInfo) => {
const isItemSelected = isSelected(row.id);
@ -391,11 +403,14 @@ function EnhancedTable() {
</Paper>
{/* 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>
);
}
const queryClient = new QueryClient();
const MapsPage = () => {
useEffect(() => {
@ -403,6 +418,7 @@ const MapsPage = () => {
}, []);
return (
<QueryClientProvider client={queryClient}>
<PageContainer>
<HeaderArea>
<h2>Header</h2>
@ -411,12 +427,12 @@ const MapsPage = () => {
<h1> Nav </h1>
</NavArea>
<MapsListArea>
<EnhancedTable/>
<EnhancedTable />
</MapsListArea>
</PageContainer>
</QueryClientProvider>
);
}
export default MapsPage;

View File

@ -1,17 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './app';
import { BrowserRouter as Router } from 'react-router-dom';
import axios from 'axios';
async function bootstrapApplication() {
ReactDOM.render(
<Router>
<App/>
</Router>,
<App />,
document.getElementById('root') as HTMLElement
)
}
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'
export type NewUser = {
@ -47,10 +46,30 @@ interface Service {
class RestService implements Service {
private baseUrl: string;
private authFailed: () => void
private maps: MapInfo[] = [];
constructor(baseUrl: string, authFailed: () => void) {
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> {
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();
}
@ -87,27 +107,8 @@ class RestService implements Service {
return new Promise(handler);
}
async fetchAllMaps(): Promise<MapInfo[]> {
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);
fetchAllMaps(): Promise<MapInfo[]> {
return Promise.resolve(this.maps);
}
resetPassword(email: string): Promise<void> {

View File

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

View File

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

View File

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