diff --git a/packages/webapp/src/classes/client/index.ts b/packages/webapp/src/classes/client/index.ts index a2a6f0e9..a54cd408 100644 --- a/packages/webapp/src/classes/client/index.ts +++ b/packages/webapp/src/classes/client/index.ts @@ -22,6 +22,8 @@ export type Label = { iconName: string; } +export type Role = 'owner' | 'editor' | 'viewer'; + export type MapInfo = { id: number; starred: boolean; @@ -33,7 +35,7 @@ export type MapInfo = { lastModificationTime: string; description: string; isPublic: boolean; - role: 'owner' | 'editor' | 'viewer' + role: Role; } export type ChangeHistory = { @@ -64,6 +66,12 @@ export type AccountInfo = { locale: Locale; } +export type Permission = { + name?: string; + email: string; + role: Role; +} + interface Client { deleteAccount(): Promise importMap(model: ImportMapInfo): Promise @@ -72,11 +80,14 @@ interface Client { deleteMap(id: number): Promise; renameMap(id: number, basicInfo: BasicMapInfo): Promise; fetchAllMaps(): Promise; + fetchMapPermissions(id: number): Promise; + addMapPermissions(id: number, message: string, permissions: Permission[]): Promise; + duplicateMap(id: number, basicInfo: BasicMapInfo): Promise; - + updateAccountLanguage(locale: LocaleCode): Promise; updateAccountPassword(pasword: string): Promise; - updateAccountInfo(firstname: string,lastname: string): Promise; + updateAccountInfo(firstname: string, lastname: string): Promise; updateStarred(id: number, starred: boolean): Promise; updateMapToPublic(id: number, starred: boolean): Promise; diff --git a/packages/webapp/src/classes/client/mock-client/index.ts b/packages/webapp/src/classes/client/mock-client/index.ts index 09d610f0..56baef53 100644 --- a/packages/webapp/src/classes/client/mock-client/index.ts +++ b/packages/webapp/src/classes/client/mock-client/index.ts @@ -1,8 +1,10 @@ -import Client, { AccountInfo, BasicMapInfo, ChangeHistory, ImportMapInfo, Label, MapInfo, NewUser } from '..'; +import Client, { AccountInfo, BasicMapInfo, ChangeHistory, ImportMapInfo, Label, MapInfo, NewUser, Permission } from '..'; import { LocaleCode, localeFromStr } from '../../app-i18n'; + class MockClient implements Client { private maps: MapInfo[] = []; private labels: Label[] = []; + private permissionsByMap: Map = new Map(); constructor() { @@ -22,6 +24,7 @@ class MockClient implements Client { ): MapInfo { return { id, title, labels, createdBy: creator, creationTime, lastModificationBy: modifiedByUser, lastModificationTime: modifiedTime, starred, description, isPublic, role }; } + this.maps = [ createMapInfo(1, true, "El Mapa", [], "Paulo", "2008-06-02T00:00:00Z", "Berna", "2008-06-02T00:00:00Z", "", true, 'owner'), createMapInfo(11, false, "El Mapa3", [1, 2, 3], "Paulo3", "2008-06-02T00:00:00Z", "Berna", "2008-06-02T00:00:00Z", "", false, 'editor'), @@ -32,7 +35,28 @@ class MockClient implements Client { { id: 1, title: "Red Label", iconName: "", color: 'red' }, { id: 2, title: "Blue Label", iconName: "", color: 'blue' } ]; + } + addMapPermissions(id: number, message: string, permissions: Permission[]): Promise { + let perm = this.permissionsByMap.get(id) || []; + perm = perm.concat(permissions); + this.permissionsByMap.set(id, perm); + + console.log(`Message ${message}`) + return Promise.resolve(); + } + + fetchMapPermissions(id: number): Promise { + let perm = this.permissionsByMap.get(id); + if (!perm) { + perm = [{ + name: 'Cosme Sharing', + email: 'pepe@gmail.com', + role: 'editor' + }]; + this.permissionsByMap.set(id, perm); + } + return Promise.resolve(perm); } deleteAccount(): Promise { diff --git a/packages/webapp/src/classes/client/rest-client/index.ts b/packages/webapp/src/classes/client/rest-client/index.ts index 812c94a4..b9835b95 100644 --- a/packages/webapp/src/classes/client/rest-client/index.ts +++ b/packages/webapp/src/classes/client/rest-client/index.ts @@ -1,5 +1,5 @@ import axios from 'axios'; -import Client, { ErrorInfo, MapInfo, BasicMapInfo, NewUser, Label, ChangeHistory, AccountInfo, ImportMapInfo } from '..'; +import Client, { ErrorInfo, MapInfo, BasicMapInfo, NewUser, Label, ChangeHistory, AccountInfo, ImportMapInfo, Permission } from '..'; import { LocaleCode, localeFromStr, Locales } from '../../app-i18n'; export default class RestClient implements Client { @@ -10,6 +10,15 @@ export default class RestClient implements Client { this.baseUrl = baseUrl; this.sessionExpired = sessionExpired; } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + addMapPermissions(id: number, message: string, permissions: Permission[]): Promise { + throw new Error('Method not implemented.'); + } + + fetchMapPermissions(id: number): Promise { + throw new Error('Method not implemented.' + id); + } + deleteAccount(): Promise { const handler = (success: () => void, reject: (error: ErrorInfo) => void) => { axios.delete(this.baseUrl + `/c/restful/account`, diff --git a/packages/webapp/src/components/login-page/index.tsx b/packages/webapp/src/components/login-page/index.tsx index 1f1febe6..d2a68c85 100644 --- a/packages/webapp/src/components/login-page/index.tsx +++ b/packages/webapp/src/components/login-page/index.tsx @@ -24,7 +24,7 @@ const ConfigStatusMessage = ({ enabled = false }: ConfigStatusProps): React.Reac

); } - return result ? result : null; + return result || null; } const LoginError = () => { diff --git a/packages/webapp/src/components/maps-page/account-menu/index.tsx b/packages/webapp/src/components/maps-page/account-menu/index.tsx index b31c5ebe..326d3ea5 100644 --- a/packages/webapp/src/components/maps-page/account-menu/index.tsx +++ b/packages/webapp/src/components/maps-page/account-menu/index.tsx @@ -32,7 +32,7 @@ const AccountMenu = (): React.ReactElement => { const account = fetchAccount(); return ( - `}> + `}> diff --git a/packages/webapp/src/components/maps-page/action-dispatcher/base-dialog/index.tsx b/packages/webapp/src/components/maps-page/action-dispatcher/base-dialog/index.tsx index 8dc7e0c6..2e71a9ec 100644 --- a/packages/webapp/src/components/maps-page/action-dispatcher/base-dialog/index.tsx +++ b/packages/webapp/src/components/maps-page/action-dispatcher/base-dialog/index.tsx @@ -5,6 +5,7 @@ import { StyledDialog, StyledDialogActions, StyledDialogContent, StyledDialogTit import GlobalError from "../../../form/global-error"; import DialogContentText from "@material-ui/core/DialogContentText"; import Button from "@material-ui/core/Button"; +import { PaperProps } from "@material-ui/core/Paper"; export type DialogProps = { onClose: () => void; @@ -18,10 +19,11 @@ export type DialogProps = { submitButton?: string; actionUrl?: string; maxWidth?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | false; + PaperProps?: Partial; } const BaseDialog = (props: DialogProps): React.ReactElement => { - const { onClose, onSubmit, maxWidth = 'sm' } = props; + const { onClose, onSubmit, maxWidth = 'sm', PaperProps } = props; const handleOnSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -31,13 +33,13 @@ const BaseDialog = (props: DialogProps): React.ReactElement => { } const description = props.description ? ({props.description}) : null; - return (
diff --git a/packages/webapp/src/components/maps-page/action-dispatcher/delete-dialog/index.tsx b/packages/webapp/src/components/maps-page/action-dispatcher/delete-dialog/index.tsx index 3c0e48e4..27a24303 100644 --- a/packages/webapp/src/components/maps-page/action-dispatcher/delete-dialog/index.tsx +++ b/packages/webapp/src/components/maps-page/action-dispatcher/delete-dialog/index.tsx @@ -2,7 +2,7 @@ import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useMutation, useQueryClient } from "react-query"; import { useSelector } from "react-redux"; -import Client from "../../../../classes/client"; +import Client, { ErrorInfo } from "../../../../classes/client"; import { activeInstance, fetchMapById } from '../../../../redux/clientSlice'; import { SimpleDialogProps, handleOnMutationSuccess } from ".."; import BaseDialog from "../base-dialog"; @@ -14,10 +14,14 @@ const DeleteDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElement const intl = useIntl(); const client: Client = useSelector(activeInstance); const queryClient = useQueryClient(); + const [error, setError] = React.useState(); const mutation = useMutation((id: number) => client.deleteMap(id), { - onSuccess: () => handleOnMutationSuccess(onClose, queryClient) + onSuccess: () => handleOnMutationSuccess(onClose, queryClient), + onError: (error: ErrorInfo) => { + setError(error); + } } ); @@ -35,6 +39,7 @@ const DeleteDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElement return (
diff --git a/packages/webapp/src/components/maps-page/action-dispatcher/delete-multiselect-dialog/index.tsx b/packages/webapp/src/components/maps-page/action-dispatcher/delete-multiselect-dialog/index.tsx index 7e1731e9..92749fad 100644 --- a/packages/webapp/src/components/maps-page/action-dispatcher/delete-multiselect-dialog/index.tsx +++ b/packages/webapp/src/components/maps-page/action-dispatcher/delete-multiselect-dialog/index.tsx @@ -21,7 +21,10 @@ const DeleteMultiselectDialog = ({ onClose, mapsId }: DeleteMultiselectDialogPro const mutation = useMutation((ids: number[]) => client.deleteMaps(ids), { - onSuccess: () => handleOnMutationSuccess(onClose, queryClient) + onSuccess: () => handleOnMutationSuccess(onClose, queryClient), + onError: (error) => { + console.error(`Unexpected error ${error}`); + } } ); diff --git a/packages/webapp/src/components/maps-page/action-dispatcher/share-dialog/index.tsx b/packages/webapp/src/components/maps-page/action-dispatcher/share-dialog/index.tsx index 5bfde05a..53563668 100644 --- a/packages/webapp/src/components/maps-page/action-dispatcher/share-dialog/index.tsx +++ b/packages/webapp/src/components/maps-page/action-dispatcher/share-dialog/index.tsx @@ -1,15 +1,16 @@ import React from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { useMutation, useQueryClient } from "react-query"; +import { useMutation, useQuery, useQueryClient } from "react-query"; import { useSelector } from "react-redux"; -import Client from "../../../../classes/client"; +import Client, { ErrorInfo, Permission } from "../../../../classes/client"; import { activeInstance } from '../../../../redux/clientSlice'; -import { SimpleDialogProps, handleOnMutationSuccess } from ".."; +import { SimpleDialogProps } from ".."; import BaseDialog from "../base-dialog"; import List from "@material-ui/core/List"; import ListItem from "@material-ui/core/ListItem"; import ListItemText from "@material-ui/core/ListItemText"; import IconButton from "@material-ui/core/IconButton"; + import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction"; import DeleteIcon from '@material-ui/icons/Delete'; import Paper from "@material-ui/core/Paper"; @@ -19,16 +20,41 @@ import Button from "@material-ui/core/Button"; import TextField from "@material-ui/core/TextField"; import FormControlLabel from "@material-ui/core/FormControlLabel"; import Checkbox from "@material-ui/core/Checkbox"; +import Typography from "@material-ui/core/Typography"; +import { useStyles } from "./style"; +import RoleIcon from "../../role-icon"; +type ShareModel = { + emails: string, + role: 'editor' | 'viewer', + message: string +} + +const defaultModel: ShareModel = { emails: '', role: 'editor', message: '' }; const ShareDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElement => { const intl = useIntl(); const client: Client = useSelector(activeInstance); const queryClient = useQueryClient(); + const classes = useStyles(); + const [showMessage, setShowMessage] = React.useState(false); + const [model, setModel] = React.useState(defaultModel); + const [error, setError] = React.useState(); - const mutation = useMutation((id: number) => client.deleteMap(id), + const mutation = useMutation( + (model: ShareModel) => { + const emails = model.emails.split("'"); + const permissions = emails.map((email) => { return { email: email, role: model.role } }); + return client.addMapPermissions(mapId, model.message, permissions); + }, { - onSuccess: () => handleOnMutationSuccess(onClose, queryClient) + onSuccess: () => { + queryClient.invalidateQueries(`perm-${mapId}`); + setModel(defaultModel); + }, + onError: (error: ErrorInfo) => { + setError(error); + } } ); @@ -36,65 +62,117 @@ const ShareDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElement onClose(); }; - const handleOnSubmit = (): void => { - mutation.mutate(mapId); + const handleOnChange = (event: React.ChangeEvent): void => { + event.preventDefault(); + + const name = event.target.name; + const value = event.target.value; + setModel({ ...model, [name as keyof ShareModel]: value }); } - // Fetch map model to be rendered ... + const handleOnClick = (event: React.MouseEvent): void => { + event.stopPropagation(); + mutation.mutate(model); + }; + + const { isLoading, data: permissions = [] } = useQuery(`perm-${mapId}`, () => { + return client.fetchMapPermissions(mapId); + }); + return (
+ description={intl.formatMessage({ id: "share.delete-description", defaultMessage: "Invite people to collaborate with you on the creation of your midnmap. They will be notified by email. " })} + PaperProps={{ classes: { root: classes.paper } }} + error={error} + > -
- - - Can Edit - Can View + + { setShowMessage(value) }} + style={{ fontSize: "5px" }} control={} - label={} + label={} labelPlacement="end" + /> + + + + {showMessage && + - + }
- - - {[.4, 5, 7, 7, 8, 9, 100, 1, 2, 3].map((value) => { - const labelId = `checkbox-list-label-${value}`; + {!isLoading && + + + {permissions && permissions.map((permission) => { + return ( + + `} primary={permission.email} /> - return ( - - - - - - - - - ); - })} - - + + + + + + + + + ); + })} + + + }
); } - export default ShareDialog; \ No newline at end of file diff --git a/packages/webapp/src/components/maps-page/action-dispatcher/share-dialog/style.ts b/packages/webapp/src/components/maps-page/action-dispatcher/share-dialog/style.ts new file mode 100644 index 00000000..7cdfa588 --- /dev/null +++ b/packages/webapp/src/components/maps-page/action-dispatcher/share-dialog/style.ts @@ -0,0 +1,27 @@ +import createStyles from "@material-ui/core/styles/createStyles"; +import makeStyles from "@material-ui/core/styles/makeStyles"; + +export const useStyles = makeStyles(() => + createStyles({ + actionContainer: { + padding: '10px 0px', + border: '1px solid rgba(0, 0, 0, 0.12)', + borderRadius: '8px 8px 0px 0px', + textAlign: "center" + }, + textArea: + { + width: '730px', + margin: '5px 0px', + padding: '10px' + }, + listPaper: { + maxHeight: 200, + overflowY: 'scroll', + }, + paper: { + width: "850px", + minWidth: "850px" + } + }), +); \ No newline at end of file diff --git a/packages/webapp/src/components/maps-page/help-menu/index.tsx b/packages/webapp/src/components/maps-page/help-menu/index.tsx index f377e8c2..f800330e 100644 --- a/packages/webapp/src/components/maps-page/help-menu/index.tsx +++ b/packages/webapp/src/components/maps-page/help-menu/index.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { FormattedMessage } from "react-intl"; +import { FormattedMessage, useIntl } from "react-intl"; import Help from "@material-ui/icons/Help"; import PolicyOutlined from "@material-ui/icons/PolicyOutlined"; @@ -11,10 +11,12 @@ import Menu from "@material-ui/core/Menu"; import MenuItem from "@material-ui/core/MenuItem"; import Link from "@material-ui/core/Link"; import ListItemIcon from "@material-ui/core/ListItemIcon"; +import Tooltip from "@material-ui/core/Tooltip"; const HelpMenu = (): React.ReactElement => { const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); + const intl = useIntl(); const handleMenu = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); @@ -26,11 +28,14 @@ const HelpMenu = (): React.ReactElement => { return ( - - - + + + + + + { const mutation = useMutation( (id: number) => client.deleteLabel(id), { - onSuccess: () => queryClient.invalidateQueries('labels') + onSuccess: () => queryClient.invalidateQueries('labels'), + onError: (error) => { + console.error(`Unexpected error ${error}`); + } } ); @@ -87,8 +90,8 @@ const MapsPage = (): ReactElement => { mutation.mutate(id); }; - const { data } = useQuery('labels', async () => { - return await client.fetchLabels(); + const { data } = useQuery('labels', () => { + return client.fetchLabels(); }); const labels: Label[] = data ? data : []; @@ -140,7 +143,7 @@ const MapsPage = (): ReactElement => { elevation={0}> - + - +