Integrate export and improve editor integration

This commit is contained in:
Matias Arriola 2022-01-19 11:21:35 -03:00
parent f84ccfdcbd
commit 2faeeedd1b
12 changed files with 110 additions and 34 deletions

View File

@ -58,6 +58,8 @@ import { DesignerOptions } from './DesignerOptionsBuilder';
import MainTopic from './MainTopic'; import MainTopic from './MainTopic';
import DragTopic from './DragTopic'; import DragTopic from './DragTopic';
export type ExportFormat = 'png' | 'svg' | 'jpg' | 'wxml';
class Designer extends Events { class Designer extends Events {
private _mindmap: Mindmap; private _mindmap: Mindmap;
@ -80,11 +82,11 @@ class Designer extends Events {
private _cleanScreen: any; private _cleanScreen: any;
constructor(options: DesignerOptions, divElement: JQuery) { constructor(options: DesignerOptions, divElement: JQuery) {
super();
$assert(options, 'options must be defined'); $assert(options, 'options must be defined');
$assert(options.zoom, 'zoom must be defined'); $assert(options.zoom, 'zoom must be defined');
$assert(options.containerSize, 'size must be defined'); $assert(options.containerSize, 'size must be defined');
$assert(divElement, 'divElement must be defined'); $assert(divElement, 'divElement must be defined');
super();
// Set up i18n location ... // Set up i18n location ...
Messages.init(options.locale); Messages.init(options.locale);
@ -342,7 +344,9 @@ class Designer extends Events {
} }
} }
export(formatType: 'png' | 'svg' | 'jpg' | 'wxml'): Promise<string> { EXPORT_SUPPORTED_FORMATS: ExportFormat[] = ['png', 'svg', 'jpg', 'wxml'];
export(formatType: ExportFormat): Promise<string> {
const workspace = this._workspace; const workspace = this._workspace;
const svgElement = workspace.getSVGElement(); const svgElement = workspace.getSVGElement();
const size = workspace.getSize(); const size = workspace.getSize();

View File

@ -57,8 +57,8 @@ class AccountSettingsPanel extends ListToolbarPanel {
const content = $("<div class='toolbarPanel' id='accountSettingsPanel'></div>"); const content = $("<div class='toolbarPanel' id='accountSettingsPanel'></div>");
content[0].innerHTML = ` content[0].innerHTML = `
<p style='text-align:center;font-weight:bold;'>${global.accountName}</p> <p style='text-align:center;font-weight:bold;'>${global.accountName}</p>
<p>pveiga@wisemapping.com</p> <p>${global.accountEmail}</p>
<div id="${global.accountMail}" model='logout' style='text-align:center'> <div id="account-logout" model='logout' style='text-align:center'>
Logout Logout
</div> </div>
`; `;

View File

@ -467,5 +467,11 @@
}, },
"share.message": { "share.message": {
"defaultMessage": "Message" "defaultMessage": "Message"
},
"editor.try-welcome": {
"defaultMessage": "This edition space showcases some of the mindmap editor capabilities !"
},
"editor.try-welcome-description": {
"defaultMessage": "Sign Up to start creating, sharing and publishing unlimited number of mindmaps for free."
} }
} }

View File

@ -467,5 +467,11 @@
}, },
"share.message": { "share.message": {
"defaultMessage": "Message" "defaultMessage": "Message"
},
"editor.try-welcome": {
"defaultMessage": "This edition space showcases some of the mindmap editor capabilities !"
},
"editor.try-welcome-description": {
"defaultMessage": "Sign Up to start creating, sharing and publishing unlimited number of mindmaps for free."
} }
} }

View File

@ -467,5 +467,11 @@
}, },
"share.message": { "share.message": {
"defaultMessage": "Message" "defaultMessage": "Message"
},
"editor.try-welcome": {
"defaultMessage": "This edition space showcases some of the mindmap editor capabilities !"
},
"editor.try-welcome-description": {
"defaultMessage": "Sign Up to start creating, sharing and publishing unlimited number of mindmaps for free."
} }
} }

View File

@ -467,5 +467,11 @@
}, },
"share.message": { "share.message": {
"defaultMessage": "Message" "defaultMessage": "Message"
},
"editor.try-welcome": {
"defaultMessage": "This edition space showcases some of the mindmap editor capabilities !"
},
"editor.try-welcome-description": {
"defaultMessage": "Sign Up to start creating, sharing and publishing unlimited number of mindmaps for free."
} }
} }

View File

