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": { "history.no-changes": {
"defaultMessage": "There is no changes available" "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": { "login.desc": {
"defaultMessage": "Log into your account" "defaultMessage": "Log into your account"
}, },
@ -201,6 +210,30 @@
"menu.signout": { "menu.signout": {
"defaultMessage": "Sign Out" "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": { "registration.desc": {
"defaultMessage": "Signing up is free and just take a moment" "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>; duplicateMap(id: number, basicInfo: BasicMapInfo): Promise<number>;
fetchMapInfo(id: number): Promise<BasicMapInfo>; fetchMapInfo(id: number): Promise<BasicMapInfo>;
changeStarred(id: number, starred: boolean): Promise<void>; changeStarred(id: number, starred: boolean): Promise<void>;
updateMapToPublic(id: number, starred: boolean): Promise<void>;
fetchLabels(): Promise<Label[]>; fetchLabels(): Promise<Label[]>;
// createLabel(label: Label): Promise<void>; // createLabel(label: Label): Promise<void>;

View File

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

View File

@ -11,6 +11,35 @@ export default class RestClient implements Client {
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
this.sessionExpired = sessionExpired; 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> { revertHistory(id: number, cid: number): Promise<void> {
// '/c/restful/maps/${mindmapId}/history' // '/c/restful/maps/${mindmapId}/history'

View File

@ -269,6 +269,24 @@
"value": "There is no changes available" "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": [ "login.desc": [
{ {
"type": 0, "type": 0,
@ -401,6 +419,54 @@
"value": "Sign Out" "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": [ "registration.desc": [
{ {
"type": 0, "type": 0,

View File

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

View File

@ -58,7 +58,7 @@ const BaseDialog = (props: DialogProps) => {
(<FormattedMessage id="action.close-button" defaultMessage="Close" />) (<FormattedMessage id="action.close-button" defaultMessage="Close" />)
} }
</Button> </Button>
{onSubmit ? ( {onSubmit &&
<Button <Button
color="primary" color="primary"
size="medium" size="medium"
@ -66,7 +66,7 @@ const BaseDialog = (props: DialogProps) => {
type="submit" type="submit"
disableElevation={true}> disableElevation={true}>
{props.submitButton} {props.submitButton}
</Button>) : null </Button>
} }
</StyledDialogActions> </StyledDialogActions>
</form> </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 { useHistory } from 'react-router-dom';
import CreateDialog from './create-dialog'; import CreateDialog from './create-dialog';
import HistoryDialog from './history-dialog'; import HistoryDialog from './history-dialog';
import ImportDialog from './import-dialog';
import PublishDialog from './publish-dialog';
export type BasicMapInfo = { export type BasicMapInfo = {
name: string; name: string;
@ -44,11 +46,14 @@ const ActionDispatcher = (props: ActionDialogProps) => {
return ( return (
<span> <span>
{action === 'create' ? <CreateDialog open={true} onClose={handleOnClose} /> : null} {action === 'create' && <CreateDialog open={true} onClose={handleOnClose} /> }
{action === 'delete' ? <DeleteDialog open={true} onClose={handleOnClose} mapId={mapId} /> : null} {action === 'delete' &&<DeleteDialog open={true} onClose={handleOnClose} mapId={mapId} />}
{action === 'rename' ? <RenameDialog open={true} onClose={handleOnClose} mapId={mapId} /> : null} {action === 'rename' && <RenameDialog open={true} onClose={handleOnClose} mapId={mapId} />}
{action === 'duplicate' ? <DuplicateDialog open={true} onClose={handleOnClose} mapId={mapId} /> : null} {action === 'duplicate' && <DuplicateDialog open={true} onClose={handleOnClose} mapId={mapId} />}
{action === 'history' ? <HistoryDialog open={true} onClose={handleOnClose} mapId={mapId} /> : null} {action === 'history' && <HistoryDialog open={true} onClose={handleOnClose} mapId={mapId} />}
{action === 'import' && <ImportDialog open={true} onClose={handleOnClose} />}
{action === 'publish' && <PublishDialog onClose={handleOnClose} mapId={mapId}/>}
</span > </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

@ -129,8 +129,15 @@ const MapsPage = () => {
</Tooltip> </Tooltip>
<Tooltip title="Import from external tools"> <Tooltip title="Import from external tools">
<Button color="primary" size="medium" variant="outlined" type="button" <Button
disableElevation={true} startIcon={<CloudUploadTwoTone />} className={classes.importButton}> 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" /> <FormattedMessage id="action.import" defaultMessage="Import" />
</Button> </Button>
</Tooltip> </Tooltip>
@ -228,12 +235,13 @@ const StyleListItem = (props: ListItemProps) => {
{icon} {icon}
</ListItemIcon> </ListItemIcon>
<ListItemText style={{ color: 'white' }} primary={label} /> <ListItemText style={{ color: 'white' }} primary={label} />
{filter.type == 'label' ? {filter.type == 'label' &&
(<ListItemSecondaryAction> <ListItemSecondaryAction>
<IconButton edge="end" aria-label="delete" onClick={e => handleOnDelete(e, filter)}> <IconButton edge="end" aria-label="delete" onClick={e => handleOnDelete(e, filter)}>
<DeleteOutlineTwoTone color="secondary" /> <DeleteOutlineTwoTone color="secondary" />
</IconButton> </IconButton>
</ListItemSecondaryAction>) : null} </ListItemSecondaryAction>
}
</ListItem> </ListItem>
); );
} }

View File

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