diff --git a/packages/editor/src/hooks/useEditor/index.ts b/packages/editor/src/hooks/useEditor/index.ts index f9333fb2..3cb7beab 100644 --- a/packages/editor/src/hooks/useEditor/index.ts +++ b/packages/editor/src/hooks/useEditor/index.ts @@ -37,15 +37,18 @@ export const useEditor = ({ const [model, setModel] = useState(); // useEditor hook creates mindplotRef const mindplotRef = useRef(null); - // This is required to redraw in case of chansges in the canvas... + // This is required to redraw in case of changes in the canvas... // eslint-disable-next-line @typescript-eslint/no-unused-vars const [, setCanvasUpdate] = useState(); const { widgetManager } = useWidgetManager(); - const capability = new Capability(options.mode, mapInfo.isLocked()); + let capability; + if (options && mapInfo) { + capability = new Capability(options.mode, mapInfo.isLocked()); + } useEffect(() => { - if (!model) { + if (!model && options) { const model = new Model(mindplotRef.current); model .loadMindmap(mapInfo.getId(), persistenceManager, widgetManager) @@ -59,15 +62,17 @@ export const useEditor = ({ }); setModel(model); } - }, [mindplotRef]); + }, [mindplotRef, options]); useEffect(() => { - if (options.enableKeyboardEvents) { - DesignerKeyboard.resume(); - } else { - DesignerKeyboard.pause(); + if (options) { + if (options.enableKeyboardEvents) { + DesignerKeyboard.resume(); + } else { + DesignerKeyboard.pause(); + } } - }, [options.enableKeyboardEvents]); + }, [options, options?.enableKeyboardEvents]); return { model, mindplotRef, mapInfo, capability, options }; }; diff --git a/packages/mindplot/src/components/RestPersistenceManager.ts b/packages/mindplot/src/components/RestPersistenceManager.ts index 7e42b9fa..0e4b5a16 100644 --- a/packages/mindplot/src/components/RestPersistenceManager.ts +++ b/packages/mindplot/src/components/RestPersistenceManager.ts @@ -26,11 +26,13 @@ class RESTPersistenceManager extends PersistenceManager { private lockUrl: string; - private onSave: boolean; + private jwt: string | undefined; private clearTimeout; - constructor(options: { documentUrl: string; revertUrl: string; lockUrl: string }) { + private onSave: boolean; + + constructor(options: { documentUrl: string; revertUrl: string; lockUrl: string; jwt?: string }) { $assert(options.documentUrl, 'documentUrl can not be null'); $assert(options.revertUrl, 'revertUrl can not be null'); $assert(options.lockUrl, 'lockUrl can not be null'); @@ -40,6 +42,7 @@ class RESTPersistenceManager extends PersistenceManager { this.revertUrl = options.revertUrl; this.lockUrl = options.lockUrl; this.onSave = false; + this.jwt = options.jwt; } saveMapXml(mapId: string, mapXml: Document, pref: string, saveHistory: boolean, events): void { @@ -61,14 +64,7 @@ class RESTPersistenceManager extends PersistenceManager { const persistence = this; - const crfs = this.getCSRFToken(); - const headers = { - 'Content-Type': 'application/json; charset=utf-8', - Accept: 'application/json', - }; - if (crfs) { - headers['X-CSRF-Token'] = crfs; - } + const headers = this._buildHttpHeader('application/json; charset=utf-8', 'application/json'); fetch(`${this.documentUrl.replace('{id}', mapId)}?${query}`, { method: 'PUT', @@ -133,15 +129,7 @@ class RESTPersistenceManager extends PersistenceManager { } discardChanges(mapId: string): void { - const crfs = this.getCSRFToken(); - const headers = { - 'Content-Type': 'application/json; charset=utf-8', - Accept: 'application/json', - }; - if (crfs) { - headers['X-CSRF-Token'] = crfs; - } - + const headers = this._buildHttpHeader('application/json; charset=utf-8'); fetch(this.revertUrl.replace('{id}', mapId), { method: 'POST', headers, @@ -149,14 +137,7 @@ class RESTPersistenceManager extends PersistenceManager { } unlockMap(mapId: string): void { - const crfs = this.getCSRFToken(); - const headers = { - 'Content-Type': 'text/plain; charset=utf-8', - }; - if (crfs) { - headers['X-CSRF-Token'] = crfs; - } - + const headers = this._buildHttpHeader('text/plain; charset=utf-8'); fetch(this.lockUrl.replace('{id}', mapId), { method: 'PUT', headers, @@ -180,14 +161,7 @@ class RESTPersistenceManager extends PersistenceManager { loadMapDom(mapId: string): Promise { const url = `${this.documentUrl.replace('{id}', mapId)}/xml`; - const crfs = this.getCSRFToken(); - const headers = { - 'Content-Type': 'text/plain; charset=utf-8', - Accept: 'application/xml', - }; - if (crfs) { - headers['X-CSRF-Token'] = crfs; - } + const headers = this._buildHttpHeader('text/plain; charset=utf-8', 'application/xml'); return fetch(url, { method: 'get', @@ -202,6 +176,28 @@ class RESTPersistenceManager extends PersistenceManager { }) .then((xmlStr) => new DOMParser().parseFromString(xmlStr, 'text/xml')); } + + private _buildHttpHeader(contentType: string, accept?: string) { + const headers = { + 'Content-Type': contentType, + }; + + if (accept) { + // eslint-disable-next-line dot-notation + headers['Accept'] = accept; + } + + if (this.jwt) { + // eslint-disable-next-line dot-notation + headers['Authorization'] = `Bearer ${this.jwt} `; + } + + const crfs = this.getCSRFToken(); + if (crfs) { + headers['X-CSRF-Token'] = crfs; + } + return headers; + } } export default RESTPersistenceManager; diff --git a/packages/webapp/src/app.tsx b/packages/webapp/src/app.tsx index 23784aa4..db89b4b5 100644 --- a/packages/webapp/src/app.tsx +++ b/packages/webapp/src/app.tsx @@ -17,7 +17,7 @@ */ import React, { ReactElement, Suspense, useEffect } from 'react'; import { FormattedMessage, IntlProvider } from 'react-intl'; -import { Route, Routes, BrowserRouter as Router, useNavigate } from 'react-router-dom'; +import { Route, Routes, BrowserRouter as Router, useNavigate, useParams } from 'react-router-dom'; import ForgotPasswordSuccessPage from './components/forgot-password-success-page'; import RegistationPage from './components/registration-page'; import LoginPage from './components/login-page'; @@ -66,6 +66,11 @@ function Redirect({ to }) { return null; } +const PageEditorWhapper = ({ isTryMode }: { isTryMode: boolean }) => { + const mapId: string = useParams().id!; + return ; +}; + const App = (): ReactElement => { const locale = AppI18n.getDefaultLocale(); const overwriteView = window.errorMvcView; @@ -126,7 +131,7 @@ const App = (): ReactElement => { } > - + } /> @@ -143,7 +148,7 @@ const App = (): ReactElement => { } > - + } /> diff --git a/packages/webapp/src/classes/app-config/index.ts b/packages/webapp/src/classes/app-config/index.ts index 02a42569..5dbd9b10 100644 --- a/packages/webapp/src/classes/app-config/index.ts +++ b/packages/webapp/src/classes/app-config/index.ts @@ -38,7 +38,7 @@ class _AppConfig { googleOauth2Url: '/c/registration-google?code=aFakeCode', }; - isDevelopEnv(): boolean { + isMockEnv(): boolean { const config = this.getInstance(); return config.clientType === 'mock'; } diff --git a/packages/webapp/src/classes/client/index.ts b/packages/webapp/src/classes/client/index.ts index c25c3c9c..3d6c32d0 100644 --- a/packages/webapp/src/classes/client/index.ts +++ b/packages/webapp/src/classes/client/index.ts @@ -1,3 +1,20 @@ +/* + * Copyright [2021] [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 { Locale, LocaleCode } from '../app-i18n'; export type JwtAuth = { @@ -42,6 +59,14 @@ export type MapInfo = { role: Role; }; +export type MapMetadata = { + id: number; + title: string; + isLocked: boolean; + isLockedBy?: string; + zoom: number; +}; + export type ChangeHistory = { id: number; lastModificationBy: string; @@ -99,6 +124,7 @@ interface Client { deleteMap(id: number): Promise; renameMap(id: number, basicInfo: BasicMapInfo): Promise; fetchAllMaps(): Promise; + fetchMapMetadata(id: number): Promise; fetchStarred(id: number): Promise; diff --git a/packages/webapp/src/classes/client/mock-client/index.ts b/packages/webapp/src/classes/client/mock-client/index.ts index d9fe56e0..f34e3f0c 100644 --- a/packages/webapp/src/classes/client/mock-client/index.ts +++ b/packages/webapp/src/classes/client/mock-client/index.ts @@ -27,6 +27,7 @@ import Client, { Oauth2CallbackResult, ForgotPasswordResult, JwtAuth, + MapMetadata, } from '..'; import { LocaleCode, localeFromStr } from '../../app-i18n'; import Cookies from 'universal-cookie'; @@ -128,6 +129,15 @@ class MockClient implements Client { this.labels = [label1, label2, label3]; } + fetchMapMetadata(id: number): Promise { + return Promise.resolve({ + title: 'my map', + id: id, + isLocked: false, + zoom: 0.8, + }); + } + logout(): Promise { return Promise.resolve(); } diff --git a/packages/webapp/src/classes/client/rest-client/index.ts b/packages/webapp/src/classes/client/rest-client/index.ts index eaba553b..7ec64c8d 100644 --- a/packages/webapp/src/classes/client/rest-client/index.ts +++ b/packages/webapp/src/classes/client/rest-client/index.ts @@ -1,3 +1,20 @@ +/* + * Copyright [2021] [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 axios, { AxiosInstance, AxiosResponse } from 'axios'; import Client, { ErrorInfo, @@ -12,6 +29,7 @@ import Client, { Oauth2CallbackResult, ForgotPasswordResult, JwtAuth, + MapMetadata, } from '..'; import { getCsrfToken } from '../../../utils'; import { LocaleCode, localeFromStr } from '../../app-i18n'; @@ -20,11 +38,11 @@ import Cookies from 'universal-cookie'; export default class RestClient implements Client { private baseUrl: string; private axios: AxiosInstance; + private _onSessionExpired: () => void; private checkResponseForSessionExpired = (error: { response?: AxiosResponse; }): Promise<{ response?: AxiosResponse }> => { - // TODO: Improve session timeout response and response handling if (error.response && (error.response.status === 405 || error.response.status === 403)) { this.sessionExpired(); } @@ -63,6 +81,28 @@ export default class RestClient implements Client { ); } + fetchMapMetadata(id: number): Promise { + const handler = ( + success: (mapMetadata: MapMetadata) => void, + reject: (error: ErrorInfo) => void, + ) => { + this.axios + .get(`${this.baseUrl}/api/restful/maps/${id}/metadata`, { + headers: { 'Content-Type': 'application/json' }, + }) + .then((response) => { + const data = response.data; + success(data); + }) + .catch((error) => { + const errorInfo = this.parseResponseOnError(error.response); + reject(errorInfo); + }); + }; + + return new Promise(handler); + } + logout(): Promise { // Set jwt token on cookie ... const cookies = new Cookies(); @@ -100,7 +140,6 @@ export default class RestClient implements Client { return token ? `Bearer ${token}` : null; } - private _onSessionExpired: () => void; onSessionExpired(callback?: () => void): () => void { if (callback) { this._onSessionExpired = callback; diff --git a/packages/webapp/src/components/editor-page/EditorOptionsBuilder.ts b/packages/webapp/src/components/editor-page/EditorOptionsBuilder.ts deleted file mode 100644 index 37b93472..00000000 --- a/packages/webapp/src/components/editor-page/EditorOptionsBuilder.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { EditorOptions } from '@wisemapping/editor'; -import { EditorRenderMode } from '@wisemapping/editor'; -import AppConfig from '../../classes/app-config'; - -class EditorOptionsBuilder { - static build(locale: string, mode: EditorRenderMode, hotkeys: boolean): EditorOptions { - let options: EditorOptions = { - enableKeyboardEvents: hotkeys, - locale: locale, - mode: mode, - }; - - if (!AppConfig.isDevelopEnv()) { - options = { - zoom: globalThis.userOptions?.zoom ? globalThis?.userOptions?.zoom : 0.8, - locked: globalThis.mindmapLocked, - lockedMsg: globalThis.mindmapLockedMsg, - mapTitle: globalThis.mapTitle, - ...options, - }; - } else { - // Running in a development mode. - console.log('Running editor in development mode'); - options = { - zoom: 0.8, - locked: false, - mapTitle: 'Develop Mindnap', - ...options, - }; - } - return options; - } - - static loadMapId(): number { - const result = !AppConfig.isDevelopEnv() ? globalThis.mapId : 11; - if (result === undefined) { - throw Error( - `Could not resolve mapId. Map Id: globalThis.mapId: ${result} , globalThis.mapTitle: ${globalThis.mapTitle}, globalThis.lockSession: ${globalThis.lockSession}`, - ); - } - return result; - } -} -export default EditorOptionsBuilder; diff --git a/packages/webapp/src/components/editor-page/PersistenceManagerUtils.ts b/packages/webapp/src/components/editor-page/PersistenceManagerUtils.ts index 052dc802..bc7aeecc 100644 --- a/packages/webapp/src/components/editor-page/PersistenceManagerUtils.ts +++ b/packages/webapp/src/components/editor-page/PersistenceManagerUtils.ts @@ -4,7 +4,7 @@ import { LocalStorageManager, Mindmap, XMLSerializerFactory } from '@wisemapping export const fetchMindmap = async (mapId: number): Promise => { let mindmap: Mindmap; if (AppConfig.isRestClient()) { - const persistence = new LocalStorageManager(`/c/restful/maps/{id}/document/xml`, true); + const persistence = new LocalStorageManager(`/api/restful/maps/{id}/document/xml`, true); mindmap = await persistence.load(String(mapId)); } else { const parser = new DOMParser(); diff --git a/packages/webapp/src/components/editor-page/index.tsx b/packages/webapp/src/components/editor-page/index.tsx index 5e829e96..1e51a743 100644 --- a/packages/webapp/src/components/editor-page/index.tsx +++ b/packages/webapp/src/components/editor-page/index.tsx @@ -34,8 +34,8 @@ import { useFetchMapById, activeInstance, sessionExpired, + useFetchMapMetadata, } from '../../redux/clientSlice'; -import EditorOptionsBuilder from './EditorOptionsBuilder'; import { useTheme } from '@mui/material/styles'; import MapInfoImpl from '../../classes/editor-map-info'; import { MapInfo } from '@wisemapping/editor'; @@ -43,24 +43,31 @@ import Client from '../../classes/client'; import AppConfig from '../../classes/app-config'; import exampleMap from '../../classes/client/mock-client/example-map.wxml'; import ClientHealthSentinel from '../common/client-health-sentinel'; +import Cookies from 'universal-cookie'; const buildPersistenceManagerForEditor = (mode: string): PersistenceManager => { let persistenceManager: PersistenceManager; if (AppConfig.isRestClient()) { if (mode === 'edition-owner' || mode === 'edition-editor') { + // Fetch JWT token ... + const cookies = new Cookies(); + const token = cookies.get('jwt-auth-token'); + persistenceManager = new RESTPersistenceManager({ - documentUrl: '/c/restful/maps/{id}/document', - revertUrl: '/c/restful/maps/{id}/history/latest', - lockUrl: '/c/restful/maps/{id}/lock', + documentUrl: '/api/restful/maps/{id}/document', + revertUrl: '/api/restful/maps/{id}/history/latest', + lockUrl: '/api/restful/maps/{id}/lock', + jwt: token, }); } else { persistenceManager = new LocalStorageManager( - `/c/restful/maps/{id}/${ + `/api/restful/maps/{id}/${ globalThis.historyId ? `${globalThis.historyId}/` : '' }document/xml${mode === 'showcase' ? '-pub' : ''}`, true, ); } + persistenceManager.addErrorHandler((error) => { if (error.errorType === 'session-expired') { // TODO: this line was in RestPersistenceClient, do something similar here @@ -75,6 +82,7 @@ const buildPersistenceManagerForEditor = (mode: string): PersistenceManager => { export type EditorPropsType = { isTryMode: boolean; + mapId: number; }; type ActionType = @@ -97,7 +105,14 @@ type ActionType = const ActionDispatcher = React.lazy(() => import('../maps-page/action-dispatcher')); const AccountMenu = React.lazy(() => import('../maps-page/account-menu')); -const EditorPage = ({ isTryMode }: EditorPropsType): React.ReactElement => { +type EditorMetadata = { + mode: EditorRenderMode; + title: string; + isLocked: boolean; + zoom: number; +}; + +const EditorPage = ({ mapId, isTryMode }: EditorPropsType): React.ReactElement => { const [activeDialog, setActiveDialog] = React.useState(null); const hotkey = useSelector(hotkeysEnabled); const userLocale = AppI18n.getUserLocale(); @@ -119,51 +134,79 @@ const EditorPage = ({ isTryMode }: EditorPropsType): React.ReactElement => { ReactGA.send({ hitType: 'pageview', page: window.location.pathname, title: `Map Editor` }); }, []); - const useFindEditorMode = (isTryMode: boolean, mapId: number): EditorRenderMode | null => { - let result: EditorRenderMode = null; - if (isTryMode) { - result = 'showcase'; - } else if (globalThis.mindmapLocked) { - result = 'viewonly'; - } else { - const fetchResult = useFetchMapById(mapId); - if (!fetchResult.isLoading) { - if (fetchResult.error) { - throw new Error(`Map info could not be loaded: ${JSON.stringify(fetchResult.error)}`); - } + const useFindEditorMode = (isTryMode: boolean, mapId: number): EditorMetadata | undefined => { + let mode: EditorRenderMode = null; + let title = ''; + let isLocked = false; - if (!fetchResult.map) { + if (isTryMode) { + mode = 'showcase'; + title = 'Try map'; + isLocked = false; + } else { + const fetchMapInfoResult = useFetchMapById(mapId); + const fetchMetadataResult = useFetchMapMetadata(mapId); + + if (!fetchMapInfoResult.isLoading && !fetchMetadataResult.isLoading) { + if (fetchMapInfoResult.error || fetchMetadataResult.error) { throw new Error( - `Map info could not be loaded. Info not present: ${JSON.stringify(fetchResult)}`, + `Map info could not be loaded: ${JSON.stringify(fetchMapInfoResult.error)}`, ); } - result = `edition-${fetchResult.map.role}`; + + if (!fetchMapInfoResult.data) { + throw new Error( + `Map info could not be loaded. Info not present: ${JSON.stringify(fetchMapInfoResult)}`, + ); + } + + if (!fetchMetadataResult.data) { + throw new Error( + `Map info could not be loaded. Info not present: ${JSON.stringify( + fetchMetadataResult, + )}`, + ); + } + + if (fetchMetadataResult.data?.isLocked) { + mode = 'viewonly'; + } else { + mode = `edition-${fetchMapInfoResult.data.role}`; + } + isLocked = fetchMetadataResult.data.isLocked; + title = fetchMetadataResult.data.title; } } - return result; + return mode ? { mode: mode, isLocked: isLocked, title: title, zoom: 0.8 } : undefined; }; // What is the role ? - const mapId = EditorOptionsBuilder.loadMapId(); - const mode = useFindEditorMode(isTryMode, mapId); + const mapMetadata = useFindEditorMode(isTryMode, mapId); // Account settings can be null and editor cannot be initilized multiple times. This creates problems // at the i18n resource loading. - const isAccountLoaded = mode === 'showcase' || useFetchAccount; - const loadCompleted = mode && isAccountLoaded; + const isAccountLoaded = mapMetadata?.mode === 'showcase' || useFetchAccount; + const loadCompleted = mapMetadata && isAccountLoaded; - let options: EditorOptions, persistence: PersistenceManager; + let persistence: PersistenceManager; let mapInfo: MapInfo; + let options: EditorOptions; if (loadCompleted) { - options = EditorOptionsBuilder.build(userLocale.code, mode, hotkey); - persistence = buildPersistenceManagerForEditor(mode); + // Configure de + options = { + enableKeyboardEvents: hotkey, + locale: userLocale.code, + mode: mapMetadata.mode, + }; + + persistence = buildPersistenceManagerForEditor(mapMetadata.mode); mapInfo = new MapInfoImpl( mapId, client, - options.mapTitle, - options.locked, - options.lockedMsg, - options.zoom, + mapMetadata.title, + mapMetadata.isLocked, + '', + mapMetadata.zoom, ); } diff --git a/packages/webapp/src/components/maps-page/action-chooser/index.tsx b/packages/webapp/src/components/maps-page/action-chooser/index.tsx index 6f002a51..346efe09 100644 --- a/packages/webapp/src/components/maps-page/action-chooser/index.tsx +++ b/packages/webapp/src/components/maps-page/action-chooser/index.tsx @@ -52,7 +52,7 @@ const ActionChooser = (props: ActionProps): React.ReactElement => { }; }; - const role = mapId !== undefined ? useFetchMapById(mapId)?.map?.role : undefined; + const role = mapId !== undefined ? useFetchMapById(mapId)?.data?.role : undefined; return ( { if (map) { setModel(map); diff --git a/packages/webapp/src/components/maps-page/action-dispatcher/export-dialog/index.tsx b/packages/webapp/src/components/maps-page/action-dispatcher/export-dialog/index.tsx index 83a03779..a835bcfb 100644 --- a/packages/webapp/src/components/maps-page/action-dispatcher/export-dialog/index.tsx +++ b/packages/webapp/src/components/maps-page/action-dispatcher/export-dialog/index.tsx @@ -39,7 +39,7 @@ const ExportDialog = ({ }: ExportDialogProps): React.ReactElement => { const intl = useIntl(); const [submit, setSubmit] = React.useState(false); - const { map } = useFetchMapById(mapId); + const { data: map } = useFetchMapById(mapId); const [exportGroup, setExportGroup] = React.useState( enableImgExport ? 'image' : 'document', diff --git a/packages/webapp/src/components/maps-page/action-dispatcher/info-dialog/index.tsx b/packages/webapp/src/components/maps-page/action-dispatcher/info-dialog/index.tsx index 5f27d144..d6b66ea3 100644 --- a/packages/webapp/src/components/maps-page/action-dispatcher/info-dialog/index.tsx +++ b/packages/webapp/src/components/maps-page/action-dispatcher/info-dialog/index.tsx @@ -18,7 +18,7 @@ import LocalizedFormat from 'dayjs/plugin/localizedFormat'; dayjs.extend(LocalizedFormat); const InfoDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElement => { - const { map } = useFetchMapById(mapId); + const { data: map } = useFetchMapById(mapId); const [error, setError] = React.useState(); const intl = useIntl(); diff --git a/packages/webapp/src/components/maps-page/action-dispatcher/publish-dialog/index.tsx b/packages/webapp/src/components/maps-page/action-dispatcher/publish-dialog/index.tsx index bcf961ad..c5f83d2f 100644 --- a/packages/webapp/src/components/maps-page/action-dispatcher/publish-dialog/index.tsx +++ b/packages/webapp/src/components/maps-page/action-dispatcher/publish-dialog/index.tsx @@ -21,7 +21,7 @@ import AppConfig from '../../../../classes/app-config'; import Box from '@mui/material/Box'; const PublishDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElement => { - const { map } = useFetchMapById(mapId); + const { data: map } = useFetchMapById(mapId); const client: Client = useSelector(activeInstance); const [model, setModel] = React.useState(map ? map.isPublic : false); diff --git a/packages/webapp/src/components/maps-page/action-dispatcher/rename-dialog/index.tsx b/packages/webapp/src/components/maps-page/action-dispatcher/rename-dialog/index.tsx index 0ac55ca7..e7777e40 100644 --- a/packages/webapp/src/components/maps-page/action-dispatcher/rename-dialog/index.tsx +++ b/packages/webapp/src/components/maps-page/action-dispatcher/rename-dialog/index.tsx @@ -58,7 +58,7 @@ const RenameDialog = ({ mapId, onClose }: SimpleDialogProps): React.ReactElement setModel({ ...model, [name as keyof BasicMapInfo]: value }); }; - const { map } = useFetchMapById(mapId); + const { data: map } = useFetchMapById(mapId); useEffect(() => { if (map) { setModel(map); diff --git a/packages/webapp/src/components/maps-page/index.tsx b/packages/webapp/src/components/maps-page/index.tsx index cf4c4a91..46fcd975 100644 --- a/packages/webapp/src/components/maps-page/index.tsx +++ b/packages/webapp/src/components/maps-page/index.tsx @@ -62,6 +62,7 @@ import LabelDeleteConfirm from './maps-list/label-delete-confirm'; import ReactGA from 'react-ga4'; import { CSSObject, Interpolation, Theme } from '@emotion/react'; import withEmotionStyles from '../HOCs/withEmotionStyles'; +import { useNavigate } from 'react-router-dom'; export type Filter = GenericFilter | LabelFilter; @@ -91,6 +92,7 @@ const MapsPage = (): ReactElement => { localStorage.getItem('desktopDrawerOpen') === 'true', ); const classes = useStyles(desktopDrawerOpen); + const navigate = useNavigate(); const handleMobileDrawerToggle = () => { setMobileDrawerOpen(!mobileDrawerOpen); @@ -123,6 +125,16 @@ const MapsPage = (): ReactElement => { ReactGA.send({ hitType: 'pageview', page: window.location.pathname, title: 'Maps List' }); }, []); + useEffect(() => { + if (client) { + client.onSessionExpired(() => { + navigate('/c/login'); + }); + } else { + console.warn('Session expiration wont be handled because could not find client'); + } + }, []); + const mutation = useMutation((id: number) => client.deleteLabel(id), { onSuccess: () => { queryClient.invalidateQueries('labels'); diff --git a/packages/webapp/src/redux/clientSlice.ts b/packages/webapp/src/redux/clientSlice.ts index 9c59c025..c07aae1e 100644 --- a/packages/webapp/src/redux/clientSlice.ts +++ b/packages/webapp/src/redux/clientSlice.ts @@ -19,7 +19,7 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { createSlice } from '@reduxjs/toolkit'; import { useQuery } from 'react-query'; -import Client, { AccountInfo, ErrorInfo, MapInfo } from '../classes/client'; +import Client, { AccountInfo, ErrorInfo, MapInfo, MapMetadata } from '../classes/client'; import { useSelector } from 'react-redux'; import AppConfig from '../classes/app-config'; import { RootState } from './rootReducer'; @@ -55,7 +55,7 @@ export const clientSlice = createSlice({ type MapLoadResult = { isLoading: boolean; error: ErrorInfo | null; - map: MapInfo | undefined; + data: MapInfo | undefined; }; export const useFetchMapById = (id: number): MapLoadResult => { @@ -82,7 +82,24 @@ export const useFetchMapById = (id: number): MapLoadResult => { }; } } - return { isLoading: isLoading, error: errorMsg, map: map }; + return { isLoading: isLoading, error: errorMsg, data: map }; +}; + +type MapMetadataLoadResult = { + isLoading: boolean; + error: ErrorInfo | null; + data: MapMetadata | undefined; +}; + +export const useFetchMapMetadata = (id: number): MapMetadataLoadResult => { + const client: Client = useSelector(activeInstance); + const { isLoading, error, data } = useQuery( + `maps-metadata-${id}`, + () => { + return client.fetchMapMetadata(id); + }, + ); + return { isLoading: isLoading, error: error, data: data }; }; export const useFetchAccount = (): AccountInfo | undefined => { diff --git a/packages/webapp/webpack.prod.js b/packages/webapp/webpack.prod.js index c774972f..af79d461 100644 --- a/packages/webapp/webpack.prod.js +++ b/packages/webapp/webpack.prod.js @@ -14,6 +14,7 @@ module.exports = merge(common, { template: path.join(__dirname, 'public/index.html'), templateParameters: { PUBLIC_URL: process.env.PUBLIC_URL ? process.env.PUBLIC_URL : 'https://www.wisemapping.com', + CLIENT_TYPE: 'rest' }, base: process.env.PUBLIC_URL ? process.env.PUBLIC_URL : 'https://www.wisemapping.com', }),