@ -32,6 +32,12 @@ const queryClient = new QueryClient({
const App = (): ReactElement => { const App = (): ReactElement => {
const appi18n = new AppI18n(); const appi18n = new AppI18n();
const locale = appi18n.getBrowserLocale(); const locale = appi18n.getBrowserLocale();
// global variables set server-side
const memoryPersistence = global.memoryPersistence;
const readOnlyMode = global.readOnly;
const mapId = parseInt(global.mapId, 10);
return locale.message ? ( return locale.message ? (
<Provider store={store}> <Provider store={store}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
@ -66,7 +72,8 @@ const App = (): ReactElement => {
<MapsPage /> <MapsPage />
</Route> </Route>
<Route exact path="/c/maps/:id/edit"> <Route exact path="/c/maps/:id/edit">
<Editor /> <Editor memoryPersistence={memoryPersistence}
readOnlyMode={readOnlyMode} mapId={mapId} />
</Route> </Route>
</Switch> </Switch>
</Router> </Router>

View File

@ -74,13 +74,30 @@ const ExportDialog = ({
useEffect(() => { useEffect(() => {
if (submit) { if (submit) {
// TODO: Remove usage of global "designer"
const designer = global.designer;
// Depending on the type of export. It will require differt POST. // Depending on the type of export. It will require differt POST.
if ( if (
exportFormat == 'pdf' || designer &&
exportFormat == 'svg' || designer.EXPORT_SUPPORTED_FORMATS.includes(exportFormat)
exportFormat == 'jpg' ||
exportFormat == 'png'
) { ) {
designer.export(exportFormat)
.then((url: string) => {
// Create hidden anchor to force download ...
const anchor: HTMLAnchorElement = document.createElement('a');
anchor.style.display = 'display: none';
anchor.download = `${mapId}.${exportFormat}`;
anchor.href = url;
document.body.appendChild(anchor);
// Trigger click ...
anchor.click();
// Clean up ...
URL.revokeObjectURL(url);
document.body.removeChild(anchor);
});
} else if (exportFormat === 'pdf') {
formTransformtRef?.submit(); formTransformtRef?.submit();
} else { } else {
formExportRef?.submit(); formExportRef?.submit();
@ -99,13 +116,15 @@ const ExportDialog = ({
description={intl.formatMessage({ id: 'export.desc', defaultMessage: 'Export this map in the format that you want and start using it in your presentations or sharing by email' })} description={intl.formatMessage({ id: 'export.desc', defaultMessage: 'Export this map in the format that you want and start using it in your presentations or sharing by email' })}
submitButton={intl.formatMessage({ id: 'export.title', defaultMessage: 'Export' })} submitButton={intl.formatMessage({ id: 'export.title', defaultMessage: 'Export' })}
> >
<Alert severity="info"> {
<FormattedMessage !enableImgExport &&
id="export.warning" <Alert severity="info">
defaultMessage="Exporting to Image (SVG,PNG,JPEG,PDF) is only available in the editor toolbar." <FormattedMessage
/> id="export.warning"
</Alert> defaultMessage="Exporting to Image (SVG,PNG,JPEG,PDF) is only available in the editor toolbar."
/>
</Alert>
}
<FormControl component="fieldset"> <FormControl component="fieldset">
<RadioGroup name="export" value={exportGroup} onChange={handleOnGroupChange}> <RadioGroup name="export" value={exportGroup} onChange={handleOnGroupChange}>
<FormControl> <FormControl>
@ -124,7 +143,7 @@ const ExportDialog = ({
/> />
{exportGroup == 'image' && ( {exportGroup == 'image' && (
<Select <Select
onSelect={handleOnExportFormatChange} onChange={handleOnExportFormatChange}
variant="outlined" variant="outlined"
value={exportFormat} value={exportFormat}
className={classes.label} className={classes.label}
@ -132,7 +151,7 @@ const ExportDialog = ({
<MenuItem value="svg" className={classes.menu}> <MenuItem value="svg" className={classes.menu}>
Scalable Vector Graphics (SVG) Scalable Vector Graphics (SVG)
</MenuItem> </MenuItem>
<MenuItem value="pdf" className={classes.select}> <MenuItem value="pdf" className={classes.menu}>
Portable Document Format (PDF) Portable Document Format (PDF)
</MenuItem> </MenuItem>
<MenuItem value="png" className={classes.menu}> <MenuItem value="png" className={classes.menu}>

View File

@ -22,9 +22,10 @@ type ActionDialogProps = {
action?: ActionType; action?: ActionType;
mapsId: number[]; mapsId: number[];
onClose: () => void; onClose: () => void;
fromEditor: boolean;
}; };
const ActionDispatcher = ({ mapsId, action, onClose }: ActionDialogProps): React.ReactElement => { const ActionDispatcher = ({ mapsId, action, onClose, fromEditor }: ActionDialogProps): React.ReactElement => {
const handleOnClose = (): void => { const handleOnClose = (): void => {
onClose(); onClose();
}; };
@ -57,12 +58,17 @@ const ActionDispatcher = ({ mapsId, action, onClose }: ActionDialogProps): React
{action === 'info' && <InfoDialog onClose={handleOnClose} mapId={mapsId[0]} />} {action === 'info' && <InfoDialog onClose={handleOnClose} mapId={mapsId[0]} />}
{action === 'create' && <CreateDialog onClose={handleOnClose} />} {action === 'create' && <CreateDialog onClose={handleOnClose} />}
{action === 'export' && ( {action === 'export' && (
<ExportDialog onClose={handleOnClose} mapId={mapsId[0]} enableImgExport={false} /> <ExportDialog onClose={handleOnClose} mapId={mapsId[0]} enableImgExport={fromEditor} />
)} )}
{action === 'share' && <ShareDialog onClose={handleOnClose} mapId={mapsId[0]} />} {action === 'share' && <ShareDialog onClose={handleOnClose} mapId={mapsId[0]} />}
</span> </span>
); );
}; };
ActionDispatcher.defaultProps = {
fromEditor: false,
};
export const handleOnMutationSuccess = (onClose: () => void, queryClient: QueryClient): void => { export const handleOnMutationSuccess = (onClose: () => void, queryClient: QueryClient): void => {
queryClient.invalidateQueries('maps'); queryClient.invalidateQueries('maps');
onClose(); onClose();

View File

@ -2,14 +2,19 @@ import React from 'react';
import Toolbar from './toolbar'; import Toolbar from './toolbar';
import ActionDispatcher from '../action-dispatcher'; import ActionDispatcher from '../action-dispatcher';
import { ActionType } from '../action-chooser'; import { ActionType } from '../action-chooser';
import { useIntl } from 'react-intl';
// this component is a hack. In order to work, we need to load externally the "loader.js" script from mindplot export type EditorPropsType = {
mapId: number;
memoryPersistence: boolean;
readOnlyMode: boolean;
};
// HACK. In order to work, we need to load externally the "loader.js" script from mindplot
// TODO: create the Editor component in the editor package, with its own build and include it instead // TODO: create the Editor component in the editor package, with its own build and include it instead
export default function Editor(): React.ReactElement { export default function Editor({ mapId, memoryPersistence, readOnlyMode } : EditorPropsType): React.ReactElement {
const memoryPersistence = false; const intl = useIntl();
const readOnlyMode = false;
const mapId = 1;
const [activeDialog, setActiveDialog] = React.useState<ActionType | null>(null); const [activeDialog, setActiveDialog] = React.useState<ActionType | null>(null);
return <> return <>
@ -42,20 +47,25 @@ export default function Editor(): React.ReactElement {
<div id="headerNotifier"></div> <div id="headerNotifier"></div>
{ {
memoryPersistence && <div id="tryInfoPanel"> memoryPersistence && <div id="tryInfoPanel">
<p>TRY_WELCOME</p> <p>
<p>TRY_WELCOME_DESC</p> { intl.formatMessage({ id: 'editor.try-welcome' }) }
<a href="/c/registration"><div className="actionButton"> </p>
SIGN_UP</div> <p>{ intl.formatMessage({ id: 'editor.try-welcome-description' }) }</p>
<a href="/c/registration">
<div className="actionButton">
{ intl.formatMessage({ id: 'login.signup', defaultMessage: 'Sign Up' }) }
</div>
</a> </a>
</div> </div>
} }
{ {
activeDialog && activeDialog &&
<ActionDispatcher <ActionDispatcher
action={activeDialog} action={activeDialog}
onClose={() => setActiveDialog(null)} onClose={() => setActiveDialog(null)}
mapsId={[mapId]} mapsId={[mapId]}
/> fromEditor
/>
} }
</> </>
} }

View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { useIntl } from 'react-intl';
import { ActionType } from '../action-chooser'; import { ActionType } from '../action-chooser';
export type ToolbarPropsType = { export type ToolbarPropsType = {
@ -12,6 +13,7 @@ export default function Toolbar({
readOnlyMode, readOnlyMode,
onAction, onAction,
}: ToolbarPropsType): React.ReactElement { }: ToolbarPropsType): React.ReactElement {
const intl = useIntl();
return ( return (
<div id="toolbar"> <div id="toolbar">
<div id="backToList"> <div id="backToList">
@ -22,6 +24,9 @@ export default function Toolbar({
<div id="save" className="buttonOn"> <div id="save" className="buttonOn">
<img src="../../images/editor/save.svg" /> <img src="../../images/editor/save.svg" />
</div> </div>
<div id="discard" className="buttonOn">
<img src="../../images/editor/discard.svg" />
</div>
</div> </div>
)} )}
{!readOnlyMode && ( {!readOnlyMode && (
@ -103,7 +108,7 @@ export default function Toolbar({
<img src="../../images/editor/account.svg" /> <img src="../../images/editor/account.svg" />
</div> </div>
<div id="share" className="actionButton" onClick={() => onAction('share')}> <div id="share" className="actionButton" onClick={() => onAction('share')}>
SHARE { intl.formatMessage({ id: 'action.share', defaultMessage: 'Share' }) }
</div> </div>
</div> </div>
)} )}

View File

@ -197,6 +197,7 @@ const MapsPage = (): ReactElement => {
action={activeDialog} action={activeDialog}
onClose={() => setActiveDialog(undefined)} onClose={() => setActiveDialog(undefined)}
mapsId={[]} mapsId={[]}
fromEditor
/> />
<div className={classes.rightButtonGroup}> <div className={classes.rightButtonGroup}>