Add expiration dialog.

This commit is contained in:
Matias Arriola 2022-02-21 02:50:03 +00:00 committed by Paulo Veiga
parent 93aae24988
commit 24b9cadf80
24 changed files with 415 additions and 90 deletions

View File

@ -38,13 +38,14 @@ declare global {
}
export type EditorPropsType = {
initCallback?: (locale: string) => void;
initCallback?: (locale: string, persistenceManager: PersistenceManager) => void;
mapId?: number;
isTryMode: boolean;
readOnlyMode: boolean;
locale?: string;
onAction: (action: ToolbarActionType) => void;
hotkeys?: boolean;
persistenceManager: PersistenceManager;
};
const loadLocaleData = (locale: string) => {
@ -62,33 +63,15 @@ const loadLocaleData = (locale: string) => {
}
}
const initMindplot = (locale: string) => {
const initMindplot = (locale: string, persistenceManager: PersistenceManager) => {
// Change page title ...
document.title = `${global.mapTitle} | WiseMapping `;
// Configure persistence manager ...
let persistence: PersistenceManager;
if (!global.memoryPersistence && !global.readOnly) {
persistence = new RESTPersistenceManager({
documentUrl: '/c/restful/maps/{id}/document',
revertUrl: '/c/restful/maps/{id}/history/latest',
lockUrl: '/c/restful/maps/{id}/lock',
timestamp: global.lockTimestamp,
session: global.lockSession,
});
} else {
persistence = new LocalStorageManager(
`/c/restful/maps/{id}/${global.historyId ? `${global.historyId}/` : ''}document/xml${!global.isAuth ? '-pub' : ''
}`,
true
);
}
const params = new URLSearchParams(window.location.search.substring(1));
const zoomParam = Number.parseFloat(params.get('zoom'));
const options = DesignerOptionsBuilder.buildOptions({
persistenceManager: persistence,
persistenceManager,
readOnly: Boolean(global.readOnly || false),
mapId: String(global.mapId),
container: 'mindplot',
@ -120,9 +103,10 @@ const Editor = ({
locale = 'en',
onAction,
hotkeys = true,
persistenceManager,
}: EditorPropsType): React.ReactElement => {
React.useEffect(() => {
initCallback(locale);
initCallback(locale, persistenceManager);
}, []);
React.useEffect(() => {

View File

@ -0,0 +1,48 @@
/*
* Copyright [2022] [wisemapping]
*
* Licensed under WiseMapping Public License, Version 1.0 (the "License").
* It is basically the Apache License, Version 2.0 (the "License") plus the
* "powered by wisemapping" text requirement on every single page;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the license at
*
* http://www.wisemapping.org/license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import $ from 'jquery';
import { $assert } from '@wisemapping/core-js';
import PersistenceManager from './PersistenceManager';
class MockPersistenceManager extends PersistenceManager {
private exampleMap: string;
constructor(exampleMapAsXml: string) {
super();
$assert(exampleMapAsXml, 'The test map must be set');
this.exampleMap = exampleMapAsXml;
}
saveMapXml(): void {
// Ignore, no implementation required ...
}
discardChanges() {
// Ignore, no implementation required ...
}
loadMapDom() {
return $.parseXML(this.exampleMap);
}
unlockMap(): void {
// Ignore, no implementation required ...
}
}
export default MockPersistenceManager;

View File

@ -20,10 +20,20 @@ import { $assert } from '@wisemapping/core-js';
import { Mindmap } from '..';
import XMLSerializerFactory from './persistence/XMLSerializerFactory';
export type PersistenceError = {
severity: string;
message: string;
errorType?: 'session-expired' | 'bad-request' | 'generic';
};
export type PersistenceErrorCallback = (error: PersistenceError) => void;
abstract class PersistenceManager {
// eslint-disable-next-line no-use-before-define
static _instance: PersistenceManager;
private _errorHandlers: PersistenceErrorCallback[] = [];
save(mindmap: Mindmap, editorProperties, saveHistory: boolean, events?) {
$assert(mindmap, 'mindmap can not be null');
$assert(editorProperties, 'editorProperties can not be null');
@ -48,6 +58,24 @@ abstract class PersistenceManager {
return PersistenceManager.loadFromDom(mapId, domDocument);
}
triggerError(error: PersistenceError) {
this._errorHandlers.forEach((handler) => handler(error));
}
addErrorHandler(callback: PersistenceErrorCallback) {
this._errorHandlers.push(callback);
}
removeErrorHandler(callback?: PersistenceErrorCallback) {
if (!callback) {
this._errorHandlers.length = 0;
}
const index = this._errorHandlers.findIndex((handler) => handler === callback);
if (index !== -1) {
this._errorHandlers.splice(index, 1);
}
}
abstract discardChanges(mapId: string): void;
abstract loadMapDom(mapId: string): Document;

View File

@ -18,7 +18,7 @@
import { $assert } from '@wisemapping/core-js';
import $ from 'jquery';
import { $msg } from './Messages';
import PersistenceManager from './PersistenceManager';
import PersistenceManager, { PersistenceError } from './PersistenceManager';
class RESTPersistenceManager extends PersistenceManager {
private documentUrl: string;
@ -76,7 +76,7 @@ class RESTPersistenceManager extends PersistenceManager {
method: 'PUT',
// Blob helps to resuce the memory on large payload.
body: new Blob([JSON.stringify(data)], { type: 'text/plain' }),
headers: { 'Content-Type': 'application/json; charset=utf-8', Accept: 'application/json' },
headers: { 'Content-Type': 'application/json; charset=utf-8', Accept: 'application/json', 'X-CSRF-Token': this.getCSRFToken() },
},
).then(async (response: Response) => {
if (response.ok) {
@ -86,7 +86,7 @@ class RESTPersistenceManager extends PersistenceManager {
console.log(`Saving error: ${response.status}`);
let userMsg;
if (response.status === 405) {
userMsg = { severity: 'SEVERE', message: $msg('SESSION_EXPIRED') };
userMsg = { severity: 'SEVERE', message: $msg('SESSION_EXPIRED'), errorType: 'session-expired' };
} else {
const responseText = await response.text();
const contentType = response.headers['Content-Type'];
@ -101,6 +101,7 @@ class RESTPersistenceManager extends PersistenceManager {
userMsg = persistence._buildError(serverMsg);
}
}
this.triggerError(userMsg);
events.onError(userMsg);
}
@ -109,9 +110,11 @@ class RESTPersistenceManager extends PersistenceManager {
clearTimeout(persistence.clearTimeout);
}
persistence.onSave = false;
}).catch((error) => {
console.log(`Unexpected save error => ${error}`);
const userMsg = { severity: 'SEVERE', message: $msg('SAVE_COULD_NOT_BE_COMPLETED') };
}).catch(() => {
const userMsg: PersistenceError = {
severity: 'SEVERE', message: $msg('SAVE_COULD_NOT_BE_COMPLETED'), errorType: 'generic',
};
this.triggerError(userMsg);
events.onError(userMsg);
// Clear event timeout ...
@ -127,7 +130,7 @@ class RESTPersistenceManager extends PersistenceManager {
fetch(this.revertUrl.replace('{id}', mapId),
{
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=utf-8', Accept: 'application/json' },
headers: { 'Content-Type': 'application/json; charset=utf-8', Accept: 'application/json', 'X-CSRF-Token': this.getCSRFToken() },
});
}
@ -136,7 +139,7 @@ class RESTPersistenceManager extends PersistenceManager {
this.lockUrl.replace('{id}', mapId),
{
method: 'PUT',
headers: { 'Content-Type': 'text/plain' },
headers: { 'Content-Type': 'text/plain', 'X-CSRF-Token': this.getCSRFToken() },
body: 'false',
},
);
@ -156,14 +159,17 @@ class RESTPersistenceManager extends PersistenceManager {
return { severity, message };
}
private getCSRFToken(): string {
return document.head.querySelector('meta[name="_csrf"]').getAttribute('content');
}
loadMapDom(mapId: string): Document {
// Let's try to open one from the local directory ...
let xml: Document;
$.ajax({
url: `${this.documentUrl.replace('{id}', mapId)}/xml`,
method: 'get',
async: false,
headers: { 'Content-Type': 'text/plain', Accept: 'application/xml' },
headers: { 'Content-Type': 'text/plain', Accept: 'application/xml', 'X-CSRF-Token': this.getCSRFToken() },
success(responseText) {
xml = responseText;
},

View File

@ -25,7 +25,8 @@ class AccountSettingsPanel extends ListToolbarPanel {
// Overwite default behaviour ...
},
setValue() {
window.location = '/c/logout';
const elem = document.getElementById('logoutFrom');
elem.submit();
},
};
super(elemId, model);
@ -59,6 +60,7 @@ class AccountSettingsPanel extends ListToolbarPanel {
content[0].innerHTML = `
<p style='text-align:center;font-weight:bold;'>${global.accountName}</p>
<p>${global.accountEmail}</p>
<form action="/c/logout" method='POST' id="logoutFrom"></form>
<div id="account-logout" model='logout' style='text-align:center'>
Logout
</div>

View File

@ -22,6 +22,7 @@ import PersistenceManager from './components/PersistenceManager';
import Designer from './components/Designer';
import LocalStorageManager from './components/LocalStorageManager';
import RESTPersistenceManager from './components/RestPersistenceManager';
import MockPersistenceManager from './components/MockPersistenceManager';
import Menu from './components/widget/Menu';
import DesignerOptionsBuilder from './components/DesignerOptionsBuilder';
import ImageExporterFactory from './components/export/ImageExporterFactory';
@ -48,6 +49,7 @@ export {
DesignerBuilder,
PersistenceManager,
RESTPersistenceManager,
MockPersistenceManager,
LocalStorageManager,
DesignerOptionsBuilder,
buildDesigner,

View File

@ -1,2 +1,3 @@
declare module '*.png';
declare module '*.svg';
declare module '*.wxml';

View File

@ -17,6 +17,7 @@ import { ThemeProvider, Theme, StyledEngineProvider } from '@mui/material/styles
import ReactGA from 'react-ga';
import EditorPage from './components/editor-page';
import AppConfig from './classes/app-config';
import withSessionExpirationHandling from './components/HOCs/withSessionExpirationHandling';
declare module '@mui/styles/defaultTheme' {
@ -43,6 +44,8 @@ const App = (): ReactElement => {
const istTryMode = global.memoryPersistence;
const mapId = parseInt(global.mapId, 10);
const EditorPageComponent = withSessionExpirationHandling(EditorPage);
return locale.message ? (
<Provider store={store}>
<QueryClientProvider client={queryClient}>
@ -80,13 +83,13 @@ const App = (): ReactElement => {
/>
<Route
exact path="/c/maps/"
component={MapsPage}
component={withSessionExpirationHandling(MapsPage)}
/>
<Route exact path="/c/maps/:id/edit">
<EditorPage isTryMode={istTryMode} mapId={mapId} />
<EditorPageComponent isTryMode={istTryMode} mapId={mapId} />
</Route>
<Route exact path="/c/maps/:id/try">
<EditorPage isTryMode={istTryMode} mapId={mapId} />
<EditorPageComponent isTryMode={istTryMode} mapId={mapId} />
</Route>
</Switch>
</Router>

View File

@ -1,4 +1,3 @@
import { sessionExpired } from "../../redux/clientSlice";
import Client from "../client";
import CacheDecoratorClient from "../client/cache-decorator-client";
import MockClient from "../client/mock-client";
@ -53,9 +52,7 @@ class _AppConfig {
const config = this.getInstance();
let result: Client;
if (config.clientType == 'rest') {
result = new RestClient(config.apiBaseUrl, () => {
sessionExpired();
});
result = new RestClient(config.apiBaseUrl);
console.log('Service using rest client. ' + JSON.stringify(config));
} else {
console.log('Warning:Service using mockservice client');

View File

@ -1,4 +1,4 @@
import { Mindmap } from '@wisemapping/mindplot';
import { Mindmap, PersistenceManager } from '@wisemapping/mindplot';
import Client, {
AccountInfo,
BasicMapInfo,
@ -18,6 +18,10 @@ class CacheDecoratorClient implements Client {
this.client = client;
}
onSessionExpired(callback?: () => void): () => void {
return this.client.onSessionExpired(callback);
}
fetchMindmap(id: number): Mindmap {
return this.client.fetchMindmap(id);
}
@ -125,6 +129,15 @@ class CacheDecoratorClient implements Client {
revertHistory(id: number, cid: number): Promise<void> {
return this.client.revertHistory(id, cid);
}
buildPersistenceManager(): PersistenceManager {
return this.client.buildPersistenceManager();
}
removePersistenceManager(): void {
return this.client.removePersistenceManager();
}
}
export default CacheDecoratorClient;

View File

@ -45,7 +45,7 @@ const ClientHealthSentinel = (): React.ReactElement => {
<DialogActions>
<Button type="button" color="primary" size="medium" onClick={handleOnClose}>
<FormattedMessage id="action.close-button" defaultMessage="Close" />
<FormattedMessage id="login.signin" defaultMessage="Sign In" />
</Button>
</DialogActions>
</Dialog>

View File

@ -1,4 +1,4 @@
import { Mindmap } from '@wisemapping/mindplot';
import { Mindmap, PersistenceManager } from '@wisemapping/mindplot';
import { Locale, LocaleCode } from '../app-i18n';
export type NewUser = {
@ -109,6 +109,10 @@ interface Client {
fetchMindmap(id:number): Mindmap;
buildPersistenceManager(): PersistenceManager;
removePersistenceManager(): void;
onSessionExpired(callback?: () => void): () => void;
}
export default Client;

View File

@ -0,0 +1,70 @@
<map name="3" version="tango">
<topic central="true" text="Welcome To WiseMapping" id="1" fontStyle=";;#ffffff;;;">
<icon id="sign_info"/>
<topic position="199,-112" order="0" id="30">
<text><![CDATA[5 min tutorial video ?
Follow the link !]]></text>
<link url="https://www.youtube.com/tv?vq=medium#/watch?v=rKxZwNKs9cE" urlType="url"/>
<icon id="hard_computer"/>
</topic>
<topic position="-167,-112" order="1" text="Try it Now!" id="11" fontStyle=";;#525c61;;;" bgColor="#250be3" brColor="#080559">
<icon id="face_surprise"/>
<topic position="-260,-141" order="0" text="Double Click" id="12" fontStyle=";;#525c61;;italic;"/>
<topic position="-278,-112" order="1" id="13">
<text><![CDATA[Press "enter" to add a
Sibling]]></text>
</topic>
<topic position="-271,-83" order="2" text="Drag map to move" id="14" fontStyle=";;#525c61;;italic;"/>
</topic>
<topic position="155,-18" order="2" text="Features" id="15" fontStyle=";;#525c61;;;">
<topic position="244,-80" order="0" text="Links to Sites" id="16" fontStyle=";6;#525c61;;;">
<link url="http://www.digg.com" urlType="url"/>
</topic>
<topic position="224,-30" order="1" text="Styles" id="31">
<topic position="285,-55" order="0" text="Fonts" id="17" fontStyle=";;#525c61;;;"/>
<topic position="299,-30" order="1" text="Topic Shapes" shape="line" id="19" fontStyle=";;#525c61;;;"/>
<topic position="295,-5" order="2" text="Topic Color" id="18" fontStyle=";;#525c61;;;"/>
</topic>
<topic position="229,20" order="2" text="Icons" id="20" fontStyle=";;#525c61;;;">
<icon id="object_rainbow"/>
</topic>
<topic position="249,45" order="3" text="History Changes" id="21" fontStyle=";;#525c61;;;">
<icon id="arrowc_turn_left"/>
</topic>
</topic>
<topic position="-176,-21" order="3" text="Mind Mapping" id="6" fontStyle=";;#525c61;;;" bgColor="#edabff">
<icon id="thumb_thumb_up"/>
<topic position="-293,-58" order="0" text="Share with Collegues" id="7" fontStyle=";;#525c61;;;"/>
<topic position="-266,-33" order="1" text="Online" id="8" fontStyle=";;#525c61;;;"/>
<topic position="-288,-8" order="2" text="Anyplace, Anytime" id="9" fontStyle=";;#525c61;;;"/>
<topic position="-266,17" order="3" text="Free!!!" id="10" fontStyle=";;#525c61;;;"/>
</topic>
<topic position="171,95" order="4" text="Productivity" id="2" fontStyle=";;#525c61;;;" bgColor="#d9b518">
<icon id="chart_bar"/>
<topic position="281,70" order="0" text="Share your ideas" id="3" fontStyle=";;#525c61;;;">
<icon id="bulb_light_on"/>
</topic>
<topic position="270,95" order="1" text="Brainstorming" id="4" fontStyle=";;#525c61;;;"/>
<topic position="256,120" order="2" text="Visual " id="5" fontStyle=";;#525c61;;;"/>
</topic>
<topic position="-191,54" order="5" text="Install In Your Server" id="27" fontStyle=";;#525c61;;;">
<icon id="hard_computer"/>
<topic position="-319,42" order="0" text="Open Source" id="29" fontStyle=";;#525c61;;;">
<icon id="soft_penguin"/>
<link url="http://www.wisemapping.org/" urlType="url"/>
</topic>
<topic position="-310,67" order="1" text="Download" id="28" fontStyle=";;#525c61;;;">
<link url="http://www.wisemapping.com/inyourserver.html" urlType="url"/>
</topic>
</topic>
<topic position="-169,117" order="7" text="Collaborate" id="32">
<icon id="people_group"/>
<topic position="-253,92" order="0" text="Embed" id="33"/>
<topic position="-254,117" order="1" text="Publish" id="34"/>
<topic position="-277,142" order="2" text="Share for Edition" id="35">
<icon id="mail_envelop"/>
</topic>
</topic>
</topic>
<relationship srcTopicId="30" destTopicId="11" lineType="3" srcCtrlPoint="-80,-56" destCtrlPoint="110,-116" endArrow="false" startArrow="true"/>
</map>

View File

@ -1,4 +1,4 @@
import { Mindmap } from '@wisemapping/mindplot';
import { Mindmap, MockPersistenceManager, PersistenceManager } from '@wisemapping/mindplot';
import XMLSerializerTango from '@wisemapping/mindplot/src/components/persistence/XMLSerializerTango';
import Client, {
AccountInfo,
@ -11,6 +11,7 @@ import Client, {
Permission,
} from '..';
import { LocaleCode, localeFromStr } from '../../app-i18n';
import exampleMap from './example-map.wxml';
const label1: Label = {
id: 1,
@ -34,6 +35,7 @@ class MockClient implements Client {
private maps: MapInfo[] = [];
private labels: Label[] = [];
private permissionsByMap: Map<number, Permission[]> = new Map();
private persistenceManager: PersistenceManager;
constructor() {
// Remove, just for develop ....
@ -110,6 +112,10 @@ class MockClient implements Client {
this.labels = [label1, label2, label3];
}
onSessionExpired(callback?: () => void): () => void {
return callback;
}
fetchMindmap(id: number): Mindmap {
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(`
@ -392,6 +398,21 @@ class MockClient implements Client {
console.log('email:' + email);
return Promise.resolve();
}
buildPersistenceManager(): PersistenceManager {
if (this.persistenceManager){
return this.persistenceManager;
}
const persistence: PersistenceManager = new MockPersistenceManager(exampleMap);
this.persistenceManager = persistence;
return persistence;
}
removePersistenceManager(): void {
if (this.persistenceManager) {
delete this.persistenceManager;
}
}
}
export default MockClient;

View File

@ -1,5 +1,6 @@
import { LocalStorageManager, Mindmap } from '@wisemapping/mindplot';
import axios from 'axios';
import { LocalStorageManager, Mindmap, PersistenceManager, RESTPersistenceManager } from '@wisemapping/mindplot';
import { PersistenceError } from '@wisemapping/mindplot/src/components/PersistenceManager';
import axios, { AxiosInstance, AxiosResponse } from 'axios';
import Client, {
ErrorInfo,
MapInfo,
@ -11,15 +12,46 @@ import Client, {
ImportMapInfo,
Permission,
} from '..';
import { getCsrfToken } from '../../../utils';
import { LocaleCode, localeFromStr } from '../../app-i18n';
export default class RestClient implements Client {
private baseUrl: string;
private sessionExpired: () => void;
private persistenceManager: PersistenceManager;
private axios: AxiosInstance;
constructor(baseUrl: string, sessionExpired: () => void) {
private checkResponseForSessionExpired = <T>(error: { response?: AxiosResponse<T> }): Promise<{ response?: AxiosResponse<T> }> => {
// TODO: Improve session timeout response and response handling
if (error.response && error.response.status === 405) {
this.sessionExpired();
}
return Promise.reject(error);
};
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
this.sessionExpired = sessionExpired;
this.axios = axios.create({ maxRedirects: 0 });
const csrfToken = getCsrfToken();
if (csrfToken) {
this.axios.defaults.headers['X-CSRF-TOKEN'] = csrfToken;
} else {
console.warn('csrf token not found in html head');
}
this.axios.interceptors.response.use((r) => r, (r) => this.checkResponseForSessionExpired(r));
}
private _onSessionExpired : () => void;
onSessionExpired(callback?: () => void): () => void {
if (callback) {
this._onSessionExpired = callback;
}
return this._onSessionExpired;
}
private sessionExpired() {
if (this._onSessionExpired) {
this._onSessionExpired();
}
}
fetchMindmap(id: number): Mindmap {
@ -34,7 +66,7 @@ export default class RestClient implements Client {
deleteMapPermission(id: number, email: string): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios
this.axios
.delete(`${this.baseUrl}/c/restful/maps/${id}/collabs?email=${encodeURIComponent(email)}`, {
headers: { 'Content-Type': 'text/plain' },
})
@ -51,7 +83,7 @@ export default class RestClient implements Client {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
addMapPermissions(id: number, message: string, permissions: Permission[]): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios
this.axios
.put(
`${this.baseUrl}/c/restful/maps/${id}/collabs/`,
{
@ -77,7 +109,7 @@ export default class RestClient implements Client {
success: (labels: Permission[]) => void,
reject: (error: ErrorInfo) => void
) => {
axios
this.axios
.get(`${this.baseUrl}/c/restful/maps/${id}/collabs`, {
headers: { 'Content-Type': 'text/plain' },
})
@ -104,7 +136,7 @@ export default class RestClient implements Client {
deleteAccount(): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios
this.axios
.delete(`${this.baseUrl}/c/restful/account`, {
headers: { 'Content-Type': 'text/plain' },
})
@ -121,12 +153,12 @@ export default class RestClient implements Client {
updateAccountInfo(firstname: string, lastname: string): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios
this.axios
.put(`${this.baseUrl}/c/restful/account/firstname`, firstname, {
headers: { 'Content-Type': 'text/plain' },
})
.then(() => {
return axios.put(`${this.baseUrl}/c/restful/account/lastname`, lastname, {
return this.axios.put(`${this.baseUrl}/c/restful/account/lastname`, lastname, {
headers: { 'Content-Type': 'text/plain' },
});
})
@ -144,7 +176,7 @@ export default class RestClient implements Client {
updateAccountPassword(pasword: string): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios
this.axios
.put(`${this.baseUrl}/c/restful/account/password`, pasword, {
headers: { 'Content-Type': 'text/plain' },
})
@ -161,7 +193,7 @@ export default class RestClient implements Client {
updateAccountLanguage(locale: LocaleCode): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios
this.axios
.put(`${this.baseUrl}/c/restful/account/locale`, locale, {
headers: { 'Content-Type': 'text/plain' },
})
@ -179,7 +211,7 @@ export default class RestClient implements Client {
importMap(model: ImportMapInfo): Promise<number> {
const handler = (success: (mapId: number) => void, reject: (error: ErrorInfo) => void) => {
axios
this.axios
.post(
`${this.baseUrl}/c/restful/maps?title=${encodeURIComponent(model.title)}&description=${model.description ? model.description : ''
}`,
@ -203,7 +235,7 @@ export default class RestClient implements Client {
success: (account: AccountInfo) => void,
reject: (error: ErrorInfo) => void
) => {
axios
this.axios
.get(`${this.baseUrl}/c/restful/account`, {
headers: { 'Content-Type': 'application/json' },
})
@ -227,7 +259,7 @@ export default class RestClient implements Client {
deleteMaps(ids: number[]): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios
this.axios
.delete(`${this.baseUrl}/c/restful/maps/batch?ids=${ids.join()}`, {
headers: { 'Content-Type': 'text/plain' },
})
@ -245,7 +277,7 @@ export default class RestClient implements Client {
updateMapToPublic(id: number, isPublic: boolean): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios
this.axios
.put(`${this.baseUrl}/c/restful/maps/${id}/publish`, isPublic.toString(), {
headers: { 'Content-Type': 'text/plain' },
})
@ -262,7 +294,7 @@ export default class RestClient implements Client {
revertHistory(id: number, hid: number): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios
this.axios
.post(`${this.baseUrl}/c/restful/maps/${id}/history/${hid}`, null, {
headers: { 'Content-Type': 'text/pain' },
})
@ -282,7 +314,7 @@ export default class RestClient implements Client {
success: (historyList: ChangeHistory[]) => void,
reject: (error: ErrorInfo) => void
) => {
axios
this.axios
.get(`${this.baseUrl}/c/restful/maps/${id}/history/`, {
headers: { 'Content-Type': 'application/json' },
})
@ -307,12 +339,12 @@ export default class RestClient implements Client {
renameMap(id: number, basicInfo: BasicMapInfo): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios
this.axios
.put(`${this.baseUrl}/c/restful/maps/${id}/title`, basicInfo.title, {
headers: { 'Content-Type': 'text/plain' },
})
.then(() => {
return axios.put(
return this.axios.put(
`${this.baseUrl}/c/restful/maps/${id}/description`, basicInfo.description || ' ',
{ headers: { 'Content-Type': 'text/plain' } }
);
@ -332,7 +364,7 @@ export default class RestClient implements Client {
createMap(model: BasicMapInfo): Promise<number> {
const handler = (success: (mapId: number) => void, reject: (error: ErrorInfo) => void) => {
axios
this.axios
.post(
`${this.baseUrl}/c/restful/maps?title=${model.title}&description=${model.description ? model.description : ''
}`,
@ -356,7 +388,7 @@ export default class RestClient implements Client {
success: (mapsInfo: MapInfo[]) => void,
reject: (error: ErrorInfo) => void
) => {
axios
this.axios
.get(`${this.baseUrl}/c/restful/maps/`, {
headers: { 'Content-Type': 'application/json' },
})
@ -390,7 +422,7 @@ export default class RestClient implements Client {
registerNewUser(user: NewUser): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios
this.axios
.post(`${this.baseUrl}/service/users/`, JSON.stringify(user), {
headers: { 'Content-Type': 'application/json' },
})
@ -408,7 +440,7 @@ export default class RestClient implements Client {
deleteMap(id: number): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios
this.axios
.delete(`${this.baseUrl}/c/restful/maps/${id}`, {
headers: { 'Content-Type': 'application/json' },
})
@ -425,7 +457,7 @@ export default class RestClient implements Client {
resetPassword(email: string): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios
this.axios
.put(`${this.baseUrl}/service/users/resetPassword?email=${encodeURIComponent(email)}`, null, {
headers: { 'Content-Type': 'application/json' },
})
@ -444,7 +476,7 @@ export default class RestClient implements Client {
duplicateMap(id: number, basicInfo: BasicMapInfo): Promise<number> {
const handler = (success: (mapId: number) => void, reject: (error: ErrorInfo) => void) => {
axios
this.axios
.post(`${this.baseUrl}/c/restful/maps/${id}`, JSON.stringify(basicInfo), {
headers: { 'Content-Type': 'application/json' },
})
@ -462,9 +494,8 @@ export default class RestClient implements Client {
}
updateStarred(id: number, starred: boolean): Promise<void> {
console.debug(`Starred => ${starred}`)
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios
this.axios
.put(`${this.baseUrl}/c/restful/maps/${id}/starred`, starred.toString(), {
headers: { 'Content-Type': 'text/plain' },
})
@ -485,7 +516,7 @@ export default class RestClient implements Client {
success: (labels: Label[]) => void,
reject: (error: ErrorInfo) => void
) => {
axios
this.axios
.get(`${this.baseUrl}/c/restful/labels/`, {
headers: { 'Content-Type': 'application/json' },
})
@ -512,7 +543,7 @@ export default class RestClient implements Client {
createLabel(title: string, color: string): Promise<number> {
const handler = (success: (labelId: number) => void, reject: (error: ErrorInfo) => void) => {
axios
this.axios
.post(`${this.baseUrl}/c/restful/labels`, JSON.stringify({ title, color, iconName: 'smile' }), {
headers: { 'Content-Type': 'application/json' },
})
@ -529,7 +560,7 @@ export default class RestClient implements Client {
deleteLabel(id: number): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios
this.axios
.delete(`${this.baseUrl}/c/restful/labels/${id}`)
.then(() => {
success();
@ -544,7 +575,7 @@ export default class RestClient implements Client {
addLabelToMap(labelId: number, mapId: number): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios
this.axios
.post(`${this.baseUrl}/c/restful/maps/${mapId}/labels`, JSON.stringify(labelId), {
headers: { 'Content-Type': 'application/json' },
})
@ -561,7 +592,7 @@ export default class RestClient implements Client {
deleteLabelFromMap(labelId: number, mapId: number): Promise<void> {
const handler = (success: () => void, reject: (error: ErrorInfo) => void) => {
axios
this.axios
.delete(`${this.baseUrl}/c/restful/maps/${mapId}/labels/${labelId}`)
.then(() => {
success();
@ -574,6 +605,45 @@ export default class RestClient implements Client {
return new Promise(handler);
}
private onPersistenceManagerError(error: PersistenceError) {
if (error.errorType === 'session-expired') {
this.sessionExpired();
}
}
buildPersistenceManager(): PersistenceManager {
if (this.persistenceManager){
return this.persistenceManager;
}
// TODO: Move globals out, make urls configurable
let persistence: PersistenceManager;
if (!global.memoryPersistence && !global.readOnly) {
persistence = new RESTPersistenceManager({
documentUrl: '/c/restful/maps/{id}/document',
revertUrl: '/c/restful/maps/{id}/history/latest',
lockUrl: '/c/restful/maps/{id}/lock',
timestamp: global.lockTimestamp,
session: global.lockSession,
});
} else {
persistence = new LocalStorageManager(
`/c/restful/maps/{id}/${global.historyId ? `${global.historyId}/` : ''}document/xml${!global.isAuth ? '-pub' : ''
}`,
true
);
}
persistence.addErrorHandler((err) => this.onPersistenceManagerError(err));
this.persistenceManager = persistence;
return persistence;
}
removePersistenceManager(): void {
if (this.persistenceManager) {
this.persistenceManager.removeErrorHandler();
delete this.persistenceManager;
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private parseResponseOnError = (response: any): ErrorInfo => {
console.error("Backend error=>");

View File

@ -0,0 +1,32 @@
/* eslint-disable react/display-name */
import React, { ComponentType, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Client from '../../classes/client';
import ClientHealthSentinel from '../../classes/client/client-health-sentinel';
import { activeInstance, sessionExpired } from '../../redux/clientSlice';
function withSessionExpirationHandling<T>(Component: ComponentType<T>) {
return (hocProps: T): React.ReactElement => {
const client: Client = useSelector(activeInstance);
const dispatch = useDispatch();
useEffect(() => {
if (client) {
client.onSessionExpired(() => {
dispatch(sessionExpired());
});
} else {
console.warn('Session expiration wont be handled because could not find client');
}
}, []);
return (
<>
<ClientHealthSentinel />
<Component {...hocProps} />;
</>
);
};
}
export default withSessionExpirationHandling;

View File

@ -6,6 +6,9 @@ import AppI18n from '../../classes/app-i18n';
import { useSelector } from 'react-redux';
import { hotkeysEnabled } from '../../redux/editorSlice';
import ReactGA from 'react-ga';
import Client from '../../classes/client';
import { activeInstance } from '../../redux/clientSlice';
import { PersistenceManager } from '@wisemapping/mindplot';
export type EditorPropsType = {
mapId: number;
@ -16,13 +19,24 @@ const EditorPage = ({ mapId, ...props }: EditorPropsType): React.ReactElement =>
const [activeDialog, setActiveDialog] = React.useState<ActionType | null>(null);
const hotkeys = useSelector(hotkeysEnabled);
const userLocale = AppI18n.getUserLocale();
const client: Client = useSelector(activeInstance);
const [persistenceManager, setPersistenceManager] = React.useState<PersistenceManager>();
useEffect(() => {
ReactGA.pageview(window.location.pathname + window.location.search);
const persistence = client.buildPersistenceManager();
setPersistenceManager(persistence);
return () => client.removePersistenceManager();
}, []);
if (!persistenceManager) {
// persistenceManager must be ready for the editor to work
return null;
}
return <>
<Editor {...props} onAction={setActiveDialog} locale={userLocale.code} hotkeys={hotkeys} />
<Editor {...props} onAction={setActiveDialog}
locale={userLocale.code} hotkeys={hotkeys}
persistenceManager={persistenceManager} />
{
activeDialog &&
<ActionDispatcher

View File

@ -15,6 +15,7 @@ import SubmitButton from '../form/submit-button';
import ReactGA from 'react-ga';
import Typography from '@mui/material/Typography';
import { getCsrfToken, getCsrfTokenParameter } from '../../utils';
const ForgotPassword = () => {
const [email, setEmail] = useState<string>('');
@ -54,6 +55,7 @@ const ForgotPassword = () => {
<GlobalError error={error} />
<form onSubmit={handleOnSubmit}>
<input type='hidden' value={getCsrfToken()} name={getCsrfTokenParameter()} />
<Input
type="email"
name="email"

View File

@ -11,6 +11,7 @@ import Typography from '@mui/material/Typography';
import FormControl from '@mui/material/FormControl';
import Link from '@mui/material/Link';
import ReactGA from 'react-ga';
import { getCsrfToken, getCsrfTokenParameter } from '../../utils';
type ConfigStatusProps = {
enabled?: boolean;
@ -89,6 +90,7 @@ const LoginPage = (): React.ReactElement => {
<FormControl>
<form action="/c/perform-login" method="POST">
<input type='hidden' value={getCsrfToken()} name={getCsrfTokenParameter()}/>
<Input
name="username"
type="email"

View File

@ -54,7 +54,7 @@ const AccountInfoDialog = ({ onClose }: AccountInfoDialogProps): React.ReactElem
},
{
onSuccess: () => {
window.location.href = '/c/logout';
window.location.href = '/c/login';
onClose();
},
onError: (error) => {

View File

@ -28,6 +28,12 @@ const AccountMenu = (): React.ReactElement => {
setAnchorEl(null);
};
const handleLogout = (event: MouseEvent) => {
event.preventDefault();
const elem = document.getElementById('logoutFrom') as HTMLFormElement;
elem.submit();
};
const account = fetchAccount();
return (
<span>
@ -77,7 +83,8 @@ const AccountMenu = (): React.ReactElement => {
</MenuItem>
<MenuItem onClick={handleClose}>
<Link color="textSecondary" href="/c/logout">
<form action="/c/logout" method='POST' id="logoutFrom"></form>
<Link color="textSecondary" href="/c/logout" onClick={(e) => handleLogout(e)}>
<ListItemIcon>
<ExitToAppOutlined fontSize="small" />
</ListItemIcon>
@ -85,11 +92,13 @@ const AccountMenu = (): React.ReactElement => {
</Link>
</MenuItem>
</Menu>
{action == 'change-password' && (
<ChangePasswordDialog onClose={() => setAction(undefined)} />
)}
{
action == 'change-password' && (
<ChangePasswordDialog onClose={() => setAction(undefined)} />
)
}
{action == 'account-info' && <AccountInfoDialog onClose={() => setAction(undefined)} />}
</span>
</span >
);
};

View File

@ -15,7 +15,6 @@ import Client, { Label } from '../../classes/client';
import ActionDispatcher from './action-dispatcher';
import { ActionType } from './action-chooser';
import AccountMenu from './account-menu';
import ClientHealthSentinel from '../../classes/client/client-health-sentinel';
import HelpMenu from './help-menu';
import LanguageMenu from './language-menu';
import AppI18n, { Locales } from '../../classes/app-i18n';
@ -153,7 +152,6 @@ const MapsPage = (): ReactElement => {
messages={userLocale.message}
>
<div className={classes.root}>
<ClientHealthSentinel />
<AppBar
position="fixed"
className={clsx(classes.appBar, {

View File

@ -0,0 +1,15 @@
export const getCsrfToken = (): string | null => {
const meta = document.head.querySelector('meta[name="_csrf"]');
if (!meta) {
return null;
}
return meta.getAttribute('content');
};
export const getCsrfTokenParameter = (): string | null => {
const meta = document.head.querySelector('meta[name="_csrf_parameter"]');
if (!meta) {
return null;
}
return meta.getAttribute('content');
};

View File

@ -30,6 +30,10 @@ module.exports = {
test: /\.(png|jpe?g|gif|svg)$/,
type: 'asset/inline',
},
{
test: /\.wxml$/i,
type: 'asset/source',
},
],
},
optimization: {