Complete public panel

This commit is contained in:
Paulo Gustavo Veiga 2021-02-06 00:45:33 -08:00
parent c923c18cf9
commit 5f41821f00
12 changed files with 426 additions and 52 deletions

View File

@ -134,6 +134,15 @@
"history.no-changes": {
"defaultMessage": "There is no changes available"
},
"import.button": {
"defaultMessage": "Create"
},
"import.description": {
"defaultMessage": "You can import FreeMind 1.0.1 and WiseMapping maps to your list of maps. Select the file you want to import."
},
"import.title": {
"defaultMessage": "Import existing mindmap"
},
"login.desc": {
"defaultMessage": "Log into your account"
},
@ -201,6 +210,30 @@
"menu.signout": {
"defaultMessage": "Sign Out"
},
"publish.button": {
"defaultMessage": "Accept"
},
"publish.checkbox": {
"defaultMessage": "Enable public sharing"
},
"publish.description": {
"defaultMessage": "By publishing the map you make it visible to everyone on the Internet."
},
"publish.embedded": {
"defaultMessage": "Embedded"
},
"publish.embedded-msg": {
"defaultMessage": "Copy this snippet of code to embed in your blog or page:"
},
"publish.public-url": {
"defaultMessage": "Public URL"
},
"publish.public-url-msg": {
"defaultMessage": "Copy and paste the link below to share your map with colleagues:"
},
"publish.title": {
"defaultMessage": "Publish"
},
"registration.desc": {
"defaultMessage": "Signing up is free and just take a moment"
},

View File

