diff --git a/packages/editor/src/index.tsx b/packages/editor/src/index.tsx
index 997a7bad..13a6e37f 100644
--- a/packages/editor/src/index.tsx
+++ b/packages/editor/src/index.tsx
@@ -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(() => {
diff --git a/packages/mindplot/src/components/MockPersistenceManager.ts b/packages/mindplot/src/components/MockPersistenceManager.ts
new file mode 100644
index 00000000..f139dc20
--- /dev/null
+++ b/packages/mindplot/src/components/MockPersistenceManager.ts
@@ -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;
diff --git a/packages/mindplot/src/components/PersistenceManager.ts b/packages/mindplot/src/components/PersistenceManager.ts
index a56bb2c7..c235ec01 100644
--- a/packages/mindplot/src/components/PersistenceManager.ts
+++ b/packages/mindplot/src/components/PersistenceManager.ts
@@ -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;
diff --git a/packages/mindplot/src/components/RestPersistenceManager.ts b/packages/mindplot/src/components/RestPersistenceManager.ts
index 7eb076a9..d97da659 100644
--- a/packages/mindplot/src/components/RestPersistenceManager.ts
+++ b/packages/mindplot/src/components/RestPersistenceManager.ts
@@ -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;
},
diff --git a/packages/mindplot/src/components/widget/AccountSettingsPanel.js b/packages/mindplot/src/components/widget/AccountSettingsPanel.js
index a91d05bc..29e4ebea 100644
--- a/packages/mindplot/src/components/widget/AccountSettingsPanel.js
+++ b/packages/mindplot/src/components/widget/AccountSettingsPanel.js
@@ -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 = `
${global.accountName}
${global.accountEmail}
+
Logout
diff --git a/packages/mindplot/src/index.ts b/packages/mindplot/src/index.ts
index ed95a2ee..550eb69d 100644
--- a/packages/mindplot/src/index.ts
+++ b/packages/mindplot/src/index.ts
@@ -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,
diff --git a/packages/webapp/src/@types/index.d.ts b/packages/webapp/src/@types/index.d.ts
index e238d01c..5d1d9747 100644
--- a/packages/webapp/src/@types/index.d.ts
+++ b/packages/webapp/src/@types/index.d.ts
@@ -1,2 +1,3 @@
declare module '*.png';
declare module '*.svg';
+declare module '*.wxml';
\ No newline at end of file
diff --git a/packages/webapp/src/app.tsx b/packages/webapp/src/app.tsx
index a07d70d5..0a9cee3b 100644
--- a/packages/webapp/src/app.tsx
+++ b/packages/webapp/src/app.tsx
@@ -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 ? (
@@ -80,13 +83,13 @@ const App = (): ReactElement => {
/>
-
+
-
+
diff --git a/packages/webapp/src/classes/app-config/index.ts b/packages/webapp/src/classes/app-config/index.ts
index 7524aed4..d586f1c0 100644
--- a/packages/webapp/src/classes/app-config/index.ts
+++ b/packages/webapp/src/classes/app-config/index.ts
@@ -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');
diff --git a/packages/webapp/src/classes/client/cache-decorator-client/index.ts b/packages/webapp/src/classes/client/cache-decorator-client/index.ts
index 8c988e4e..e1106151 100644
--- a/packages/webapp/src/classes/client/cache-decorator-client/index.ts
+++ b/packages/webapp/src/classes/client/cache-decorator-client/index.ts
@@ -1,4 +1,4 @@
-import { Mindmap } from '@wisemapping/mindplot';
+import { Mindmap, PersistenceManager } from '@wisemapping/mindplot';
import Client, {
AccountInfo,
BasicMapInfo,
@@ -17,7 +17,11 @@ class CacheDecoratorClient implements Client {
constructor(client: 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 {
return this.client.revertHistory(id, cid);
}
+
+ buildPersistenceManager(): PersistenceManager {
+ return this.client.buildPersistenceManager();
+ }
+
+ removePersistenceManager(): void {
+ return this.client.removePersistenceManager();
+ }
+
}
export default CacheDecoratorClient;
\ No newline at end of file
diff --git a/packages/webapp/src/classes/client/client-health-sentinel/index.tsx b/packages/webapp/src/classes/client/client-health-sentinel/index.tsx
index f442306f..468882bb 100644
--- a/packages/webapp/src/classes/client/client-health-sentinel/index.tsx
+++ b/packages/webapp/src/classes/client/client-health-sentinel/index.tsx
@@ -45,7 +45,7 @@ const ClientHealthSentinel = (): React.ReactElement => {
diff --git a/packages/webapp/src/classes/client/index.ts b/packages/webapp/src/classes/client/index.ts
index 09728cb3..f64ba644 100644
--- a/packages/webapp/src/classes/client/index.ts
+++ b/packages/webapp/src/classes/client/index.ts
@@ -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;
diff --git a/packages/webapp/src/classes/client/mock-client/example-map.wxml b/packages/webapp/src/classes/client/mock-client/example-map.wxml
new file mode 100644
index 00000000..6d0ab9c5
--- /dev/null
+++ b/packages/webapp/src/classes/client/mock-client/example-map.wxml
@@ -0,0 +1,70 @@
+
diff --git a/packages/webapp/src/classes/client/mock-client/index.ts b/packages/webapp/src/classes/client/mock-client/index.ts
index 57376cea..a52e094f 100644
--- a/packages/webapp/src/classes/client/mock-client/index.ts
+++ b/packages/webapp/src/classes/client/mock-client/index.ts
@@ -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,7 +35,8 @@ class MockClient implements Client {
private maps: MapInfo[] = [];
private labels: Label[] = [];
private permissionsByMap: Map = new Map();
-
+ private persistenceManager: PersistenceManager;
+
constructor() {
// Remove, just for develop ....
function createMapInfo(
@@ -109,6 +111,10 @@ class MockClient implements Client {
this.labels = [label1, label2, label3];
}
+
+ onSessionExpired(callback?: () => void): () => void {
+ return callback;
+ }
fetchMindmap(id: number): Mindmap {
const parser = new DOMParser();
@@ -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;
diff --git a/packages/webapp/src/classes/client/rest-client/index.ts b/packages/webapp/src/classes/client/rest-client/index.ts
index 0cb227e6..74fc6d84 100644
--- a/packages/webapp/src/classes/client/rest-client/index.ts
+++ b/packages/webapp/src/classes/client/rest-client/index.ts
@@ -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 = (error: { response?: AxiosResponse }): Promise<{ response?: AxiosResponse }> => {
+ // 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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
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 {
- 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 {
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 {
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 {
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 {
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=>");
diff --git a/packages/webapp/src/components/HOCs/withSessionExpirationHandling.tsx b/packages/webapp/src/components/HOCs/withSessionExpirationHandling.tsx
new file mode 100644
index 00000000..2e2fbda9
--- /dev/null
+++ b/packages/webapp/src/components/HOCs/withSessionExpirationHandling.tsx
@@ -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(Component: ComponentType) {
+ 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 (
+ <>
+
+ ;
+ >
+ );
+ };
+}
+
+export default withSessionExpirationHandling;
diff --git a/packages/webapp/src/components/editor-page/index.tsx b/packages/webapp/src/components/editor-page/index.tsx
index d757e3a5..5ce49495 100644
--- a/packages/webapp/src/components/editor-page/index.tsx
+++ b/packages/webapp/src/components/editor-page/index.tsx
@@ -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(null);
const hotkeys = useSelector(hotkeysEnabled);
const userLocale = AppI18n.getUserLocale();
+ const client: Client = useSelector(activeInstance);
+ const [persistenceManager, setPersistenceManager] = React.useState();
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 <>
-
+
{
activeDialog &&
{
const [email, setEmail] = useState('');
@@ -54,6 +55,7 @@ const ForgotPassword = () => {