mirror of
https://bitbucket.org/wisemapping/wisemapping-frontend.git
synced 2024-11-25 15:47:55 +01:00
Initial version of maps list. WIP
This commit is contained in:
parent
3a049d203d
commit
5b2bc9390b
@ -45,6 +45,8 @@
|
||||
"webpack-dev-server": "^3.11.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.11.2",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@types/react-google-recaptcha": "^2.1.0",
|
||||
"axios": "^0.21.0",
|
||||
"react": "^17.0.1",
|
||||
|
@ -7,6 +7,7 @@ import RegistrationSuccessPage from './components/registration-success-page';
|
||||
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 {
|
||||
Route,
|
||||
@ -70,6 +71,9 @@ const App = (props: AppProps) => {
|
||||
<ForgotPasswordPage service={service} />
|
||||
</Route>
|
||||
<Route path="/c/forgot-password-success" component={ForgotPasswordSuccessPage} />
|
||||
<Route path="/c/maps/">
|
||||
<MapsPage service={service} />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
</IntlProvider>
|
||||
|
@ -17,8 +17,8 @@ type ForgotPasswordProps = {
|
||||
}
|
||||
|
||||
const ForgotPassword = (props: ServiceProps) => {
|
||||
const [email, setEmail] = useState("");
|
||||
const [errorMsg, setErrorMsg] = useState("");
|
||||
const [email, setEmail] = useState('');
|
||||
const [errorMsg, setErrorMsg] = useState('');
|
||||
|
||||
const [disableButton, setDisableButton] = useState(false);
|
||||
|
||||
@ -33,8 +33,8 @@ const ForgotPassword = (props: ServiceProps) => {
|
||||
props.service.resetPassword(
|
||||
email,
|
||||
() => history.push("/c/forgot-password-success"),
|
||||
(msg) => {
|
||||
setErrorMsg(msg);
|
||||
(errorInfo) => {
|
||||
setErrorMsg(errorInfo.msg ? errorInfo.msg : '');
|
||||
setDisableButton(false);
|
||||
}
|
||||
);
|
||||
@ -42,8 +42,8 @@ const ForgotPassword = (props: ServiceProps) => {
|
||||
|
||||
return (
|
||||
<PageContent>
|
||||
<h1><FormattedMessage id="forgot.title" defaultMessage="Reset your password" /></h1>
|
||||
<p><FormattedMessage id="forgot.desc" defaultMessage="We will send you an email to reset your password" /></p>
|
||||
<h1><FormattedMessage id="forgot.title" defaultMessage="Reset your password"/></h1>
|
||||
<p><FormattedMessage id="forgot.desc" defaultMessage="We will send you an email to reset your password"/></p>
|
||||
|
||||
<form onSubmit={e => handleSubmit(e)}>
|
||||
<input type="email" name="email" onChange={e => setEmail(e.target.value)} placeholder={intl.formatMessage({ id: "forgot.email", defaultMessage: "Email" })} required={true} autoComplete="email" />
|
||||
|
@ -2,7 +2,7 @@ import React from 'react'
|
||||
import { StyleDiv } from './styled'
|
||||
|
||||
type FormErrorDialogProps = {
|
||||
message: string | null;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
const FormErrorDialog = (props: FormErrorDialogProps) => {
|
||||
|
@ -30,7 +30,7 @@ const LoginError = () => {
|
||||
const errorCode = new URLSearchParams(window.location.search).get('login_error');
|
||||
const intl = useIntl();
|
||||
|
||||
let msg = null;
|
||||
let msg;
|
||||
if (errorCode) {
|
||||
if (errorCode === "3") {
|
||||
msg = intl.formatMessage({ id: "login.userinactive", defaultMessage: "Sorry, your account has not been activated yet. You'll receive a notification email when it becomes active. Stay tuned!." });
|
||||
|
157
packages/webapp/src/components/maps-page/ActionDialog.tsx
Normal file
157
packages/webapp/src/components/maps-page/ActionDialog.tsx
Normal file
@ -0,0 +1,157 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
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 { BasicMapInfo, Service } from '../../services/Service';
|
||||
import { FormControl, TextField } from '@material-ui/core';
|
||||
|
||||
type DialogProps = {
|
||||
open: boolean,
|
||||
mapId: number,
|
||||
onClose: (reload: boolean) => void,
|
||||
service: Service
|
||||
}
|
||||
|
||||
function DeleteConfirmDialog(props: DialogProps) {
|
||||
|
||||
const handleOnClose = (action: 'accept' | undefined): void => {
|
||||
let result = false;
|
||||
if (action == 'accept') {
|
||||
props.service.deleteMap(props.mapId);
|
||||
result = true;
|
||||
}
|
||||
props.onClose(result);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dialog
|
||||
open={props.open}
|
||||
onClose={() => handleOnClose(undefined)} >
|
||||
<DialogTitle><FormattedMessage id="action.delete-title" defaultMessage="Delete" /></DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
<FormattedMessage id="action.delete-description" defaultMessage="Deleted mindmap can not be recovered. Do you want to continue ?." />
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => handleOnClose('accept')} color="primary">
|
||||
<FormattedMessage id="action.delete-button" defaultMessage="Delete" />
|
||||
</Button>
|
||||
<Button onClick={() => handleOnClose(undefined)} color="secondary" autoFocus>
|
||||
<FormattedMessage id="action.cancel-button" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RenameDialog(props: DialogProps) {
|
||||
const [model, setModel] = React.useState<BasicMapInfo>({ name: '', description: '' });
|
||||
const intl = useIntl();
|
||||
const [nameErrorMsg, setNameErrorMsg] = React.useState<string|undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
props.service.loadMapInfo(props.mapId)
|
||||
.then((info: BasicMapInfo) => {
|
||||
setModel(info);
|
||||
})
|
||||
}, []);
|
||||
|
||||
const handleOnClose = (): void => {
|
||||
props.onClose(false);
|
||||
};
|
||||
|
||||
const handleOnSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
|
||||
// Stop form submit ...
|
||||
event.preventDefault();
|
||||
|
||||
// Fire rest call ...
|
||||
const mapId = props.mapId;
|
||||
props.service.remameMap(mapId, model).
|
||||
then(e => {
|
||||
// Close the dialog ...
|
||||
props.onClose(true);
|
||||
}).catch(e => {
|
||||
setNameErrorMsg("Error ....")
|
||||
});
|
||||
};
|
||||
|
||||
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="Rename Desc" />
|
||||
</DialogContentText>
|
||||
|
||||
<FormControl margin="normal" required fullWidth>
|
||||
<TextField name="name" label={intl.formatMessage({ id: "action.name-lable", defaultMessage: "Name" })} value={model.name} variant="filled" onChange={handleOnChange} required={true} error={Boolean(nameErrorMsg)} helperText={nameErrorMsg} />
|
||||
</FormControl>
|
||||
<FormControl margin="normal" required fullWidth>
|
||||
<TextField name="description" label={intl.formatMessage({ id: "action.description-lable", defaultMessage: "Description" })} value={model.description} onChange={handleOnChange} variant="filled" />
|
||||
</FormControl>
|
||||
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button color="primary" fullWidth type="submit">
|
||||
<FormattedMessage id="action.rename-button" defaultMessage="Rename" />
|
||||
</Button>
|
||||
|
||||
<Button color="secondary" autoFocus fullWidth onClick={handleOnClose}>
|
||||
<FormattedMessage id="action.cancel-button" defaultMessage="Cancel" />
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type DialogType = 'share' | 'delete' | 'info' | 'duplicate' | 'export' | 'rename' | 'publish';
|
||||
|
||||
type ActionDialogProps = {
|
||||
action: DialogType | undefined,
|
||||
mapId: number,
|
||||
service: Service,
|
||||
onClose: (reload: boolean) => void
|
||||
}
|
||||
|
||||
const ActionDialog = (props: ActionDialogProps) => {
|
||||
|
||||
const handleOnClose = (reload: boolean): void => {
|
||||
props.onClose(reload);
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<DeleteConfirmDialog open={props.action === 'delete'} service={props.service} onClose={handleOnClose} mapId={props.mapId} />
|
||||
<RenameDialog open={props.action === 'rename'} service={props.service} onClose={handleOnClose} mapId={props.mapId} />
|
||||
|
||||
</span >
|
||||
);
|
||||
}
|
||||
|
||||
export default ActionDialog;
|
76
packages/webapp/src/components/maps-page/MapActionMenu.tsx
Normal file
76
packages/webapp/src/components/maps-page/MapActionMenu.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import { Divider, Menu, MenuItem } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import DescriptionOutlinedIcon from '@material-ui/icons/DescriptionOutlined';
|
||||
import FileCopyOutlinedIcon from '@material-ui/icons/FileCopyOutlined';
|
||||
import DeleteOutlinedIcon from '@material-ui/icons/DeleteOutlined';
|
||||
import CloudDownloadOutlinedIcon from '@material-ui/icons/CloudDownloadOutlined';
|
||||
import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined';
|
||||
import EditOutlinedIcon from '@material-ui/icons/EditOutlined';
|
||||
import PublicOutlinedIcon from '@material-ui/icons/PublicOutlined';
|
||||
import PrintOutlinedIcon from '@material-ui/icons/PrintOutlined';
|
||||
import ShareOutlinedIcon from '@material-ui/icons/ShareOutlined';
|
||||
import { StyledMenuItem } from './styled';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export type ActionType = 'open' | 'share' | 'delete' | 'info' | 'duplicate' | 'export' | 'rename' | 'print' | 'info' | 'publish' | undefined;
|
||||
|
||||
interface MapActionProps {
|
||||
onClose: (action: ActionType) => void;
|
||||
anchor: undefined | HTMLElement;
|
||||
}
|
||||
|
||||
const MapActionDialog = (props: MapActionProps) => {
|
||||
const { anchor, onClose } = props;
|
||||
|
||||
const handleOnClose = (action: ActionType): ((event: React.MouseEvent<HTMLLIElement>) => void) => {
|
||||
return (event): void => {
|
||||
event.stopPropagation();
|
||||
onClose(action);
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<Menu
|
||||
anchorEl={anchor}
|
||||
keepMounted
|
||||
open={Boolean(anchor)}
|
||||
onClose={handleOnClose(undefined)}
|
||||
>
|
||||
<StyledMenuItem onClick={handleOnClose('open')}>
|
||||
<DescriptionOutlinedIcon /><FormattedMessage id="action.open" defaultMessage="Open" />
|
||||
</StyledMenuItem>
|
||||
<Divider />
|
||||
|
||||
<MenuItem onClick={handleOnClose('duplicate')}>
|
||||
<FileCopyOutlinedIcon /><FormattedMessage id="action.duplicate" defaultMessage="Duplicate" />
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleOnClose('rename')}>
|
||||
<EditOutlinedIcon /> <FormattedMessage id="action.rename" defaultMessage="Rename" />
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleOnClose('delete')}>
|
||||
<DeleteOutlinedIcon /><FormattedMessage id="action.delete" defaultMessage="Delete" />
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
|
||||
<MenuItem onClick={handleOnClose('export')}>
|
||||
<CloudDownloadOutlinedIcon /><FormattedMessage id="action.export" defaultMessage="Export" />
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleOnClose('print')}>
|
||||
<PrintOutlinedIcon /><FormattedMessage id="action.print" defaultMessage="Print" />
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleOnClose('publish')}>
|
||||
<PublicOutlinedIcon /><FormattedMessage id="action.publish" defaultMessage="Publish" />
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleOnClose('share')}>
|
||||
<ShareOutlinedIcon /><FormattedMessage id="action.share" defaultMessage="Share" />
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
|
||||
<MenuItem onClick={handleOnClose('info')}>
|
||||
<InfoOutlinedIcon /><FormattedMessage id="action.share" defaultMessage="Info" />
|
||||
</MenuItem>
|
||||
</Menu>);
|
||||
}
|
||||
|
||||
export default MapActionDialog;
|
434
packages/webapp/src/components/maps-page/index.tsx
Normal file
434
packages/webapp/src/components/maps-page/index.tsx
Normal file
@ -0,0 +1,434 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { MapInfo, Service } from '../../services/Service'
|
||||
import { PageContainer, MapsListArea, NavArea, HeaderArea, StyledTableCell } from './styled';
|
||||
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles';
|
||||
import Table from '@material-ui/core/Table';
|
||||
import TableBody from '@material-ui/core/TableBody';
|
||||
import TableCell from '@material-ui/core/TableCell';
|
||||
import TableContainer from '@material-ui/core/TableContainer';
|
||||
import TableHead from '@material-ui/core/TableHead';
|
||||
import TablePagination from '@material-ui/core/TablePagination';
|
||||
import TableRow from '@material-ui/core/TableRow';
|
||||
import TableSortLabel from '@material-ui/core/TableSortLabel';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import DeleteIcon from '@material-ui/icons/Delete';
|
||||
import FilterListIcon from '@material-ui/icons/FilterList';
|
||||
import StarRateRoundedIcon from '@material-ui/icons/StarRateRounded';
|
||||
import MoreHorizIcon from '@material-ui/icons/MoreHoriz';
|
||||
import { CSSProperties } from 'react';
|
||||
import MapActionMenu, { ActionType } from './MapActionMenu';
|
||||
import ActionDialog, { DialogType } from './ActionDialog';
|
||||
|
||||
function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
|
||||
if (b[orderBy] < a[orderBy]) {
|
||||
return -1;
|
||||
}
|
||||
if (b[orderBy] > a[orderBy]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
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 {
|
||||
return order === 'desc'
|
||||
? (a, b) => descendingComparator(a, b, orderBy)
|
||||
: (a, b) => -descendingComparator(a, b, orderBy);
|
||||
}
|
||||
|
||||
function stableSort<T>(array: T[], comparator: (a: T, b: T) => number) {
|
||||
const stabilizedThis = array.map((el, index) => [el, index] as [T, number]);
|
||||
stabilizedThis.sort((a, b) => {
|
||||
const order = comparator(a[0], b[0]);
|
||||
if (order !== 0) return order;
|
||||
return a[1] - b[1];
|
||||
});
|
||||
return stabilizedThis.map((el) => el[0]);
|
||||
}
|
||||
|
||||
interface HeadCell {
|
||||
disablePadding: boolean;
|
||||
id: keyof MapInfo;
|
||||
label: string;
|
||||
numeric: boolean;
|
||||
style: CSSProperties;
|
||||
}
|
||||
|
||||
const headCells: HeadCell[] = [
|
||||
{ id: 'starred', numeric: false, disablePadding: false, label: '', style: { width: '20px', padding: '0px' } },
|
||||
{ id: 'name', numeric: false, disablePadding: true, label: 'Name', style: {} },
|
||||
{ id: 'labels', numeric: false, disablePadding: true, label: 'Labels', style: {} },
|
||||
{ id: 'creator', numeric: false, disablePadding: false, label: 'Creator', style: {} },
|
||||
{ id: 'modified', numeric: true, disablePadding: false, label: 'Modified', style: { width: '50px' } }
|
||||
];
|
||||
|
||||
interface EnhancedTableProps {
|
||||
classes: ReturnType<typeof useStyles>;
|
||||
numSelected: number;
|
||||
onRequestSort: (event: React.MouseEvent<unknown>, property: keyof MapInfo) => void;
|
||||
onSelectAllClick: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
order: Order;
|
||||
orderBy: string;
|
||||
rowCount: number;
|
||||
}
|
||||
|
||||
function EnhancedTableHead(props: EnhancedTableProps) {
|
||||
const { classes, onSelectAllClick, order, orderBy, numSelected, rowCount, onRequestSort } = props;
|
||||
const createSortHandler = (property: keyof MapInfo) => (event: React.MouseEvent<unknown>) => {
|
||||
onRequestSort(event, property);
|
||||
};
|
||||
|
||||
return (
|
||||
<TableHead>
|
||||
|
||||
<TableRow>
|
||||
<TableCell padding='checkbox' key='select' style={{ width: '20px' }}>
|
||||
<Checkbox
|
||||
indeterminate={numSelected > 0 && numSelected < rowCount}
|
||||
checked={rowCount > 0 && numSelected === rowCount}
|
||||
onChange={onSelectAllClick}
|
||||
size='small'
|
||||
inputProps={{ 'aria-label': 'select all desserts' }}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
{headCells.map((headCell) => (
|
||||
<TableCell
|
||||
key={headCell.id}
|
||||
sortDirection={orderBy === headCell.id ? order : false}
|
||||
style={headCell.style}
|
||||
>
|
||||
<TableSortLabel
|
||||
active={orderBy === headCell.id}
|
||||
direction={orderBy === headCell.id ? order : 'asc'}
|
||||
onClick={createSortHandler(headCell.id)}>
|
||||
{headCell.label}
|
||||
{orderBy === headCell.id ? (
|
||||
<span className={classes.visuallyHidden}>
|
||||
{order === 'desc' ? 'sorted descending' : 'sorted ascending'}
|
||||
</span>
|
||||
) : null}
|
||||
</TableSortLabel>
|
||||
</TableCell>
|
||||
))}
|
||||
<TableCell style={{ width: '20px', padding: '0px' }} key='actions'>
|
||||
<TableSortLabel>""</TableSortLabel>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
);
|
||||
}
|
||||
|
||||
const useToolbarStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(1),
|
||||
},
|
||||
title: {
|
||||
flex: '1 1 100%',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
interface EnhancedTableToolbarProps {
|
||||
numSelected: number;
|
||||
}
|
||||
|
||||
const EnhancedTableToolbar = (props: EnhancedTableToolbarProps) => {
|
||||
const classes = useToolbarStyles();
|
||||
const { numSelected } = props;
|
||||
|
||||
return (
|
||||
<Toolbar>
|
||||
{numSelected > 0 ? (
|
||||
<Typography className={classes.title} color="inherit" variant="subtitle1" component="div">
|
||||
{numSelected} selected
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography className={classes.title} variant="h6" id="tableTitle" component="div">
|
||||
Nutrition
|
||||
</Typography>
|
||||
)}
|
||||
{numSelected > 0 ? (
|
||||
<Tooltip title="Delete">
|
||||
<IconButton aria-label="delete">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title="Filter list">
|
||||
<IconButton aria-label="filter list">
|
||||
<FilterListIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Toolbar>
|
||||
);
|
||||
};
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
width: '100%',
|
||||
},
|
||||
paper: {
|
||||
width: '100%',
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
table: {
|
||||
minWidth: 750,
|
||||
border: 0,
|
||||
},
|
||||
visuallyHidden: {
|
||||
border: 0,
|
||||
clip: 'rect(0 0 0 0)',
|
||||
height: 1,
|
||||
margin: -1,
|
||||
overflow: 'hidden',
|
||||
padding: 0,
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
width: 1,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
type ActionPanelState = {
|
||||
el: HTMLElement | undefined,
|
||||
mapId: number
|
||||
}
|
||||
|
||||
function EnhancedTable(props: ServiceProps) {
|
||||
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 [rows, setRows] = React.useState<MapInfo[]>([]);
|
||||
|
||||
|
||||
const [activeRowAction, setActiveRowAction] = React.useState<ActionPanelState | undefined>(undefined);
|
||||
|
||||
type ActiveDialog = {
|
||||
actionType: DialogType;
|
||||
mapId: number
|
||||
|
||||
};
|
||||
const [activeDialog, setActiveDialog] = React.useState<ActiveDialog | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const mapsInfo = await props.service.fetchAllMaps();
|
||||
setRows(mapsInfo);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const handleRequestSort = (event: React.MouseEvent<unknown>, property: keyof MapInfo) => {
|
||||
const isAsc = orderBy === property && order === 'asc';
|
||||
setOrder(isAsc ? 'desc' : 'asc');
|
||||
setOrderBy(property);
|
||||
};
|
||||
|
||||
const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
if (event.target.checked) {
|
||||
const newSelecteds = rows.map((n) => n.id);
|
||||
setSelected(newSelecteds);
|
||||
return;
|
||||
}
|
||||
setSelected([]);
|
||||
};
|
||||
|
||||
const handleRowClick = (event: React.MouseEvent<unknown>, id: number): void => {
|
||||
const selectedIndex = selected.indexOf(id);
|
||||
let newSelected: number[] = [];
|
||||
|
||||
if (selectedIndex === -1) {
|
||||
newSelected = newSelected.concat(selected, id);
|
||||
} else if (selectedIndex === 0) {
|
||||
newSelected = newSelected.concat(selected.slice(1));
|
||||
} else if (selectedIndex === selected.length - 1) {
|
||||
newSelected = newSelected.concat(selected.slice(0, -1));
|
||||
} else if (selectedIndex > 0) {
|
||||
newSelected = newSelected.concat(
|
||||
selected.slice(0, selectedIndex),
|
||||
selected.slice(selectedIndex + 1),
|
||||
);
|
||||
}
|
||||
|
||||
setSelected(newSelected);
|
||||
};
|
||||
|
||||
const handleChangePage = (event: unknown, newPage: number) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const handleActionClick = (mapId: number): ((event: any) => void) => {
|
||||
return (event: any): void => {
|
||||
setActiveRowAction(
|
||||
{
|
||||
mapId: mapId,
|
||||
el: event.currentTarget
|
||||
}
|
||||
);
|
||||
event.stopPropagation();
|
||||
};
|
||||
};
|
||||
|
||||
const handleActionMenuClose = (action: ActionType): void => {
|
||||
if (action) {
|
||||
const mapId = activeRowAction?.mapId;
|
||||
|
||||
setActiveRowAction(undefined);
|
||||
setActiveDialog({
|
||||
actionType: action as DialogType,
|
||||
mapId: mapId as number
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const isSelected = (id: number) => selected.indexOf(id) !== -1;
|
||||
|
||||
const emptyRows = rowsPerPage - Math.min(rowsPerPage, rows.length - page * rowsPerPage);
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Paper className={classes.paper}>
|
||||
<EnhancedTableToolbar numSelected={selected.length} />
|
||||
<TableContainer>
|
||||
<Table
|
||||
className={classes.table}
|
||||
aria-labelledby="tableTitle"
|
||||
size={'small'}
|
||||
aria-label="sticky table"
|
||||
stickyHeader
|
||||
>
|
||||
<EnhancedTableHead
|
||||
classes={classes}
|
||||
numSelected={selected.length}
|
||||
order={order}
|
||||
orderBy={orderBy}
|
||||
onSelectAllClick={handleSelectAllClick}
|
||||
onRequestSort={handleRequestSort}
|
||||
rowCount={rows.length}
|
||||
/>
|
||||
<TableBody>
|
||||
{stableSort(rows, getComparator(order, orderBy))
|
||||
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||
.map((row: MapInfo) => {
|
||||
const isItemSelected = isSelected(row.id);
|
||||
const labelId = row.id;
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
hover
|
||||
onClick={(event: any) => handleRowClick(event, row.id)}
|
||||
role="checkbox"
|
||||
aria-checked={isItemSelected}
|
||||
tabIndex={-1}
|
||||
key={row.id}
|
||||
selected={isItemSelected}
|
||||
>
|
||||
|
||||
<StyledTableCell padding="checkbox">
|
||||
<Checkbox
|
||||
checked={isItemSelected}
|
||||
inputProps={{ 'aria-labelledby': String(labelId) }}
|
||||
size="small"
|
||||
/>
|
||||
</StyledTableCell>
|
||||
|
||||
<StyledTableCell>
|
||||
<Tooltip title="Starred">
|
||||
<IconButton aria-label="Starred" size="small" onClick={(e) => { alert("") }}>
|
||||
<StarRateRoundedIcon color="action" style={{ color: row.starred ? 'yellow' : 'gray' }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</StyledTableCell>
|
||||
|
||||
<StyledTableCell>{row.name}</StyledTableCell>
|
||||
<StyledTableCell>{row.labels}</StyledTableCell>
|
||||
<StyledTableCell>{row.creator}</StyledTableCell>
|
||||
<StyledTableCell>{row.modified}</StyledTableCell>
|
||||
|
||||
<StyledTableCell>
|
||||
<Tooltip title="Others">
|
||||
<IconButton aria-label="Others" size="small" onClick={handleActionClick(row.id)}>
|
||||
<MoreHorizIcon color="action" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<MapActionMenu anchor={activeRowAction?.el} onClose={handleActionMenuClose} />
|
||||
</StyledTableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
{emptyRows > 0 && (
|
||||
<TableRow style={{ height: 33 * emptyRows }}>
|
||||
<TableCell colSpan={6} />
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
component="div"
|
||||
count={rows.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onChangePage={handleChangePage}
|
||||
onChangeRowsPerPage={handleChangeRowsPerPage}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
{/* Action Dialog */}
|
||||
<ActionDialog action={activeDialog?.actionType} onClose={(refresh: boolean) => setActiveDialog(undefined)} mapId={activeDialog ? activeDialog.mapId : -1} service={props.service}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type ServiceProps = {
|
||||
service: Service
|
||||
}
|
||||
|
||||
const MapsPage = (props: ServiceProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'Maps | WiseMapping';
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<HeaderArea>
|
||||
<h2>Header</h2>
|
||||
</HeaderArea>
|
||||
<NavArea>
|
||||
<h1> Nav </h1>
|
||||
</NavArea>
|
||||
<MapsListArea>
|
||||
<EnhancedTable service={props.service} />
|
||||
</MapsListArea>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default MapsPage;
|
||||
|
||||
|
46
packages/webapp/src/components/maps-page/styled.ts
Normal file
46
packages/webapp/src/components/maps-page/styled.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { MenuItem, TableCell } from '@material-ui/core';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const PageContainer = styled.div`
|
||||
display: grid;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
grid-template-areas: "nav header header"
|
||||
"nav main ads"
|
||||
"nav main ads";
|
||||
grid-template-columns: 240px 1fr 240px;
|
||||
grid-template-rows: 60px 1fr 30px;
|
||||
`
|
||||
|
||||
export const MapsListArea = styled.div`
|
||||
grid-area: main;
|
||||
background-color: #ffff64;
|
||||
`
|
||||
|
||||
export const NavArea = styled.div`
|
||||
grid-area: nav;
|
||||
background-color: red;
|
||||
`
|
||||
|
||||
export const HeaderArea = styled.div`
|
||||
grid-area: header;
|
||||
background-color: blue;
|
||||
`
|
||||
|
||||
|
||||
export const StyledTableCell = withStyles({
|
||||
root: {
|
||||
color: 'black',
|
||||
padding: '0px',
|
||||
cursor: 'pointer'
|
||||
}
|
||||
})(TableCell);
|
||||
|
||||
export const StyledMenuItem = withStyles({
|
||||
root: {
|
||||
width: '300px',
|
||||
padding: '10px 20px',
|
||||
marging: '0px 20px'
|
||||
}
|
||||
})(MenuItem)
|
@ -13,12 +13,12 @@ import { StyledReCAPTCHA } from './styled';
|
||||
import { PageContent } from '../../theme/global-style';
|
||||
|
||||
const RegistrationForm = (props: ServiceProps) => {
|
||||
const [email, setEmail] = useState("");
|
||||
const [lastname, setLastname] = useState("")
|
||||
const [firstname, setFirstname] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [recaptchaToken, setRecaptchaToken] = useState<string | null>("");
|
||||
const [errorMsg, setErrorMsg] = useState("");
|
||||
const [email, setEmail] = useState('');
|
||||
const [lastname, setLastname] = useState('')
|
||||
const [firstname, setFirstname] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [recaptchaToken, setRecaptchaToken] = useState<string | null>('');
|
||||
const [errorMsg, setErrorMsg] = useState<string>();
|
||||
|
||||
const [disableButton, setDisableButton] = useState(false);
|
||||
|
||||
@ -42,7 +42,10 @@ const RegistrationForm = (props: ServiceProps) => {
|
||||
props.service.registerNewUser(
|
||||
user,
|
||||
() => history.push("/c/registration-success"),
|
||||
(msg) => { setErrorMsg(msg); setDisableButton(false); }
|
||||
(errorInfo) => {
|
||||
const errorMsg = errorInfo.msg ? errorInfo.msg : '';
|
||||
setErrorMsg(errorMsg); setDisableButton(false);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -65,7 +68,7 @@ const RegistrationForm = (props: ServiceProps) => {
|
||||
|
||||
<FormErrorDialog message={errorMsg} />
|
||||
|
||||
<div style={{ width: "300px", textAlign: "center", fontSize:"13px",margin:"auto" }}>
|
||||
<div style={{ width: "300px", textAlign: "center", fontSize: "13px", margin: "auto" }}>
|
||||
<FormattedMessage id="registration.termandconditions" defaultMessage="Terms of Service: Please check the WiseMapping Account information you've entered above, and review the Terms of Service here. By clicking on 'Register' below you are agreeing to the Terms of Service above and the Privacy Policy" />
|
||||
</div>
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import axios from 'axios'
|
||||
|
||||
type NewUser = {
|
||||
export type NewUser = {
|
||||
email: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
@ -8,10 +8,38 @@ type NewUser = {
|
||||
recaptcha: string | null;
|
||||
}
|
||||
|
||||
export type MapInfo = {
|
||||
id: number;
|
||||
starred: boolean;
|
||||
name: string;
|
||||
labels: [string];
|
||||
creator: string;
|
||||
modified: number;
|
||||
}
|
||||
|
||||
export type BasicMapInfo = {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export type FieldError = {
|
||||
id: string,
|
||||
msg: string
|
||||
}
|
||||
|
||||
export type ErrorInfo = {
|
||||
msg?: string;
|
||||
fields?: FieldError[];
|
||||
}
|
||||
|
||||
interface Service {
|
||||
registerNewUser(user: NewUser, onSuccess: () => void, onError: (msg: string) => void): void;
|
||||
resetPassword(email: string, onSuccess: () => void, onError: (msg: string) => void): void;
|
||||
registerNewUser(user: NewUser, onSuccess: () => void, onError: (errorInfo: ErrorInfo) => void):void;
|
||||
resetPassword(email: string, onSuccess: () => void, onError: (errorInfo: ErrorInfo) => void): void;
|
||||
fetchAllMaps(): Promise<MapInfo[]>;
|
||||
|
||||
deleteMap(id: number): Promise<void>;
|
||||
remameMap(id: number, basicInfo: BasicMapInfo): Promise<void>;
|
||||
loadMapInfo(id: number): Promise<BasicMapInfo>;
|
||||
}
|
||||
|
||||
class RestService implements Service {
|
||||
@ -22,7 +50,19 @@ class RestService implements Service {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
async registerNewUser(user: NewUser, onSuccess: () => void, onError: (msg: string) => void) {
|
||||
loadMapInfo(id: number): Promise<BasicMapInfo> {
|
||||
return Promise.resolve({ name: 'My Map', description: 'My Descition' });
|
||||
}
|
||||
|
||||
async remameMap(id: number, basicInfo: BasicMapInfo) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async deleteMap(id: number): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async registerNewUser(user: NewUser, onSuccess: () => void, onError: (errorInfo: ErrorInfo) => void) {
|
||||
|
||||
await axios.post(this.baseUrl + '/service/users',
|
||||
JSON.stringify(user),
|
||||
@ -37,7 +77,29 @@ class RestService implements Service {
|
||||
});
|
||||
}
|
||||
|
||||
async resetPassword(email: string, onSuccess: () => void, onError: (msg: string) => void) {
|
||||
async fetchAllMaps(): Promise<MapInfo[]> {
|
||||
function createMapInfo(
|
||||
id: number,
|
||||
starred: boolean,
|
||||
name: string,
|
||||
labels: [string],
|
||||
creator: string,
|
||||
modified: number,
|
||||
): MapInfo {
|
||||
return { id, name, labels, creator, modified, starred };
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
async resetPassword(email: string, onSuccess: () => void, onError: (errorInfo: ErrorInfo) => void) {
|
||||
await axios.put(this.baseUrl + '/service/users/resetPassword?email=' + email,
|
||||
null,
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
@ -46,13 +108,14 @@ class RestService implements Service {
|
||||
onSuccess();
|
||||
}).catch(error => {
|
||||
const response = error.response;
|
||||
const errorMsg = this.parseResponseOnError(response);
|
||||
onError(errorMsg);
|
||||
const errorInfo: ErrorInfo = this.parseResponseOnError(response);
|
||||
onError(errorInfo);
|
||||
});
|
||||
}
|
||||
|
||||
private parseResponseOnError = (response: any) => {
|
||||
let msg;
|
||||
private parseResponseOnError = (response: any): ErrorInfo => {
|
||||
|
||||
let result: ErrorInfo | undefined;
|
||||
if (response) {
|
||||
const status: number = response.status;
|
||||
const data = response.data;
|
||||
@ -64,30 +127,35 @@ class RestService implements Service {
|
||||
break;
|
||||
default:
|
||||
if (data) {
|
||||
let errors: string[] = [];
|
||||
// Set global errorrs ...
|
||||
if (data.globalErrors) {
|
||||
errors = data.globalErrors;
|
||||
} else if (data.fieldErrors) {
|
||||
errors = Object.values(data.fieldErrors);
|
||||
}
|
||||
|
||||
let msg;
|
||||
let errors = data.globalErrors;
|
||||
if (errors.length > 0) {
|
||||
msg = errors[0];
|
||||
}
|
||||
result = { msg: errors };
|
||||
}
|
||||
|
||||
// Set field errors ...
|
||||
if (data.fieldErrors) {
|
||||
// @Todo: Fix this ...
|
||||
result = { msg: data.fieldErrors };
|
||||
}
|
||||
|
||||
} else {
|
||||
msg = response.statusText;
|
||||
result = { msg: response.statusText };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Network related problem ...
|
||||
if (!msg) {
|
||||
msg = 'Unexpected error. Please, try latter';
|
||||
if (!result) {
|
||||
result = { msg: 'Unexpected error. Please, try latter' };
|
||||
}
|
||||
|
||||
return msg;
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
export { Service, RestService, NewUser }
|
||||
export { Service, RestService }
|
@ -38,11 +38,15 @@ module.exports = {
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin(),
|
||||
// @FixMe: Fatten the dir ...
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [{
|
||||
from: 'public/*',
|
||||
to: '[name].[ext]'
|
||||
to: '[name].[ext]',
|
||||
globOptions: {
|
||||
ignore: [
|
||||
'**/index.html'
|
||||
]
|
||||
}
|
||||
}]
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
|
Loading…
Reference in New Issue
Block a user