Complete info dialog.

This commit is contained in:
Paulo Gustavo Veiga 2021-01-14 01:14:07 -08:00
parent 3552f33ee1
commit 1dda8b6c9f
18 changed files with 317 additions and 107 deletions

View File

@ -8,12 +8,18 @@
"action.delete-description": { "action.delete-description": {
"defaultMessage": "Deleted mindmap can not be recovered. Do you want to continue ?." "defaultMessage": "Deleted mindmap can not be recovered. Do you want to continue ?."
}, },
"action.delete-title": {
"defaultMessage": "Delete"
},
"action.duplicate": { "action.duplicate": {
"defaultMessage": "Duplicate" "defaultMessage": "Duplicate"
}, },
"action.export": { "action.export": {
"defaultMessage": "Export" "defaultMessage": "Export"
}, },
"action.info": {
"defaultMessage": "Info"
},
"action.open": { "action.open": {
"defaultMessage": "Open" "defaultMessage": "Open"
}, },
@ -27,7 +33,7 @@
"defaultMessage": "Rename" "defaultMessage": "Rename"
}, },
"action.share": { "action.share": {
"defaultMessage": "Info" "defaultMessage": "Share"
}, },
"common.wait": { "common.wait": {
"defaultMessage": "Please wait ..." "defaultMessage": "Please wait ..."

View File

@ -17,6 +17,12 @@
"value": "Deleted mindmap can not be recovered. Do you want to continue ?." "value": "Deleted mindmap can not be recovered. Do you want to continue ?."
} }
], ],
"action.delete-title": [
{
"type": 0,
"value": "Delete"
}
],
"action.duplicate": [ "action.duplicate": [
{ {
"type": 0, "type": 0,
@ -29,6 +35,12 @@
"value": "Export" "value": "Export"
} }
], ],
"action.info": [
{
"type": 0,
"value": "Info"
}
],
"action.open": [ "action.open": [
{ {
"type": 0, "type": 0,
@ -56,7 +68,7 @@
"action.share": [ "action.share": [
{ {
"type": 0, "type": 0,
"value": "Info" "value": "Share"
} }
], ],
"common.wait": [ "common.wait": [

View File

@ -14,12 +14,12 @@ import { FormattedMessage } from 'react-intl';
export type ActionType = 'open' | 'share' | 'delete' | 'info' | 'duplicate' | 'export' | 'rename' | 'print' | 'info' | 'publish' | undefined; export type ActionType = 'open' | 'share' | 'delete' | 'info' | 'duplicate' | 'export' | 'rename' | 'print' | 'info' | 'publish' | undefined;
interface MapActionProps { interface ActionProps {
onClose: (action: ActionType) => void; onClose: (action: ActionType) => void;
anchor: undefined | HTMLElement; anchor: undefined | HTMLElement;
} }
const ActionChooser = (props: MapActionProps) => { const ActionChooser = (props: ActionProps) => {
const { anchor, onClose } = props; const { anchor, onClose } = props;
const handleOnClose = (action: ActionType): ((event: React.MouseEvent<HTMLLIElement>) => void) => { const handleOnClose = (action: ActionType): ((event: React.MouseEvent<HTMLLIElement>) => void) => {
@ -68,7 +68,7 @@ const ActionChooser = (props: MapActionProps) => {
<Divider /> <Divider />
<MenuItem onClick={handleOnClose('info')}> <MenuItem onClick={handleOnClose('info')}>
<InfoOutlinedIcon /><FormattedMessage id="action.share" defaultMessage="Info" /> <InfoOutlinedIcon /><FormattedMessage id="action.info" defaultMessage="Info" />
</MenuItem> </MenuItem>
</Menu>); </Menu>);
} }

View File

@ -1,36 +0,0 @@
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

@ -1,34 +0,0 @@
import React from 'react';
import RenameDialog from './rename';
import DeleteDialog from './delete';
import { ActionType } from '../action-chooser';
export type BasicMapInfo = {
name: string;
description: string | undefined;
}
type ActionDialogProps = {
action?: ActionType,
mapId: number,
onClose: () => void
}
const ActionDialogDispatcher = (props: ActionDialogProps) => {
const handleOnClose = (): void => {
props.onClose();
}
const mapId = props.mapId;
const action = props.action;
return (
<span>
<DeleteDialog open={action === 'delete'} onClose={handleOnClose} mapId={mapId} />
<RenameDialog open={action === 'rename'} onClose={handleOnClose} mapId={mapId} />
</span >
);
}
export default ActionDialogDispatcher;

View File

@ -7,14 +7,14 @@ import GlobalError from "../../../form/global-error";
export type DialogProps = { export type DialogProps = {
onClose: () => void; onClose: () => void;
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void; onSubmit?: (event: React.FormEvent<HTMLFormElement>) => void;
open: boolean; open: boolean;
children: any; children: any;
error?: ErrorInfo; error?: ErrorInfo;
title: MessageDescriptor; title: MessageDescriptor;
description?: MessageDescriptor; description?: MessageDescriptor;
submitButton: MessageDescriptor; submitButton?: MessageDescriptor;
} }
const BaseDialog = (props: DialogProps) => { const BaseDialog = (props: DialogProps) => {
@ -44,12 +44,13 @@ const BaseDialog = (props: DialogProps) => {
</StyledDialogContent> </StyledDialogContent>
<StyledDialogActions> <StyledDialogActions>
{handleOnSubmit ? (
<ButtonStyled color="primary" size="medium" variant="outlined" type="submit"> <ButtonStyled color="primary" size="medium" variant="outlined" type="submit">
{intl.formatMessage(props.title)} {intl.formatMessage(props.title)}
</ButtonStyled> </ButtonStyled>) : null
}
<ButtonStyled color="secondary" size="medium" variant="outlined" autoFocus onClick={handleOnClose}> <ButtonStyled color="secondary" size="medium" variant="outlined" autoFocus onClick={handleOnClose}>
<FormattedMessage id="action.cancel-button" defaultMessage="Cancel" /> {handleOnSubmit ? (<FormattedMessage id="action.cancel-button" defaultMessage="Cancel" />) : (<FormattedMessage id="action.close-button" defaultMessage="Close" />)};
</ButtonStyled> </ButtonStyled>
</StyledDialogActions> </StyledDialogActions>
</form> </form>

View File

@ -1,11 +1,11 @@
import { Alert, AlertTitle } from "@material-ui/lab"; import { Alert, AlertTitle } from "@material-ui/lab";
import React from "react"; import React from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { useMutation, useQueryClient } from "react-query"; import { useMutation, useQueryClient } from "react-query";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { Service } from "../../../../services/Service"; import { Service } from "../../../../services/Service";
import { activeInstance } from '../../../../reducers/serviceSlice'; import { activeInstance } from '../../../../reducers/serviceSlice';
import { DialogProps, fetchMapById, handleOnMutationSuccess } from "../DialogCommon"; import { DialogProps, fetchMapById, handleOnMutationSuccess } from "..";
import BaseDialog from "../action-dialog"; import BaseDialog from "../action-dialog";
@ -35,7 +35,6 @@ const DeleteDialog = (props: DialogProps) => {
open={props.open} onClose={handleOnClose} onSubmit={handleOnSubmit} open={props.open} onClose={handleOnClose} onSubmit={handleOnSubmit}
title={{ id: "action.delete-title", defaultMessage: "Delete" }} title={{ id: "action.delete-title", defaultMessage: "Delete" }}
submitButton={{ id: "action.delete-title", defaultMessage: "Delete" }} > submitButton={{ id: "action.delete-title", defaultMessage: "Delete" }} >
<Alert severity="warning"> <Alert severity="warning">
<AlertTitle>Delete '{map?.name}'</AlertTitle> <AlertTitle>Delete '{map?.name}'</AlertTitle>
<FormattedMessage id="action.delete-description" defaultMessage="Deleted mindmap can not be recovered. Do you want to continue ?." /> <FormattedMessage id="action.delete-description" defaultMessage="Deleted mindmap can not be recovered. Do you want to continue ?." />

View File

@ -0,0 +1,90 @@
import React, { useEffect } from "react";
import { 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 Input from "../../../form/input";
import { FormControl } from "@material-ui/core";
import { DialogProps, fetchMapById, handleOnMutationSuccess } from "..";
import BaseDialog from "../action-dialog";
export type DuplicateModel = {
id: number;
name: string;
description?: string;
}
const defaultModel: DuplicateModel = { name: '', description: '', id: -1 };
const DuplicateDialog = (props: DialogProps) => {
const service: Service = useSelector(activeInstance);
const [model, setModel] = React.useState<DuplicateModel>(defaultModel);
const [error, setError] = React.useState<ErrorInfo>();
const { mapId, open } = props;
const intl = useIntl();
const queryClient = useQueryClient();
const mutation = useMutation<DuplicateModel, ErrorInfo, DuplicateModel>((model: DuplicateModel) => {
const { id, ...rest } = model;
return service.duplicateMap(id, rest).then(() => model);
},
{
onSuccess: () => {
handleOnMutationSuccess(props.onClose, queryClient);
},
onError: (error) => {
setError(error);
}
}
);
const handleOnClose = (): void => {
props.onClose();
setModel(defaultModel);
setError(undefined);
};
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>
<BaseDialog open={open} onClose={handleOnClose} onSubmit={handleOnSubmit} error={error}
title={{ id: 'duplicate.title', defaultMessage: 'Duplicate' }}
description={{ id: 'rename.description', defaultMessage: 'Please, fill the new map name and description.' }}
submitButton={{ id: 'duplicate.title', defaultMessage: 'Duplicate' }}>
<FormControl fullWidth={true}>
<Input name="name" type="text" label={{ id: "action.rename-name-placeholder", defaultMessage: "Name" }}
value={model.name} onChange={handleOnChange} error={error} fullWidth={true} />
<Input name="description" type="text" label={{ id: "action.rename-description-placeholder", defaultMessage: "Description" }}
value={model.description} onChange={handleOnChange} required={false} fullWidth={true} />
</FormControl>
</BaseDialog>
</div>
);
}
export default DuplicateDialog;

View File

@ -0,0 +1,86 @@
import React from 'react';
import RenameDialog from './rename';
import DeleteDialog from './delete';
import { ActionType } from '../action-chooser';
import { ErrorInfo, MapInfo, Service } from '../../../services/Service';
import { useSelector } from 'react-redux';
import { QueryClient, useQuery } from 'react-query';
import { activeInstance } from '../../../reducers/serviceSlice';
import DuplicateDialog from './duplicate';
import { useHistory } from 'react-router-dom';
import InfoDialog from './info';
export type BasicMapInfo = {
name: string;
description: string | undefined;
}
type ActionDialogProps = {
action?: ActionType,
mapId: number,
onClose: () => void
}
const ActionDispatcher = (props: ActionDialogProps) => {
const history = useHistory();
const mapId = props.mapId;
const action = props.action;
const handleOnClose = (): void => {
props.onClose();
}
switch (action) {
case 'open':
history.push(`/c/maps/${mapId}/edit`);
break;
case 'print':
history.push(`/c/maps/${mapId}/print`);
break;
}
return (
<span>
<DeleteDialog open={action === 'delete'} onClose={handleOnClose} mapId={mapId} />
<RenameDialog open={action === 'rename'} onClose={handleOnClose} mapId={mapId} />
<DuplicateDialog open={action === 'duplicate'} onClose={handleOnClose} mapId={mapId} />
<InfoDialog open={action === 'info'} onClose={handleOnClose} mapId={mapId} />
</span >
);
}
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
}
export default ActionDispatcher;

View File

@ -0,0 +1,35 @@
import React from "react";
import { useQueryClient } from "react-query";
import { useSelector } from "react-redux";
import { Service } from "../../../../services/Service";
import { activeInstance } from '../../../../reducers/serviceSlice';
import { DialogProps, fetchMapById } from "..";
import BaseDialog from "../action-dialog";
const InfoDialog = (props: DialogProps) => {
const service: Service = useSelector(activeInstance);
const queryClient = useQueryClient();
const mapId = props.mapId;
const handleOnClose = (): void => {
props.onClose();
};
const { map } = fetchMapById(mapId);
return (
<div>
<BaseDialog
open={props.open} onClose={handleOnClose}
title={{ id: "action.info-title", defaultMessage: "Info" }}>
<iframe src="http://www.clarin.com" style={{width:'100%',height:'400px'}}/>
</BaseDialog>
</div>
);
}
export default InfoDialog;

View File

@ -4,7 +4,7 @@ import { useMutation, useQueryClient } from "react-query";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { BasicMapInfo, ErrorInfo, Service } from "../../../../services/Service"; import { BasicMapInfo, ErrorInfo, Service } from "../../../../services/Service";
import { activeInstance } from '../../../../reducers/serviceSlice'; import { activeInstance } from '../../../../reducers/serviceSlice';
import { DialogProps, fetchMapById, handleOnMutationSuccess } from "./../DialogCommon"; import { DialogProps, fetchMapById, handleOnMutationSuccess } from "..";
import Input from "../../../form/input"; import Input from "../../../form/input";
import { FormControl } from "@material-ui/core"; import { FormControl } from "@material-ui/core";
import BaseDialog from "../action-dialog"; import BaseDialog from "../action-dialog";
@ -72,8 +72,8 @@ const RenameDialog = (props: DialogProps) => {
<div> <div>
<BaseDialog open={open} onClose={handleOnClose} onSubmit={handleOnSubmit} error={error} <BaseDialog open={open} onClose={handleOnClose} onSubmit={handleOnSubmit} error={error}
title={{ id: 'rename.title', defaultMessage: 'Rename' }} title={{ id: 'rename.title', defaultMessage: 'Rename' }}
description={{ id: 'rename.description', defaultMessage: 'Rename' }} description={{ id: 'rename.description', defaultMessage: 'Please, fill the new map name and description.' }}
submitButton={{ id: 'rename.title', defaultMessage: 'Rename Description' }}> submitButton={{ id: 'rename.title', defaultMessage: 'Rename' }}>
<FormControl fullWidth={true}> <FormControl fullWidth={true}>
<Input name="name" type="text" label={{ id: "action.rename-name-placeholder", defaultMessage: "Name" }} <Input name="name" type="text" label={{ id: "action.rename-name-placeholder", defaultMessage: "Name" }}

View File

@ -1,5 +1,5 @@
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { PageContainer, MapsListArea, NavArea, HeaderArea, StyledTableCell } from './styled'; import { PageContainer, MapsListArea, HeaderArea, StyledTableCell } from './styled';
import { createStyles, makeStyles, Theme, ThemeProvider } from '@material-ui/core/styles'; import { createStyles, makeStyles, Theme, ThemeProvider } from '@material-ui/core/styles';
import Table from '@material-ui/core/Table'; import Table from '@material-ui/core/Table';
@ -28,7 +28,8 @@ import { ErrorInfo, MapInfo, Service } from '../../services/Service';
import { theme } from '../../theme/global-style'; import { theme } from '../../theme/global-style';
import { CssBaseline } from '@material-ui/core'; import { CssBaseline } from '@material-ui/core';
import ActionChooser, { ActionType } from './action-chooser'; import ActionChooser, { ActionType } from './action-chooser';
import ActionDialogDispatcher from './action-dialog-dispatcher'; import ActionDispatcher from './action-dispatcher';
import NavPanel from './nav-panel';
@ -404,7 +405,7 @@ const EnhancedTable = () => {
</Paper> </Paper>
{/* Action Dialog */} {/* Action Dialog */}
<ActionDialogDispatcher action={activeDialog?.actionType} onClose={() => setActiveDialog(undefined)} mapId={activeDialog ? activeDialog.mapId : -1} /> <ActionDispatcher action={activeDialog?.actionType} onClose={() => setActiveDialog(undefined)} mapId={activeDialog ? activeDialog.mapId : -1} />
</div> </div>
); );
} }
@ -420,13 +421,12 @@ const MapsPage = () => {
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline /> <CssBaseline />
<PageContainer> <PageContainer>
<HeaderArea> <HeaderArea>
<h2>Header</h2> <h2>Header</h2>
</HeaderArea> </HeaderArea>
<NavArea> <NavPanel/>
<h1> Nav </h1>
</NavArea>
<MapsListArea> <MapsListArea>
<EnhancedTable /> <EnhancedTable />
</MapsListArea> </MapsListArea>

View File

@ -0,0 +1,8 @@
import React from "react";
const NavPanel = (props: any) => {
return (<p>nav</p>);
}
export default NavPanel;

View File

@ -0,0 +1,7 @@
import styled from "styled-components";
export const NavArea = styled.div`
grid-area: nav;
background-color: red;
`

View File

@ -18,11 +18,6 @@ grid-area: main;
background-color: #ffff64; background-color: #ffff64;
` `
export const NavArea = styled.div`
grid-area: nav;
background-color: red;
`
export const HeaderArea = styled.div` export const HeaderArea = styled.div`
grid-area: header; grid-area: header;
background-color: blue; background-color: blue;

View File

@ -12,7 +12,7 @@ export type MapInfo = {
id: number; id: number;
starred: boolean; starred: boolean;
name: string; name: string;
labels: [string]; labels: string[];
creator: string; creator: string;
modified: number; modified: number;
description: string; description: string;
@ -40,6 +40,7 @@ interface Service {
deleteMap(id: number): Promise<void>; deleteMap(id: number): Promise<void>;
renameMap(id: number, basicInfo: BasicMapInfo): Promise<void>; renameMap(id: number, basicInfo: BasicMapInfo): Promise<void>;
duplicateMap(id: number, basicInfo: BasicMapInfo): Promise<void>;
loadMapInfo(id: number): Promise<BasicMapInfo>; loadMapInfo(id: number): Promise<BasicMapInfo>;
} }
@ -56,7 +57,7 @@ class MockService implements Service {
id: number, id: number,
starred: boolean, starred: boolean,
name: string, name: string,
labels: [string], labels: string[],
creator: string, creator: string,
modified: number, modified: number,
description: string description: string
@ -99,6 +100,33 @@ class MockService implements Service {
}) })
}; };
} }
duplicateMap(id: number, basicInfo: BasicMapInfo): Promise<void> {
const exists = this.maps.find(m => m.name == basicInfo.name) != undefined;
if (!exists) {
const newMap: MapInfo = {
id: Math.random() * 1000,
description: String(basicInfo.description),
name: basicInfo.name,
starred: false,
creator: "current user",
labels: [],
modified: -1
};
this.maps.push(newMap);
return Promise.resolve();
} else {
const fieldErrors: Map<string, string> = new Map<string, string>();
fieldErrors.set('name', 'name already exists ')
return Promise.reject({
msg: 'Maps name must be unique:' + basicInfo.name,
fields: fieldErrors
})
};
}
deleteMap(id: number): Promise<void> { deleteMap(id: number): Promise<void> {
this.maps = this.maps.filter(m => m.id != id); this.maps = this.maps.filter(m => m.id != id);

View File

@ -150,14 +150,27 @@ const theme = createMuiTheme({
fontFamily: [ fontFamily: [
'Montserrat' 'Montserrat'
].join(','), ].join(','),
h6: {
fontSize: '25px',
fontWeight: 'bold'
},
},
palette: {
primary: {
main: '#ffffff',
light: '#ffffff',
dark: '#ffffff',
contrastText: '#ffffff'
},
secondary: {
main: '#ffffff',
light: '#ffffff',
dark: '#ffffff',
contrastText: '#ffffff'
}
} }
}); });
theme.typography.h6 = { theme.palette.secondary
fontSize: '25px',
fontWeight: 'bold'
};
export { GlobalStyle, PageContent, theme }; export { GlobalStyle, PageContent, theme };