Complete menu navigation

This commit is contained in:
Paulo Gustavo Veiga 2021-01-31 18:04:50 -08:00
parent db934afed8
commit a1867168ec
20 changed files with 288 additions and 161 deletions

View File

@ -6,7 +6,7 @@
"defaultMessage": "Close"
},
"action.delete": {
"defaultMessage": "Delete"
"defaultMessage": "History"
},
"action.delete-description": {
"defaultMessage": "Deleted mindmap can not be recovered. Do you want to continue ?."
@ -26,6 +26,9 @@
"action.info-title": {
"defaultMessage": "Info"
},
"action.label": {
"defaultMessage": "Add Label"
},
"action.open": {
"defaultMessage": "Open"
},
@ -120,6 +123,12 @@
"login.userinactive": {
"defaultMessage": "Sorry, your account has not been activated yet. You'll receive a notification email when it becomes active. Stay tuned!."
},
"menu.account": {
"defaultMessage": "Account"
},
"menu.signout": {
"defaultMessage": "Sign Out"
},
"registration.desc": {
"defaultMessage": "Signing up is free and just take a moment"
},

View File

@ -14,7 +14,7 @@
"action.delete": [
{
"type": 0,
"value": "Delete"
"value": "History"
}
],
"action.delete-description": [
@ -53,6 +53,12 @@
"value": "Info"
}
],
"action.label": [
{
"type": 0,
"value": "Add Label"
}
],
"action.open": [
{
"type": 0,
@ -239,6 +245,18 @@
"value": "Sorry, your account has not been activated yet. You'll receive a notification email when it becomes active. Stay tuned!."
}
],
"menu.account": [
{
"type": 0,
"value": "Account"
}
],
"menu.signout": [
{
"type": 0,
"value": "Sign Out"
}
],
"registration.desc": [
{
"type": 0,

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react'
import { FormattedMessage, useIntl } from 'react-intl'
import { useHistory } from "react-router-dom"
import { Service, ErrorInfo } from '../../services/Service'
import Service, { ErrorInfo } from '../../services'
import Header from '../layout/header'
import Footer from '../layout/footer'

View File

@ -1,5 +1,5 @@
import React from "react";
import { ErrorInfo } from "../../../services/Service"
import { ErrorInfo } from "../../../services"
import StyledAlert from "./styled";

View File

@ -1,7 +1,7 @@
import { TextField } from "@material-ui/core";
import React, { ChangeEvent } from "react";
import { MessageDescriptor, useIntl } from "react-intl";
import { ErrorInfo } from "../../../services/Service";
import { ErrorInfo } from "../../../services";
type InputProps = {
name: string;

View File

@ -36,6 +36,7 @@ const ActionChooser = (props: ActionProps) => {
keepMounted
open={Boolean(anchor)}
onClose={handleOnClose(undefined)}
elevation={1}
>
<MenuItem onClick={handleOnClose('open')} style={{width:"220px"}}>
<ListItemIcon>

View File

@ -1,7 +1,7 @@
import React from "react";
import { Button, DialogContentText } from "@material-ui/core";
import { FormattedMessage, useIntl } from "react-intl";
import { ErrorInfo } from "../../../../services/Service";
import { ErrorInfo } from "../../../../services";
import { StyledDialog, StyledDialogActions, StyledDialogContent, StyledDialogTitle } from "./style";
import GlobalError from "../../../form/global-error";

View File

@ -3,7 +3,7 @@ import React from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useMutation, useQueryClient } from "react-query";
import { useSelector } from "react-redux";
import { Service } from "../../../../services/Service";
import Service from "../../../../services";
import { activeInstance } from '../../../../reducers/serviceSlice';
import { DialogProps, fetchMapById, handleOnMutationSuccess } from "..";
import BaseDialog from "../action-dialog";

View File

@ -4,7 +4,7 @@ import { useMutation, useQueryClient } from "react-query";
import { useSelector } from "react-redux";
import { FormControl } from "@material-ui/core";
import { BasicMapInfo, ErrorInfo, Service } from "../../../../services/Service";
import Service, { BasicMapInfo, ErrorInfo } from "../../../../services";
import { activeInstance } from '../../../../reducers/serviceSlice';
import Input from "../../../form/input";
import { DialogProps, fetchMapById, handleOnMutationSuccess } from "..";

View File

@ -2,7 +2,8 @@ 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 { ErrorInfo, MapInfo } from '../../../services';
import Service from '../../../services';
import { useSelector } from 'react-redux';
import { QueryClient, useQuery } from 'react-query';
import { activeInstance } from '../../../reducers/serviceSlice';

View File

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

View File

@ -2,7 +2,7 @@ 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 Service, { BasicMapInfo, ErrorInfo } from "../../../../services";
import { activeInstance } from '../../../../reducers/serviceSlice';
import { DialogProps, fetchMapById, handleOnMutationSuccess } from "..";
import Input from "../../../form/input";

View File

@ -7,31 +7,40 @@ import List from '@material-ui/core/List';
import IconButton from '@material-ui/core/IconButton';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import { ListItemTextStyled, useStyles } from './style';
import { AccountCircle, AddCircleTwoTone, BlurCircular, CloudUploadTwoTone, DeleteOutlineTwoTone, EmailOutlined, EmojiPeopleOutlined, ExitToAppOutlined, FeedbackOutlined, Help, LabelTwoTone, PolicyOutlined, PublicTwoTone, SettingsApplicationsOutlined, ShareTwoTone, StarRateTwoTone, Translate, TranslateTwoTone } from '@material-ui/icons';
import { useStyles } from './style';
import { AccountCircle, AddCircleTwoTone, BlurCircular, CloudUploadTwoTone, EmailOutlined, EmojiPeopleOutlined, ExitToAppOutlined, FeedbackOutlined, Help, LabelTwoTone, PolicyOutlined, PublicTwoTone, SettingsApplicationsOutlined, ShareTwoTone, StarRateTwoTone, Translate, TranslateTwoTone } from '@material-ui/icons';
import InboxTwoToneIcon from '@material-ui/icons/InboxTwoTone';
import { Button, Link, ListItemSecondaryAction, Menu, MenuItem, Tooltip } from '@material-ui/core';
import { Button, Link, ListItemSecondaryAction, ListItemText, Menu, MenuItem, Tooltip } from '@material-ui/core';
import { MapsList } from './maps-list';
import { FormattedMessage } from 'react-intl';
import { useQueryClient } from 'react-query';
const logoIcon = require('../../images/logo-small.svg')
const poweredByIcon = require('../../images/pwrdby-white.svg')
type FilterType = 'public' | 'all' | 'starred' | 'shared' | 'label' | 'owned'
export type Filter = GenericFilter | LabelFilter;
interface Filter {
type: FilterType
interface GenericFilter {
type: 'public' | 'all' | 'starred' | 'shared' | 'label' | 'owned';
}
interface LabelFinter extends Filter {
interface LabelFilter {
type: 'label',
label: string
}
const MapsPage = (props: any) => {
const classes = useStyles();
const [filter, setFilter] = React.useState<Filter>({ type: 'all' });
const queryClient = useQueryClient();
useEffect(() => {
document.title = 'Maps | WiseMapping';
}, []);
const handleMenuClick = (filter: Filter) => {
setFilter(filter);
};
return (
<div className={classes.root}>
<AppBar
@ -88,53 +97,43 @@ const MapsPage = (props: any) => {
</div>
<List component="nav">
<ListItem button >
<ListItemIcon>
<InboxTwoToneIcon color="secondary" />
</ListItemIcon>
<ListItemTextStyled primary="All" />
</ListItem>
<ListItem button >
<ListItemIcon>
<BlurCircular color="secondary" />
</ListItemIcon>
<ListItemTextStyled primary="Owned" />
</ListItem>
<ListItem button >
<ListItemIcon>
<StarRateTwoTone color="secondary" />
</ListItemIcon>
<ListItemTextStyled primary="Starred" />
</ListItem>
<ListItem button >
<ListItemIcon>
<ShareTwoTone color="secondary" />
</ListItemIcon>
<ListItemTextStyled primary="Shared With Me" />
</ListItem>
<ListItem button >
<ListItemIcon>
<PublicTwoTone color="secondary" />
</ListItemIcon>
<ListItemTextStyled primary="Public" />
</ListItem>
<ListItem button >
<ListItemIcon>
<LabelTwoTone color="secondary" />
</ListItemIcon>
<ListItemTextStyled primary="Some label>" />
<ListItemSecondaryAction>
<IconButton edge="end" aria-label="delete">
<DeleteOutlineTwoTone color="secondary" />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
<StyleListItem
icon={<InboxTwoToneIcon color="secondary" />}
label={"All"}
filter={{ type: 'all' }}
active={filter}
onClick={handleMenuClick}
/>
<StyleListItem
icon={<BlurCircular color="secondary" />}
label={"Owned"}
filter={{ type: 'owned' }}
active={filter}
onClick={handleMenuClick}
/>
<StyleListItem
icon={<StarRateTwoTone color="secondary" />}
label={"Starred"}
filter={{ type: 'starred' }}
active={filter}
onClick={handleMenuClick}
/>
<StyleListItem
icon={<ShareTwoTone color="secondary" />}
label={"Shared With Me"}
filter={{ type: 'shared' }}
active={filter}
onClick={handleMenuClick}
/>
<StyleListItem
icon={<PublicTwoTone color="secondary" />}
label={"Public"}
filter={{ type: 'public' }}
active={filter}
onClick={handleMenuClick}
/>
</List>
<div style={{ position: 'absolute', bottom: '10px', left: '20px' }}>
<Link href="http://www.wisemapping.org/">
<img src={poweredByIcon} alt="Powered By WiseMapping" />
@ -143,12 +142,50 @@ const MapsPage = (props: any) => {
</Drawer>
<main className={classes.content}>
<div className={classes.toolbar} />
<MapsList />
<MapsList filter={filter} />
</main>
</div>
);
}
interface ListItemProps {
icon: any,
label: string,
filter: Filter,
active?: Filter
onClick: (filter: Filter) => void;
}
const StyleListItem = (props: ListItemProps) => {
const icon = props.icon;
const label = props.label;
const filter = props.filter;
const activeType = props.active?.type;
const onClick = props.onClick;
const handleOnClick = (event: any, filter: Filter) => {
// Invalidate cache to provide a fresh load ...
event.stopPropagation();
onClick(filter);
}
return (
<ListItem button selected={activeType == filter.type} onClick={e => { handleOnClick(e, filter) }}>
<ListItemIcon>
{icon}
</ListItemIcon>
<ListItemText style={{ color: 'white' }} primary={label} />
{/* <ListItemSecondaryAction>
<IconButton edge="end" aria-label="delete">
<DeleteOutlineTwoTone color="secondary" />
</IconButton>
</ListItemSecondaryAction> */}
</ListItem>
);
}
const ProfileToobarButton = () => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
@ -163,11 +200,14 @@ const ProfileToobarButton = () => {
return (
<span>
<IconButton
<Tooltip title="Paulo Veiga <pveiga@gmail.com>">
<Button
aria-haspopup="true"
onClick={handleMenu}>
<AccountCircle fontSize="large" />
</IconButton >
Paulo Veiga
</Button >
</Tooltip>
<Menu id="appbar-profile"
anchorEl={anchorEl}
keepMounted

View File

@ -1,4 +1,4 @@
import React from 'react'
import React, { useEffect } from 'react'
import { useStyles } from './styled';
import Table from '@material-ui/core/Table';
@ -21,12 +21,14 @@ import { CSSProperties } from 'react';
import { useSelector } from 'react-redux';
import { activeInstance } from '../../../reducers/serviceSlice';
import { useMutation, useQuery, useQueryClient } from 'react-query';
import { ErrorInfo, MapInfo, Service } from '../../../services/Service';
import { ErrorInfo, MapInfo } from '../../../services';
import Service from '../../../services';
import ActionChooser, { ActionType } from '../action-chooser';
import ActionDispatcher from '../action-dispatcher';
import { InputBase, Link } from '@material-ui/core';
import SearchIcon from '@material-ui/icons/Search';
import moment from 'moment'
import { Filter } from '..';
function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
@ -139,21 +141,66 @@ type ActionPanelState = {
mapId: number
}
export const MapsList = () => {
interface MapsListProps {
filter: Filter
}
const mapsFilter = (filter: Filter, search: string): ((mapInfo: MapInfo) => boolean) => {
return (mapInfo: MapInfo) => {
// Check for filter condition
let result = false;
switch (filter.type) {
case 'all':
result = true;
break;
case 'public':
result = mapInfo.isPublic;
break;
case 'starred':
result = mapInfo.starred;
break;
default:
result = false;
}
// Does it match search filter criteria...
if (search && result) {
result = mapInfo.name.toLowerCase().indexOf(search.toLowerCase()) != -1;
}
return result;
}
}
export const MapsList = (props: MapsListProps) => {
const classes = useStyles();
const [order, setOrder] = React.useState<Order>('asc');
const [filter, setFilter] = React.useState<Filter>({ type: 'all' });
const [orderBy, setOrderBy] = React.useState<keyof MapInfo>('modified');
const [selected, setSelected] = React.useState<number[]>([]);
const [searchCondition, setSearchCondition] = React.useState<string>('');
const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(5);
const [rowsPerPage, setRowsPerPage] = React.useState(10);
const service: Service = useSelector(activeInstance);
const { isLoading, error, data } = useQuery<unknown, ErrorInfo, MapInfo[]>('maps', async () => {
useEffect(() => {
console.log("Update maps state.")
setSelected([]);
setSearchCondition('');
setPage(0);
setFilter(props.filter)
queryClient.invalidateQueries('maps');
const result = await service.fetchAllMaps();
return result;
}, [props.filter.type]);
const { isLoading, error, data } = useQuery<unknown, ErrorInfo, MapInfo[]>('maps', async () => {
return await service.fetchAllMaps();
});
const mapsInfo: MapInfo[] = data ? data : [];
const mapsInfo: MapInfo[] = data ? data.filter(mapsFilter(filter, searchCondition)) : [];
const [activeRowAction, setActiveRowAction] = React.useState<ActionPanelState | undefined>(undefined);
@ -236,8 +283,6 @@ export const MapsList = () => {
const handleStarred = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>, id: number) => {
event.stopPropagation();
event.preventDefault();
starredMultation.mutate(id);
}
@ -253,9 +298,11 @@ export const MapsList = () => {
setActiveRowAction(undefined);
};
const isSelected = (id: number) => selected.indexOf(id) !== -1;
const emptyRows = rowsPerPage - Math.min(rowsPerPage, mapsInfo.length - page * rowsPerPage);
const handleOnSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchCondition(e.target.value);
}
const isSelected = (id: number) => selected.indexOf(id) !== -1;
return (
<div className={classes.root}>
<Paper className={classes.paper} elevation={0}>
@ -276,7 +323,6 @@ export const MapsList = () => {
<div className={classes.toolbarListActions}>
<TablePagination
style={{ float: 'right', border: "0", paddingBottom: "5px" }}
rowsPerPageOptions={[50]}
count={mapsInfo.length}
rowsPerPage={rowsPerPage}
page={page}
@ -295,6 +341,7 @@ export const MapsList = () => {
input: classes.searchInputInput,
}}
inputProps={{ 'aria-label': 'search' }}
onChange={handleOnSearchChange}
/>
</div>
</div>

View File

@ -17,7 +17,10 @@ export const useStyles = makeStyles((theme: Theme) =>
'& tr:nth-child(odd)':
{
background: 'rgba(221, 221, 221, 0.35)'
}
},
// '&:hover tr': {
// backgroundColor: 'rgba(150, 150, 150, 0.7)',
// }
},
headerCell: {
background: 'white',
@ -26,7 +29,7 @@ export const useStyles = makeStyles((theme: Theme) =>
border: 0
},
bodyCell: {
border: 0
border: '0px'
},
visuallyHidden: {
border: 0,
@ -66,7 +69,7 @@ export const useStyles = makeStyles((theme: Theme) =>
float: 'right'
},
searchIcon: {
padding: '5px 0 0 5px',
padding: '6px 0 0 5px',
height: '100%',
position: 'absolute',
pointerEvents: 'none',

View File

@ -66,8 +66,7 @@ export const useStyles = makeStyles((theme: Theme) =>
toolbar: {
display: 'flex',
justifyContent: 'flex-end',
// necessary for content to be below app bar
...theme.mixins.toolbar,
minHeight: '44px'
},
content: {
flexGrow: 1,
@ -75,11 +74,3 @@ export const useStyles = makeStyles((theme: Theme) =>
}
}),
);
export const ListItemTextStyled = withStyles({
root:
{
color: 'white',
}
})(ListItemText);

View File

@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import ReCAPTCHA from 'react-google-recaptcha';
import { useHistory } from 'react-router-dom';
import { ErrorInfo, Service } from '../../services/Service';
import Service , { ErrorInfo} from '../../services';
import FormContainer from '../layout/form-container';
import Header from '../layout/header';

View File

@ -1,7 +1,7 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import axios from 'axios';
import { ErrorInfo } from 'react';
import { RestService, Service } from '../services/Service';
import Service from '../services';
import MockService from '../services/mock-service';
type RutimeConfig = {
apiBaseUrl: string;
@ -38,7 +38,7 @@ interface ServiceState {
}
const initialState: ServiceState = {
instance: new RestService("", () => { console.log("401 error") })
instance: new MockService("", () => { console.log("401 error") })
};
export const serviceSlice = createSlice({
@ -46,7 +46,7 @@ export const serviceSlice = createSlice({
initialState: initialState,
reducers: {
initialize(state, action: PayloadAction<void[]>) {
state.instance = new RestService("", () => { console.log("401 error") });
state.instance = new MockService("", () => { console.log("401 error") });
}
},
});

View File

@ -0,0 +1,48 @@
export type NewUser = {
email: string;
firstname: string;
lastname: string;
password: string;
recaptcha: string | null;
}
export type MapInfo = {
id: number;
starred: boolean;
name: string;
labels: string[];
creator: string;
modified: number;
description: string;
isPublic: boolean;
}
export type BasicMapInfo = {
name: string;
description?: string;
}
export type FieldError = {
id: string,
msg: string
}
export type ErrorInfo = {
msg?: string;
fields?: Map<String, String>;
}
interface Service {
registerNewUser(user: NewUser): Promise<void>;
resetPassword(email: string): Promise<void>;
fetchAllMaps(): Promise<MapInfo[]>;
deleteMap(id: number): Promise<void>;
renameMap(id: number, basicInfo: BasicMapInfo): Promise<void>;
duplicateMap(id: number, basicInfo: BasicMapInfo): Promise<void>;
loadMapInfo(id: number): Promise<BasicMapInfo>;
changeStarred(id: number): Promise<void>;
}
export default Service;

View File

@ -1,49 +1,6 @@
import axios from 'axios'
export type NewUser = {
email: string;
firstname: string;
lastname: string;
password: string;
recaptcha: string | null;
}
export type MapInfo = {
id: number;
starred: boolean;
name: string;
labels: string[];
creator: string;
modified: number;
description: string;
}
export type BasicMapInfo = {
name: string;
description?: string;
}
export type FieldError = {
id: string,
msg: string
}
export type ErrorInfo = {
msg?: string;
fields?: Map<String, String>;
}
interface Service {
registerNewUser(user: NewUser): Promise<void>;
resetPassword(email: string): Promise<void>;
fetchAllMaps(): Promise<MapInfo[]>;
deleteMap(id: number): Promise<void>;
renameMap(id: number, basicInfo: BasicMapInfo): Promise<void>;
duplicateMap(id: number, basicInfo: BasicMapInfo): Promise<void>;
loadMapInfo(id: number): Promise<BasicMapInfo>;
changeStarred(id: number): Promise<void>;
}
import { BasicMapInfo, ErrorInfo, MapInfo, NewUser } from "..";
import Service from "..";
import axios from "axios";
class MockService implements Service {
private baseUrl: string;
@ -61,14 +18,24 @@ class MockService implements Service {
labels: string[],
creator: string,
modified: number,
description: string
description: string,
isPublic: boolean
): MapInfo {
return { id, name, labels, creator, modified, starred, description };
return { id, name, labels, creator, modified, starred, description, isPublic };
}
this.maps = [
createMapInfo(1, true, "El Mapa", [""], "Paulo", 67, ""),
createMapInfo(2, false, "El Mapa2", [""], "Paulo2", 67, ""),
createMapInfo(3, false, "El Mapa3", [""], "Paulo3", 67, "")
createMapInfo(1, true, "El Mapa", [""], "Paulo", 67, "", true),
createMapInfo(2, false, "El Mapa2", [""], "Paulo2", 67, "", false),
createMapInfo(3, false, "El Mapa3", [""], "Paulo3", 67, "", false),
createMapInfo(4, false, "El Mapa3", [""], "Paulo3", 67, "", false),
createMapInfo(5, false, "El Mapa3", [""], "Paulo3", 67, "", false),
createMapInfo(6, false, "El Mapa3", [""], "Paulo3", 67, "", false),
createMapInfo(7, false, "El Mapa3", [""], "Paulo3", 67, "", false),
createMapInfo(8, false, "El Mapa3", [""], "Paulo3", 67, "", false),
createMapInfo(9, false, "El Mapa3", [""], "Paulo3", 67, "", false),
createMapInfo(10, false, "El Mapa3", [""], "Paulo3", 67, "", false),
createMapInfo(11, false, "El Mapa3", [""], "Paulo3", 67, "", false),
createMapInfo(12, false, "El Mapa3", [""], "Paulo3", 67, "", false)
];
}
@ -125,7 +92,8 @@ class MockService implements Service {
starred: false,
creator: "current user",
labels: [],
modified: -1
modified: -1,
isPublic: false
};
this.maps.push(newMap);
return Promise.resolve();
@ -164,6 +132,7 @@ class MockService implements Service {
}
fetchAllMaps(): Promise<MapInfo[]> {
console.log("Fetch maps from server")
return Promise.resolve(this.maps);
}
@ -229,6 +198,6 @@ class MockService implements Service {
return result;
}
}
export { Service, MockService as RestService }
export default MockService;