Complete refactor to react query

This commit is contained in:
Paulo Gustavo Veiga 2020-12-26 19:32:41 -08:00
parent fbb2b11175
commit 503bc125c7
6 changed files with 230 additions and 183 deletions

View File

@ -1,186 +1,13 @@
import React from 'react'; import React from 'react';
import Button from '@material-ui/core/Button'; import RenameDialog from './RenameDialog';
import Dialog from '@material-ui/core/Dialog'; import DeleteDialog from './DeleteDialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
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 { useSelector } from 'react-redux';
import {
activeInstance,
} from '../../reducers/serviceSlice'
import { useMutation, useQuery, useQueryClient } from 'react-query';
type DialogProps = {
open: boolean,
mapId: number,
onClose: () => void
}
export type BasicMapInfo = { export type BasicMapInfo = {
name: string; name: string;
description: string | undefined; description: string | undefined;
} }
function DeleteDialog(props: DialogProps) {
const service: Service = useSelector(activeInstance);
const queryClient = useQueryClient();
const mapId = props.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) {
mutation.mutate(mapId);
}
};
return (
<div>
<Dialog
open={props.open}
onClose={() => handleOnClose(undefined)} >
<DialogTitle><FormattedMessage id="action.delete-title" defaultMessage="Delete" /></DialogTitle>
<DialogContent>
<Alert severity="warning">
<AlertTitle>Delete '{mapInfo?.name}'</AlertTitle>
<FormattedMessage id="action.delete-description" defaultMessage="Deleted mindmap can not be recovered. Do you want to continue ?." />
</Alert>
</DialogContent>
<DialogActions>
<Button onClick={() => handleOnClose('accept')} variant="outlined" color="primary">
<FormattedMessage id="action.delete-button" defaultMessage="Delete" />
</Button>
<Button onClick={() => handleOnClose(undefined)} variant="outlined" color="secondary" autoFocus>
<FormattedMessage id="action.cancel-button" defaultMessage="Cancel" />
</Button>
</DialogActions>
</Dialog>
</div>
);
}
export type RenameModel = {
id: number;
name: string;
description?: string;
}
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 intl = useIntl();
// 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.";
// }
// setModel({ ...mapInfo });
// }
// }, []);
const handleOnClose = (): void => {
// Clean Up ...
setModel(defaultModel);
setErroInfo(undefined);
props.onClose();
};
const handleOnSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
// Stop form submit ...
event.preventDefault();
// Fire rename ...
const mapId: number = props.mapId;
try {
// dispatch(rename({ id: mapId, name: model.name, description: model.description }))
handleOnClose();
} catch (errorInfo) {
setErroInfo(errorInfo)
}
};
const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
event.preventDefault();
// Update value ...
const name = event.target.name;
const value = event.target.value;
setModel({ ...model, [name as keyof BasicMapInfo]: value });
}
return (
<div>
<Dialog
open={props.open}
onClose={() => handleOnClose()} >
<form autoComplete="off" onSubmit={handleOnSubmit}>
<DialogTitle>
<FormattedMessage id="action.rename-title" defaultMessage="Rename" />
</DialogTitle>
<DialogContent>
<DialogContentText>
<FormattedMessage id="action.rename-description" defaultMessage="Please, update the name and description for your mindmap." />
</DialogContentText>
{Boolean(errorInfo?.msg) ? <Alert severity="error" variant="filled" hidden={!Boolean(errorInfo?.msg)}>{errorInfo?.msg}</Alert> : null}
<FormControl margin="normal" required fullWidth>
<TextField name="name" label={intl.formatMessage({ id: "action.rename-name-placeholder", defaultMessage: "Name" })}
value={model.name} onChange={handleOnChange}
error={Boolean(errorInfo?.fields?.get('name'))} helperText={errorInfo?.fields?.get('name')}
variant="filled" required={true} />
</FormControl>
<FormControl margin="normal" required fullWidth>
<TextField name="description" label={intl.formatMessage({ id: "action.rename-description-placeholder", defaultMessage: "Description" })} value={model.description} onChange={handleOnChange} variant="filled" />
</FormControl>
</DialogContent>
<DialogActions>
<Button color="primary" variant="outlined" type="submit">
<FormattedMessage id="action.rename-button" defaultMessage="Rename" />
</Button>
<Button color="secondary" variant="outlined" autoFocus onClick={handleOnClose}>
<FormattedMessage id="action.cancel-button" defaultMessage="Cancel" />
</Button>
</DialogActions>
</form>
</Dialog>
</div>
);
}
export type DialogType = 'share' | 'delete' | 'info' | 'duplicate' | 'export' | 'rename' | 'publish'; export type DialogType = 'share' | 'delete' | 'info' | 'duplicate' | 'export' | 'rename' | 'publish';
type ActionDialogProps = { type ActionDialogProps = {

View File

@ -0,0 +1,58 @@
import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from "@material-ui/core";
import { Alert, AlertTitle } from "@material-ui/lab";
import React from "react";
import { FormattedMessage } from "react-intl";
import { useMutation, useQueryClient } from "react-query";
import { useSelector } from "react-redux";
import { Service } from "../../services/Service";
import { activeInstance } from '../../reducers/serviceSlice';
import { DialogProps, fetchMapById, handleOnMutationSuccess } from "./DialogCommon";
const DeleteDialog = (props: DialogProps) => {
const service: Service = useSelector(activeInstance);
const queryClient = useQueryClient();
const mutation = useMutation((id: number) => service.deleteMap(id),
{
onSuccess: () => handleOnMutationSuccess(props.onClose, queryClient)
}
);
const mapId = props.mapId;
const handleOnClose = (action: 'accept' | undefined): void => {
if (action == 'accept') {
mutation.mutate(mapId);
} else {
props.onClose();
}
};
// Fetch map model to be rendered ...
const { map } = fetchMapById(mapId);
return (
<div>
<Dialog
open={props.open}
onClose={() => handleOnClose(undefined)} >
<DialogTitle><FormattedMessage id="action.delete-title" defaultMessage="Delete" /></DialogTitle>
<DialogContent>
<Alert severity="warning">
<AlertTitle>Delete '{map?.name}'</AlertTitle>
<FormattedMessage id="action.delete-description" defaultMessage="Deleted mindmap can not be recovered. Do you want to continue ?." />
</Alert>
</DialogContent>
<DialogActions>
<Button onClick={() => handleOnClose('accept')} variant="outlined" color="primary">
<FormattedMessage id="action.delete-button" defaultMessage="Delete" />
</Button>
<Button onClick={() => handleOnClose(undefined)} variant="outlined" color="secondary" autoFocus>
<FormattedMessage id="action.cancel-button" defaultMessage="Cancel" />
</Button>
</DialogActions>
</Dialog>
</div>
);
}
export default DeleteDialog;

View File

@ -0,0 +1,36 @@
import { QueryClient, useQuery } from "react-query";
import { useSelector } from "react-redux";
import { ErrorInfo, MapInfo, Service } from "../../services/Service";
import { activeInstance, } from '../../reducers/serviceSlice';
type MapLoadResult = {
isLoading: boolean,
error: ErrorInfo | null,
map: MapInfo | null
}
export const fetchMapById = (id: number): MapLoadResult => {
const service: Service = useSelector(activeInstance);
const { isLoading, error, data } = useQuery<unknown, ErrorInfo, MapInfo[]>('maps', () => {
return service.fetchAllMaps();
});
const result = data?.find(m => m.id == id);
const map = result ? result : null;
return { isLoading: isLoading, error: error, map: map };
}
export const handleOnMutationSuccess = (onClose: () => void, queryClient: QueryClient): void => {
queryClient.invalidateQueries('maps')
onClose();
}
export type DialogProps = {
open: boolean,
mapId: number,
onClose: () => void
}

View File

@ -0,0 +1,112 @@
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, FormControl, TextField } from "@material-ui/core";
import { Alert } from "@material-ui/lab";
import React, { useEffect } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useMutation, useQueryClient } from "react-query";
import { useSelector } from "react-redux";
import { BasicMapInfo, ErrorInfo, Service } from "../../services/Service";
import { activeInstance } from '../../reducers/serviceSlice';
import { DialogProps, fetchMapById, handleOnMutationSuccess } from "./DialogCommon";
export type RenameModel = {
id: number;
name: string;
description?: string;
}
const defaultModel: RenameModel = { name: '', description: '', id: -1 };
const RenameDialog = (props: DialogProps) => {
const service: Service = useSelector(activeInstance);
const [model, setModel] = React.useState<RenameModel>(defaultModel);
const [error, setError] = React.useState<ErrorInfo>();
const { mapId, open } = props;
const intl = useIntl();
const queryClient = useQueryClient();
const mutation = useMutation<RenameModel, ErrorInfo, RenameModel>((model: RenameModel) => {
const { id, ...rest } = model;
return service.renameMap(id, rest).then(() => model);
},
{
onSuccess: () => {
handleOnMutationSuccess(props.onClose, queryClient);
},
onError: (error) => {
setError(error);
}
}
);
const handleOnClose = (): void => {
setModel(defaultModel);
setError(undefined);
props.onClose();
};
const handleOnSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
event.preventDefault();
mutation.mutate(model);
};
const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
event.preventDefault();
const name = event.target.name;
const value = event.target.value;
setModel({ ...model, [name as keyof BasicMapInfo]: value });
}
const { map } = fetchMapById(mapId);
useEffect(() => {
if (open && map) {
setModel(map);
} else {
setModel(defaultModel);
setError(undefined);
}
}, [mapId])
return (
<div>
<Dialog
open={props.open}
onClose={() => handleOnClose()} >
<form autoComplete="off" onSubmit={handleOnSubmit}>
<DialogTitle>
<FormattedMessage id="action.rename-title" defaultMessage="Rename" />
</DialogTitle>
<DialogContent>
<DialogContentText>
<FormattedMessage id="action.rename-description" defaultMessage="Please, update the name and description for your mindmap." />
</DialogContentText>
{Boolean(error?.msg) ? <Alert severity="error" variant="filled" hidden={!Boolean(error?.msg)}>{error?.msg}</Alert> : null}
<FormControl margin="normal" required fullWidth>
<TextField name="name" label={intl.formatMessage({ id: "action.rename-name-placeholder", defaultMessage: "Name" })}
value={model.name} onChange={handleOnChange}
error={Boolean(error?.fields?.get('name'))} helperText={error?.fields?.get('name')}
variant="filled" required={true} />
</FormControl>
<FormControl margin="normal" required fullWidth>
<TextField name="description" label={intl.formatMessage({ id: "action.rename-description-placeholder", defaultMessage: "Description" })} value={model.description} onChange={handleOnChange} variant="filled" />
</FormControl>
</DialogContent>
<DialogActions>
<Button color="primary" variant="outlined" type="submit">
<FormattedMessage id="action.rename-button" defaultMessage="Rename" />
</Button>
<Button color="secondary" variant="outlined" autoFocus onClick={handleOnClose}>
<FormattedMessage id="action.cancel-button" defaultMessage="Cancel" />
</Button>
</DialogActions>
</form>
</Dialog>
</div>
);
}
export default RenameDialog;