@ -55,6 +55,7 @@ interface Client {
duplicateMap(id: number, basicInfo: BasicMapInfo): Promise<number>;
fetchMapInfo(id: number): Promise<BasicMapInfo>;
changeStarred(id: number, starred: boolean): Promise<void>;
updateMapToPublic(id: number, starred: boolean): Promise<void>;
fetchLabels(): Promise<Label[]>;
// createLabel(label: Label): Promise<void>;

View File

@ -54,15 +54,21 @@ class MockClient implements Client {
return Promise.resolve(this.labels);
}
updateMapToPublic(id: number, isPublic: boolean): Promise<void> {
const mapInfo = this.maps.find(m => m.id == id);
if (mapInfo) {
mapInfo.isPublic = isPublic;
}
return Promise.resolve();
}
changeStarred(id: number, starred: boolean): Promise<void> {
const mapInfo = this.maps.find(m => m.id == id);
if (!mapInfo) {
console.log(`Could not find the map iwth id ${id}`);
return Promise.reject();
}
const newStarredValue = !mapInfo?.starred;
mapInfo.starred = newStarredValue;
mapInfo.starred = starred;
return Promise.resolve();
}

View File

@ -11,6 +11,35 @@ export default class RestClient implements Client {
this.baseUrl = baseUrl;
this.sessionExpired = sessionExpired;
}
updateMapToPublic(id: number, isPublic: boolean): Promise<void> {
/*
jQuery.ajax("c/restful/maps/${mindmap.id}/publish", {
async:false,
dataType:'json',
data:$('#dialogMainForm #enablePublicView')[0].checked ? 'true' : 'false',
type:'PUT',
contentType:"text/plain",
success:function (data, textStatus, jqXHR) {
$('#publish-dialog-modal').modal('hide');
},
*/
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios.put(`${this.baseUrl}/c/restful/maps/${id}/publish`,
isPublic,
{ headers: { 'Content-Type': 'text/plain' } }
).then(response => {
// All was ok, let's sent to success page ...;
success();
}).catch(error => {
const response = error.response;
const errorInfo = this.parseResponseOnError(response);
reject(errorInfo);
});
}
return new Promise(handler);
}
revertHistory(id: number, cid: number): Promise<void> {
// '/c/restful/maps/${mindmapId}/history'
@ -37,7 +66,7 @@ export default class RestClient implements Client {
case 401:
case 302:
this.sessionExpired();
result = { msg: intl.formatMessage({ id: "expired.description", defaultMessage: "Your current session has expired. Please, sign in and try again." })}
result = { msg: intl.formatMessage({ id: "expired.description", defaultMessage: "Your current session has expired. Please, sign in and try again." }) }
break;
default:
if (data) {

View File

@ -269,6 +269,24 @@
"value": "There is no changes available"
}
],
"import.button": [
{
"type": 0,
"value": "Create"
}
],
"import.description": [
{
"type": 0,
"value": "You can import FreeMind 1.0.1 and WiseMapping maps to your list of maps. Select the file you want to import."
}
],
"import.title": [
{
"type": 0,
"value": "Import existing mindmap"
}
],
"login.desc": [
{
"type": 0,
@ -401,6 +419,54 @@
"value": "Sign Out"
}
],
"publish.button": [
{
"type": 0,
"value": "Accept"
}
],
"publish.checkbox": [
{
"type": 0,
"value": "Enable public sharing"
}
],
"publish.description": [
{
"type": 0,
"value": "By publishing the map you make it visible to everyone on the Internet."
}
],
"publish.embedded": [
{
"type": 0,
"value": "Embedded"
}
],
"publish.embedded-msg": [
{
"type": 0,
"value": "Copy this snippet of code to embed in your blog or page:"
}
],
"publish.public-url": [
{
"type": 0,
"value": "Public URL"
}
],
"publish.public-url-msg": [
{
"type": 0,
"value": "Copy and paste the link below to share your map with colleagues:"
}
],
"publish.title": [
{
"type": 0,
"value": "Publish"
}
],
"registration.desc": [
{
"type": 0,

View File

@ -12,15 +12,16 @@ import ShareOutlinedIcon from '@material-ui/icons/ShareOutlined';
import { FormattedMessage } from 'react-intl';
import { LabelOutlined } from '@material-ui/icons';
export type ActionType = 'open' | 'share' | 'delete' | 'info' | 'create'| 'duplicate' | 'export' | 'label' | 'rename' | 'print' | 'info' | 'publish' | 'history' | undefined;
export type ActionType = 'open' | 'share' | 'import' | 'delete' | 'info' | 'create' | 'duplicate' | 'export' | 'label' | 'rename' | 'print' | 'info' | 'publish' | 'history' | undefined;
interface ActionProps {
onClose: (action: ActionType) => void;
anchor: undefined | HTMLElement;
role: 'owner' | 'editor' | 'viewer'
}
const ActionChooser = (props: ActionProps) => {
const { anchor, onClose } = props;
const { anchor, onClose, role } = props;
const handleOnClose = (action: ActionType): ((event: React.MouseEvent<HTMLLIElement>) => void) => {
return (event): void => {
@ -38,12 +39,13 @@ const ActionChooser = (props: ActionProps) => {
onClose={handleOnClose(undefined)}
elevation={1}
>
<MenuItem onClick={handleOnClose('open')} style={{width:"220px"}}>
<MenuItem onClick={handleOnClose('open')} style={{ width: "220px" }}>
<ListItemIcon>
<DescriptionOutlinedIcon />
</ListItemIcon>
<FormattedMessage id="action.open" defaultMessage="Open" />
</MenuItem>
<Divider />
<MenuItem onClick={handleOnClose('duplicate')}>
@ -53,12 +55,14 @@ const ActionChooser = (props: ActionProps) => {
<FormattedMessage id="action.duplicate" defaultMessage="Duplicate" />
</MenuItem>
<MenuItem onClick={handleOnClose('rename')}>
<ListItemIcon>
<EditOutlinedIcon />
</ListItemIcon>
<FormattedMessage id="action.rename" defaultMessage="Rename" />
</MenuItem>
{role == 'owner' &&
<MenuItem onClick={handleOnClose('rename')}>
<ListItemIcon>
<EditOutlinedIcon />
</ListItemIcon>
<FormattedMessage id="action.rename" defaultMessage="Rename" />
</MenuItem>
}
<MenuItem onClick={handleOnClose('label')}>
<ListItemIcon>
@ -89,19 +93,23 @@ const ActionChooser = (props: ActionProps) => {
<FormattedMessage id="action.print" defaultMessage="Print" />
</MenuItem>
<MenuItem onClick={handleOnClose('publish')}>
<ListItemIcon>
<PublicOutlinedIcon />
</ListItemIcon>
<FormattedMessage id="action.publish" defaultMessage="Publish" />
</MenuItem>
{role != 'viewer' &&
<MenuItem onClick={handleOnClose('publish')}>
<ListItemIcon>
<PublicOutlinedIcon />
</ListItemIcon>
<FormattedMessage id="action.publish" defaultMessage="Publish" />
</MenuItem>
}
<MenuItem onClick={handleOnClose('share')}>
<ListItemIcon>
<ShareOutlinedIcon />
</ListItemIcon>
<FormattedMessage id="action.share" defaultMessage="Share" />
</MenuItem>
{role != 'viewer' &&
<MenuItem onClick={handleOnClose('share')}>
<ListItemIcon>
<ShareOutlinedIcon />
</ListItemIcon>
<FormattedMessage id="action.share" defaultMessage="Share" />
</MenuItem>
}
<Divider />
<MenuItem onClick={handleOnClose('info')}>
@ -111,12 +119,14 @@ const ActionChooser = (props: ActionProps) => {
<FormattedMessage id="action.info" defaultMessage="Info" />
</MenuItem>
<MenuItem onClick={handleOnClose('history')}>
<ListItemIcon>
<DeleteOutlinedIcon />
</ListItemIcon>
<FormattedMessage id="action.history" defaultMessage="History" />
</MenuItem>
{role != 'viewer' &&
<MenuItem onClick={handleOnClose('history')}>
<ListItemIcon>
<DeleteOutlinedIcon />
</ListItemIcon>
<FormattedMessage id="action.history" defaultMessage="History" />
</MenuItem>
}
</Menu>);
}

View File

@ -58,7 +58,7 @@ const BaseDialog = (props: DialogProps) => {
(<FormattedMessage id="action.close-button" defaultMessage="Close" />)
}
</Button>
{onSubmit ? (
{onSubmit &&
<Button
color="primary"
size="medium"
@ -66,7 +66,7 @@ const BaseDialog = (props: DialogProps) => {
type="submit"
disableElevation={true}>
{props.submitButton}
</Button>) : null
</Button>
}
</StyledDialogActions>
</form>

View File

@ -0,0 +1,93 @@
import React from 'react';
import { useIntl } from 'react-intl';
import { useMutation } from 'react-query';
import { useSelector } from 'react-redux';
import { Button, FormControl } from '@material-ui/core';
import Client, { BasicMapInfo, ErrorInfo } from '../../../../client';
import { activeInstance } from '../../../../redux/clientSlice';
import Input from '../../../form/input';
import BaseDialog from '../base-dialog';
export type ImportModel = {
title: string;
description?: string;
}
export type CreateProps = {
open: boolean,
onClose: () => void
}
const defaultModel: ImportModel = { title: '', description: '' };
const ImportDialog = (props: CreateProps) => {
const client: Client = useSelector(activeInstance);
const [model, setModel] = React.useState<ImportModel>(defaultModel);
const [error, setError] = React.useState<ErrorInfo>();
const intl = useIntl();
const mutation = useMutation<number, ErrorInfo, ImportModel>((model: ImportModel) => {
return client.createMap(model);
},
{
onSuccess: (mapId: number) => {
window.location.href = `/c/maps/${mapId}/edit`;
},
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 });
}
return (
<div>
<BaseDialog onClose={handleOnClose} onSubmit={handleOnSubmit} error={error}
title={intl.formatMessage({ id: 'import.title', defaultMessage: 'Import existing mindmap' })}
description={intl.formatMessage({ id: 'import.description', defaultMessage: 'You can import FreeMind 1.0.1 and WiseMapping maps to your list of maps. Select the file you want to import.' })}
submitButton={intl.formatMessage({ id: 'import.button', defaultMessage: 'Create' })}>
<FormControl fullWidth={true}>
<Input name="title" type="text" label={intl.formatMessage({ id: "action.rename-name-placeholder", defaultMessage: "Name" })}
value={model.title} onChange={handleOnChange} error={error} fullWidth={true} />
<Input name="description" type="text" label={intl.formatMessage({ id: "action.rename-description-placeholder", defaultMessage: "Description" })}
value={model.description} onChange={handleOnChange} required={false} fullWidth={true} />
<input
accept="image/*"
id="contained-button-file"
type="file"
style={{display: 'none'}}
/>
<label htmlFor="contained-button-file">
<Button variant="contained" color="primary" component="span">
Upload
</Button>
</label>
</FormControl>
</BaseDialog>
</div>
);
}
export default ImportDialog;

View File

@ -11,6 +11,8 @@ import DuplicateDialog from './duplicate-dialog';
import { useHistory } from 'react-router-dom';
import CreateDialog from './create-dialog';
import HistoryDialog from './history-dialog';
import ImportDialog from './import-dialog';
import PublishDialog from './publish-dialog';
export type BasicMapInfo = {
name: string;
@ -44,11 +46,14 @@ const ActionDispatcher = (props: ActionDialogProps) => {
return (
<span>
{action === 'create' ? <CreateDialog open={true} onClose={handleOnClose} /> : null}
{action === 'delete' ? <DeleteDialog open={true} onClose={handleOnClose} mapId={mapId} /> : null}
{action === 'rename' ? <RenameDialog open={true} onClose={handleOnClose} mapId={mapId} /> : null}
{action === 'duplicate' ? <DuplicateDialog open={true} onClose={handleOnClose} mapId={mapId} /> : null}
{action === 'history' ? <HistoryDialog open={true} onClose={handleOnClose} mapId={mapId} /> : null}
{action === 'create' && <CreateDialog open={true} onClose={handleOnClose} /> }
{action === 'delete' &&<DeleteDialog open={true} onClose={handleOnClose} mapId={mapId} />}
{action === 'rename' && <RenameDialog open={true} onClose={handleOnClose} mapId={mapId} />}
{action === 'duplicate' && <DuplicateDialog open={true} onClose={handleOnClose} mapId={mapId} />}
{action === 'history' && <HistoryDialog open={true} onClose={handleOnClose} mapId={mapId} />}
{action === 'import' && <ImportDialog open={true} onClose={handleOnClose} />}
{action === 'publish' && <PublishDialog onClose={handleOnClose} mapId={mapId}/>}
</span >
);
}

View File

@ -0,0 +1,121 @@
import React from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { useMutation, useQueryClient } from 'react-query';
import { useSelector } from 'react-redux';
import { AppBar, Checkbox, FormControl, FormControlLabel, Tab, Typography } from '@material-ui/core';
import Client, { ErrorInfo } from '../../../../client';
import { activeInstance } from '../../../../redux/clientSlice';
import BaseDialog from '../base-dialog';
import { TabContext, TabList, TabPanel } from '@material-ui/lab';
import { fetchMapById, handleOnMutationSuccess } from '..';
export type PublishProps = {
mapId: number,
onClose: () => void
}
const PublishDialog = (props: PublishProps) => {
const { mapId, onClose } = props;
const { map } = fetchMapById(mapId);
const client: Client = useSelector(activeInstance);
const [model, setModel] = React.useState<boolean>(map ? map.isPublic : false);
const [error, setError] = React.useState<ErrorInfo>();
const [value, setValue] = React.useState('1');
const queryClient = useQueryClient();
const intl = useIntl();
const mutation = useMutation<void, ErrorInfo, boolean>((model: boolean) => {
return client.updateMapToPublic(mapId, model);
},
{
onSuccess: () => {
setModel(model);
handleOnMutationSuccess(onClose, queryClient);
},
onError: (error) => {
setError(error);
}
}
);
const handleOnClose = (): void => {
props.onClose();
setError(undefined);
};
const handleOnSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
event.preventDefault();
mutation.mutate(model);
};
const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>, checked: boolean): void => {
event.preventDefault();
setModel(checked);
}
const handleTabChange = (event, newValue) => {
setValue(newValue);
};
return (
<div>
<BaseDialog onClose={handleOnClose} onSubmit={handleOnSubmit} error={error}
title={intl.formatMessage({ id: 'publish.title', defaultMessage: 'Publish' })}
description={intl.formatMessage({ id: 'publish.description', defaultMessage: 'By publishing the map you make it visible to everyone on the Internet.' })}
submitButton={intl.formatMessage({ id: 'publish.button', defaultMessage: 'Accept' })}>
<FormControl fullWidth={true}>
<FormControlLabel
control={
<Checkbox
checked={model}
onChange={handleOnChange}
name="public"
color="primary"
/>
}
label={intl.formatMessage({ id: 'publish.checkbox', defaultMessage: 'Enable public sharing' })}
/>
</FormControl>
<div style={!model ? { visibility: 'hidden' } : {}}>
<TabContext value={value}>
<AppBar position="static">
<TabList onChange={handleTabChange}>
<Tab label={intl.formatMessage({ id: 'publish.embedded', defaultMessage: 'Embedded' })} value="1" />
<Tab label={intl.formatMessage({ id: 'publish.public-url', defaultMessage: 'Public URL' })} value="2" />
</TabList>
</AppBar>
<TabPanel value="1">
<Typography variant="subtitle2">
<FormattedMessage id="publish.embedded-msg" defaultMessage="Copy this snippet of code to embed in your blog or page:" />
</Typography>
<p>
<textarea style={{ width: "100%" }}>
{`<iframe style="width:600px;height:400px;border:1px solid black" src="https://app.wisemapping.com/c/maps/${mapId}/embed?zoom=1.0"></iframe>`}
</textarea>
</p>
</TabPanel>
<TabPanel value="2">
<Typography variant="subtitle2">
<FormattedMessage id="publish.public-url-msg" defaultMessage="Copy and paste the link below to share your map with colleagues:" />
</Typography>
<p>
<textarea style={{ width: "100%" }}>
{`https://app.wisemapping.com/c/maps/${mapId}/public`}
</textarea>
</p>
</TabPanel>
</TabContext>
</div>
</BaseDialog>
</div>
);
}
export default PublishDialog;

View File

@ -105,7 +105,7 @@ const MapsPage = () => {
return (
<div className={classes.root}>
<HandleClientStatus/>
<HandleClientStatus />
<AppBar
position="fixed"
className={clsx(classes.appBar, {
@ -129,8 +129,15 @@ const MapsPage = () => {
</Tooltip>
<Tooltip title="Import from external tools">
<Button color="primary" size="medium" variant="outlined" type="button"
disableElevation={true} startIcon={<CloudUploadTwoTone />} className={classes.importButton}>
<Button
color="primary"
size="medium"
variant="outlined"
type="button"
disableElevation={true}
startIcon={<CloudUploadTwoTone />}
className={classes.importButton}
onClick={e => setActiveDialog('import')}>
<FormattedMessage id="action.import" defaultMessage="Import" />
</Button>
</Tooltip>
@ -228,12 +235,13 @@ const StyleListItem = (props: ListItemProps) => {
{icon}
</ListItemIcon>
<ListItemText style={{ color: 'white' }} primary={label} />
{filter.type == 'label' ?
(<ListItemSecondaryAction>
{filter.type == 'label' &&
<ListItemSecondaryAction>
<IconButton edge="end" aria-label="delete" onClick={e => handleOnDelete(e, filter)}>
<DeleteOutlineTwoTone color="secondary" />
</IconButton>
</ListItemSecondaryAction>) : null}
</ListItemSecondaryAction>
}
</ListItem>
);
}
@ -254,7 +262,7 @@ const HandleClientStatus = () => {
fullWidth={true}>
<DialogTitle>
<FormattedMessage id="expired.title" defaultMessage="Your session has expired" />
<FormattedMessage id="expired.title" defaultMessage="Your session has expired" />
</DialogTitle>
<DialogContent>

View File

@ -125,11 +125,13 @@ function EnhancedTableHead(props: EnhancedTableProps) {
direction={orderBy === headCell.id ? order : 'asc'}
onClick={createSortHandler(headCell.id)}>
{headCell.label}
{orderBy === headCell.id ? (
{orderBy === headCell.id && (
<span className={classes.visuallyHidden}>
{order === 'desc' ? 'sorted descending' : 'sorted ascending'}
</span>
) : null}
)}
</TableSortLabel>
</TableCell>)
})}
@ -321,7 +323,7 @@ export const MapsList = (props: MapsListProps) => {
<Toolbar className={classes.toolbar} variant="dense">
<div className={classes.toolbarActions}>
{selected.length > 0 ? (
{selected.length > 0 &&
<Tooltip title="Delete selected">
<Button
color="primary"
@ -333,9 +335,9 @@ export const MapsList = (props: MapsListProps) => {
<FormattedMessage id="action.delete" defaultMessage="Delete" />
</Button>
</Tooltip>
) : null}
}
{selected.length > 0 ? (
{selected.length > 0 &&
<Tooltip title="Add label to selected">
<Button
color="primary"
@ -348,7 +350,7 @@ export const MapsList = (props: MapsListProps) => {
<FormattedMessage id="action.label" defaultMessage="Add Label" />
</Button>
</Tooltip>
) : null}
}
</div>
<div className={classes.toolbarListActions}>
@ -465,7 +467,7 @@ export const MapsList = (props: MapsListProps) => {
<MoreHorizIcon color="action" />
</IconButton>
</Tooltip>
<ActionChooser anchor={activeRowAction?.el} onClose={handleActionMenuClose} />
<ActionChooser anchor={activeRowAction?.el} onClose={handleActionMenuClose} role={row.role} />
</TableCell>
</TableRow>
);