View File

@ -236,10 +236,9 @@ const EnhancedTable = () => {
type ActiveDialog = { type ActiveDialog = {
actionType: DialogType; actionType: DialogType;
mapId: number mapId: number
}; };
const [activeDialog, setActiveDialog] = React.useState<ActiveDialog | undefined>(undefined);
const [activeDialog, setActiveDialog] = React.useState<ActiveDialog | undefined>(undefined);
const handleRequestSort = (event: React.MouseEvent<unknown>, property: keyof MapInfo) => { const handleRequestSort = (event: React.MouseEvent<unknown>, property: keyof MapInfo) => {
const isAsc = orderBy === property && order === 'asc'; const isAsc = orderBy === property && order === 'asc';
setOrder(isAsc ? 'desc' : 'asc'); setOrder(isAsc ? 'desc' : 'asc');

View File

@ -76,13 +76,28 @@ class RestService implements Service {
} }
renameMap(id: number, basicInfo: BasicMapInfo): Promise<void> { renameMap(id: number, basicInfo: BasicMapInfo): Promise<void> {
const exists = this.maps.find(m => m.name == basicInfo.name) != undefined;
if (!exists) {
this.maps = this.maps.map(m => {
const result = m;
if (m.id == id) {
result.description = basicInfo.description ? basicInfo.description : '';
result.name = basicInfo.name;
}
return result;
})
return Promise.resolve();
} else {
const fieldErrors: Map<string, string> = new Map<string, string>(); const fieldErrors: Map<string, string> = new Map<string, string>();
fieldErrors.set('name', 'name already exists ') fieldErrors.set('name', 'name already exists ')
return Promise.reject({ return Promise.reject({
msg: 'Map already exists ...' + basicInfo.name, msg: 'Map already exists ...' + basicInfo.name,
fields: fieldErrors fields: fieldErrors
});
})
};
} }
deleteMap(id: number): Promise<void> { deleteMap(id: number): Promise<void> {