From 0b4e99ac20edaf839968f2a088e9748ceb634de5 Mon Sep 17 00:00:00 2001 From: egenerse Date: Tue, 1 Oct 2024 09:56:42 +0200 Subject: [PATCH 01/17] feature: slices created --- package-lock.json | 65 +++- packages/webapp/package.json | 6 +- packages/webapp/src/main/application.tsx | 47 +-- .../apollon-editor-component.tsx | 324 ++++++++---------- .../application-bar/application-bar.tsx | 3 +- .../components/store/application-state.ts | 4 +- .../components/store/application-store.tsx | 109 +++--- .../webapp/src/main/components/store/hooks.ts | 43 +++ .../main/services/diagram/diagram-reducer.ts | 18 - .../main/services/diagram/diagram-types.ts | 7 - .../src/main/services/diagram/diagramSlice.ts | 131 +++++++ .../editor-options/editorOptionSlice.ts | 54 +++ .../error-management/errorManagementSlice.ts | 43 +++ .../src/main/services/export/useExportJson.ts | 28 ++ .../src/main/services/export/useExportPdf.ts | 36 ++ .../src/main/services/export/useExportPng.ts | 70 ++++ .../src/main/services/export/useExportSvg.ts | 27 ++ .../services/file-download/useFileDownload.ts | 32 ++ .../src/main/services/import/useImport.ts | 21 ++ .../services/local-storage/useLocalStorage.ts | 50 +++ .../src/main/services/modal/modalSlice.ts | 36 ++ packages/webapp/src/main/services/reducer.ts | 6 +- .../src/main/services/share/shareSlice.ts | 53 +++ 23 files changed, 919 insertions(+), 294 deletions(-) create mode 100644 packages/webapp/src/main/components/store/hooks.ts delete mode 100644 packages/webapp/src/main/services/diagram/diagram-reducer.ts create mode 100644 packages/webapp/src/main/services/diagram/diagramSlice.ts create mode 100644 packages/webapp/src/main/services/editor-options/editorOptionSlice.ts create mode 100644 packages/webapp/src/main/services/error-management/errorManagementSlice.ts create mode 100644 packages/webapp/src/main/services/export/useExportJson.ts create mode 100644 packages/webapp/src/main/services/export/useExportPdf.ts create mode 100644 packages/webapp/src/main/services/export/useExportPng.ts create mode 100644 packages/webapp/src/main/services/export/useExportSvg.ts create mode 100644 packages/webapp/src/main/services/file-download/useFileDownload.ts create mode 100644 packages/webapp/src/main/services/import/useImport.ts create mode 100644 packages/webapp/src/main/services/local-storage/useLocalStorage.ts create mode 100644 packages/webapp/src/main/services/modal/modalSlice.ts create mode 100644 packages/webapp/src/main/services/share/shareSlice.ts diff --git a/package-lock.json b/package-lock.json index 5e43e60..f9ce8f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2442,6 +2442,42 @@ "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.2.1.tgz", "integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==" }, + "node_modules/@reduxjs/toolkit": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.7.tgz", + "integrity": "sha512-faI3cZbSdFb8yv9dhDTmGwclW0vk0z5o1cia+kf7gCbaCwHI5e+7tP57mJUv22pNcNbeA62GSrPpfrUfdXcQ6g==", + "dependencies": { + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + }, + "node_modules/@reduxjs/toolkit/node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/@restart/hooks": { "version": "0.4.16", "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", @@ -3188,6 +3224,15 @@ "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==" }, + "node_modules/@types/websocket": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-1.0.10.tgz", + "integrity": "sha512-svjGZvPB7EzuYS94cI7a+qhwgGU1y89wUgjT6E2wVUfmAGIvRfT7obBvRtnhXCSsoMdlG4gBFGE7MfkIXZLoww==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/ws": { "version": "8.5.10", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", @@ -7128,6 +7173,15 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -9735,6 +9789,11 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -13532,6 +13591,7 @@ "version": "2.1.3", "dependencies": { "@ls1intum/apollon": "3.3.14", + "@reduxjs/toolkit": "2.2.7", "@sentry/react": "7.110.1", "bootstrap": "5.3.3", "moment": "2.30.1", @@ -13543,7 +13603,7 @@ "react-dom": "18.2.0", "react-redux": "8.1.3", "react-router-dom": "6.22.3", - "redux": "4.2.1", + "redux": "^4.2.1", "redux-observable": "2.0.0", "rxjs": "7.8.1", "shared": "2.1.3", @@ -13559,9 +13619,10 @@ "@types/react": "18.2.79", "@types/react-bootstrap": "0.32.36", "@types/react-dom": "18.2.25", - "@types/react-redux": "7.1.33", + "@types/react-redux": "^7.1.33", "@types/styled-components": "5.1.34", "@types/uuid": "9.0.8", + "@types/websocket": "^1.0.10", "@typescript-eslint/eslint-plugin": "6.21.0", "@typescript-eslint/parser": "6.21.0", "circular-dependency-plugin": "5.2.2", diff --git a/packages/webapp/package.json b/packages/webapp/package.json index cb5e773..8576adb 100644 --- a/packages/webapp/package.json +++ b/packages/webapp/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "@ls1intum/apollon": "3.3.14", + "@reduxjs/toolkit": "2.2.7", "@sentry/react": "7.110.1", "bootstrap": "5.3.3", "moment": "2.30.1", @@ -27,7 +28,7 @@ "react-dom": "18.2.0", "react-redux": "8.1.3", "react-router-dom": "6.22.3", - "redux": "4.2.1", + "redux": "^4.2.1", "redux-observable": "2.0.0", "rxjs": "7.8.1", "shared": "2.1.3", @@ -50,9 +51,10 @@ "@types/react": "18.2.79", "@types/react-bootstrap": "0.32.36", "@types/react-dom": "18.2.25", - "@types/react-redux": "7.1.33", + "@types/react-redux": "^7.1.33", "@types/styled-components": "5.1.34", "@types/uuid": "9.0.8", + "@types/websocket": "^1.0.10", "@typescript-eslint/eslint-plugin": "6.21.0", "@typescript-eslint/parser": "6.21.0", "circular-dependency-plugin": "5.2.2", diff --git a/packages/webapp/src/main/application.tsx b/packages/webapp/src/main/application.tsx index 28c0c41..83f2856 100644 --- a/packages/webapp/src/main/application.tsx +++ b/packages/webapp/src/main/application.tsx @@ -2,13 +2,7 @@ import React, { useState } from 'react'; import { ApplicationBar } from './components/application-bar/application-bar'; import { ApollonEditorWrapper } from './components/apollon-editor-component/apollon-editor-component'; import { ApollonEditor } from '@ls1intum/apollon'; -import { ApplicationStore } from './components/store/application-store'; -import { ApplicationState } from './components/store/application-state'; import { - localStorageCollaborationColor, - localStorageCollaborationName, - localStorageDiagramPrefix, - localStorageLatest, POSTHOG_HOST, POSTHOG_KEY, } from './constant'; @@ -16,55 +10,18 @@ import { ApollonEditorContext, ApollonEditorProvider, } from './components/apollon-editor-component/apollon-editor-context'; -import { uuid } from './utils/uuid'; -import moment from 'moment'; import { FirefoxIncompatibilityHint } from './components/incompatability-hints/firefox-incompatibility-hint'; -import { defaultEditorOptions } from './services/editor-options/editor-options-reducer'; -import { EditorOptions } from './services/editor-options/editor-options-types'; import { ErrorPanel } from './components/error-handling/error-panel'; -import { Diagram } from './services/diagram/diagram-types'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { ApplicationModal } from './components/modals/application-modal'; import { ToastContainer } from 'react-toastify'; import { PostHogProvider } from 'posthog-js/react'; +import { ApplicationStore } from './components/store/application-store'; const postHogOptions = { api_host: POSTHOG_HOST, }; -const getInitialStore = (): ApplicationState => { - const latestId: string | null = window.localStorage.getItem(localStorageLatest); - let diagram: { diagram: Diagram }; - const editorOptions: EditorOptions = defaultEditorOptions; - if (latestId) { - const latestDiagram: Diagram = JSON.parse(window.localStorage.getItem(localStorageDiagramPrefix + latestId)!); - diagram = { diagram: latestDiagram }; - editorOptions.type = latestDiagram?.model?.type ? latestDiagram.model.type : editorOptions.type; - } else { - diagram = { - diagram: { id: uuid(), title: 'UMLClassDiagram', model: undefined, lastUpdate: moment() }, - }; - } - - // initial application state - return { - ...diagram, - editorOptions, - errors: [], - modal: { - type: null, - size: 'sm', - }, - share: { - collaborationName: window.localStorage.getItem(localStorageCollaborationName) || '', - collaborationColor: window.localStorage.getItem(localStorageCollaborationColor) || '', - collaborators: [], - fromServer: false, - }, - }; -}; - -const initialStore = getInitialStore(); export const Application = () => { const [editor, setEditor] = useState(); @@ -79,7 +36,7 @@ export const Application = () => { return ( - + {isFirefox && } diff --git a/packages/webapp/src/main/components/apollon-editor-component/apollon-editor-component.tsx b/packages/webapp/src/main/components/apollon-editor-component/apollon-editor-component.tsx index f125e05..5124f0e 100644 --- a/packages/webapp/src/main/components/apollon-editor-component/apollon-editor-component.tsx +++ b/packages/webapp/src/main/components/apollon-editor-component/apollon-editor-component.tsx @@ -1,16 +1,13 @@ import { ApollonEditor, ApollonMode, ApollonOptions, Patch, Selection, UMLModel } from '@ls1intum/apollon'; -import React, { Component, FunctionComponent } from 'react'; +import React, { useEffect, useRef, useState, useMemo } from 'react'; import { connect } from 'react-redux'; -import { RouterTypes, withRouter } from '../../hocs/withRouter'; import { compose } from 'redux'; -import { DiagramView } from 'shared/src/main/diagram-view'; - import styled from 'styled-components'; -// @ts-ignore +import { DiagramView } from 'shared/src/main/diagram-view'; import { w3cwebsocket as W3CWebSocket } from 'websocket'; import { APPLICATION_SERVER_VERSION, DEPLOYMENT_URL, NO_HTTP_URL, WS_PROTOCOL } from '../../constant'; import { DiagramRepository } from '../../services/diagram/diagram-repository'; -import { Diagram } from '../../services/diagram/diagram-types'; + import { EditorOptionsRepository } from '../../services/editor-options/editor-options-repository'; import { ImportRepository } from '../../services/import/import-repository'; import { ModalRepository } from '../../services/modal/modal-repository'; @@ -18,11 +15,12 @@ import { ShareRepository } from '../../services/share/share-repository'; import { uuid } from '../../utils/uuid'; import { ModalContentType } from '../modals/application-modal-types'; import { ApplicationState } from '../store/application-state'; -import { ApollonEditorContext } from './apollon-editor-context'; -import { withApollonEditor } from './with-apollon-editor'; import { toast } from 'react-toastify'; import { selectionDiff } from '../../utils/selection-diff'; import { CollaborationMessage } from '../../utils/collaboration-message-type'; +import { withRouter } from '../../hocs/withRouter'; +import { withApollonEditor } from './with-apollon-editor'; +import { Diagram, DiagramState } from '../../services/diagram/diagramSlice'; const ApollonContainer = styled.div` display: flex; @@ -33,10 +31,8 @@ const ApollonContainer = styled.div` type OwnProps = {}; -type State = {}; - type StateProps = { - diagram: Diagram | null; + diagram: DiagramState; options: ApollonOptions; fromServer: boolean; collaborationName: string; @@ -49,144 +45,134 @@ type DispatchProps = { changeEditorMode: typeof EditorOptionsRepository.changeEditorMode; changeReadonlyMode: typeof EditorOptionsRepository.changeReadonlyMode; updateCollaborators: typeof ShareRepository.updateCollaborators; - gotFromServer: typeof ShareRepository.gotFromServer; openModal: typeof ModalRepository.showModal; }; -type Props = OwnProps & StateProps & DispatchProps & ApollonEditorContext & RouterTypes; +type Props = OwnProps & StateProps & DispatchProps & { params: any; location: any; setEditor: any; editor: any }; -const enhance = compose>( - withRouter, - withApollonEditor, - connect( - (state) => ({ - diagram: state.diagram, - // merge application state to valid ApollonOptions - options: { - type: state.editorOptions.type, - mode: state.editorOptions.mode, - readonly: state.editorOptions.readonly, - enablePopups: state.editorOptions.enablePopups, - copyPasteToClipboard: state.editorOptions.enableCopyPaste, - model: state.diagram?.model, - theme: state.editorOptions.theme, - locale: state.editorOptions.locale, - colorEnabled: state.editorOptions.colorEnabled, - }, - fromServer: state.share.fromServer, - collaborationName: state.share.collaborationName, - collaborationColor: state.share.collaborationColor, - }), - { - updateDiagram: DiagramRepository.updateDiagram, - importDiagram: ImportRepository.importJSON, - changeEditorMode: EditorOptionsRepository.changeEditorMode, - changeReadonlyMode: EditorOptionsRepository.changeReadonlyMode, - updateCollaborators: ShareRepository.updateCollaborators, - gotFromServer: ShareRepository.gotFromServer, - openModal: ModalRepository.showModal, - }, - ), -); +const ApollonEditorComponent: React.FC = (props) => { + const containerRef = useRef(null); + const editorRef = useRef(null); + const clientRef = useRef(null); // Ref to keep client stable + const [selection, setSelection] = useState({ elements: {}, relationships: {} }); -class ApollonEditorComponent extends Component { - private readonly containerRef: (element: HTMLDivElement) => void; - private ref?: HTMLDivElement; - private client: any; - private selection: Selection = { elements: {}, relationships: {} }; + // Memoize props.options to avoid unnecessary re-renders + const memoizedOptions = useMemo( + () => props.options, + [props.options.type, props.options.mode, props.options.readonly], + ); - componentDidUpdate(prevProps: Props) { - if (this.client) { - if ( - this.props.collaborationName !== prevProps.collaborationName || - this.props.collaborationColor !== prevProps.collaborationColor - ) { - this.setCollaborationConnectionName(); - } - this.hideSelfFromSelectedList(); + // Establish collaboration connection + const establishCollaborationConnection = (token: string, name: string, color: string) => { + if (!clientRef.current) { + const newClient = new W3CWebSocket(`${WS_PROTOCOL}://${NO_HTTP_URL}`); + clientRef.current = newClient; + + newClient.onopen = () => { + const collaborators = { name, color }; + newClient.send(JSON.stringify({ token, collaborators })); + }; + + newClient.onmessage = (message: any) => { + const { originator, collaborators, diagram, patch, selection } = JSON.parse( + message.data, + ) as CollaborationMessage; + if (collaborators) { + props.updateCollaborators(collaborators); + props.editor?.pruneRemoteSelectors(collaborators); + } + if (diagram) { + props.importDiagram(JSON.stringify(diagram)); + } + if (patch) { + props.editor?.importPatch(patch); + } + if (selection && originator) { + props.editor?.remoteSelect(originator.name, originator.color, selection.selected, selection.deselected); + } + }; } - } + }; - constructor(props: Props) { - super(props); - this.containerRef = (element: HTMLDivElement) => { - this.ref = element; - if (this.ref) { - const editor = new ApollonEditor(this.ref, this.props.options); + // Initialize the editor once, when the container is available + useEffect(() => { + console.log('DEBUGF wwowoow'); + if (containerRef.current && !editorRef.current) { + editorRef.current = new ApollonEditor(containerRef.current, memoizedOptions); - editor.subscribeToAllModelChangePatches((patch: Patch) => { - if (this.client) { - const { token } = this.props.params; - const { collaborationName, collaborationColor } = this.props; - this.client.send( - JSON.stringify({ - token, - collaborator: { name: collaborationName, color: collaborationColor }, - patch, - }), - ); - } - }); + editorRef.current.subscribeToAllModelChangePatches((patch: Patch) => { + if (clientRef.current) { + const { token } = props.params; + const { collaborationName, collaborationColor } = props; + clientRef.current.send( + JSON.stringify({ + token, + collaborator: { name: collaborationName, color: collaborationColor }, + patch, + }), + ); + } + }); - editor.subscribeToModelChange((model: UMLModel) => { - const diagram: Diagram = { ...this.props.diagram, model } as Diagram; - this.props.updateDiagram(diagram); - }); + editorRef.current.subscribeToModelChange((model: UMLModel) => { + const diagram: Diagram = { ...props.diagram.diagram, model } as Diagram; + props.updateDiagram(diagram); + }); - editor.subscribeToSelectionChange((selection: Selection) => { - const diff = selectionDiff(this.selection, selection); - this.selection = selection; + editorRef.current.subscribeToSelectionChange((newSelection: Selection) => { + const diff = selectionDiff(selection, newSelection); + setSelection(newSelection); - if (this.client && (diff.selected.length > 0 || diff.deselected.length > 0)) { - const { collaborationName, collaborationColor } = this.props; - const { token } = this.props.params; + if (clientRef.current && (diff.selected.length > 0 || diff.deselected.length > 0)) { + const { collaborationName, collaborationColor } = props; + const { token } = props.params; - this.client.send( - JSON.stringify({ - token, - collaborator: { name: collaborationName, color: collaborationColor }, - selection: diff, - }), - ); - } - }); + clientRef.current.send( + JSON.stringify({ + token, + collaborator: { name: collaborationName, color: collaborationColor }, + selection: diff, + }), + ); + } + }); - this.props.setEditor(editor); - } - }; + props.setEditor(editorRef.current); + } + }, [containerRef, memoizedOptions, props]); + + useEffect(() => { + console.log('DEBUGF wwowoow'); if (APPLICATION_SERVER_VERSION && DEPLOYMENT_URL) { - // hosted with backend - const { token } = this.props.params; + const { token } = props.params; if (token) { - // get query param - const query = new URLSearchParams(this.props.location.search); + const query = new URLSearchParams(props.location.search); const view: DiagramView | null = query.get('view') as DiagramView; const notifyUser: string | null = query.get('notifyUser'); if (view) { switch (view) { case DiagramView.SEE_FEEDBACK: - this.props.changeEditorMode(ApollonMode.Assessment); - this.props.changeReadonlyMode(true); + props.changeEditorMode(ApollonMode.Assessment); + props.changeReadonlyMode(true); break; case DiagramView.GIVE_FEEDBACK: - this.props.changeEditorMode(ApollonMode.Assessment); - this.props.changeReadonlyMode(false); + props.changeEditorMode(ApollonMode.Assessment); + props.changeReadonlyMode(false); break; case DiagramView.EDIT: - this.props.changeEditorMode(ApollonMode.Modelling); - this.props.changeReadonlyMode(false); + props.changeEditorMode(ApollonMode.Modelling); + props.changeReadonlyMode(false); break; case DiagramView.COLLABORATE: - this.props.changeEditorMode(ApollonMode.Modelling); - this.props.changeReadonlyMode(false); - // Enforces users to have color assigned to them - if (!this.props.collaborationName || !this.props.collaborationColor) { - this.props.openModal(ModalContentType.CollaborationModal, 'lg'); + props.changeEditorMode(ApollonMode.Modelling); + props.changeReadonlyMode(false); + if (!props.collaborationName || !props.collaborationColor) { + props.openModal(ModalContentType.CollaborationModal, 'lg'); } - this.establishCollaborationConnection(token, this.props.collaborationName, this.props.collaborationColor); + establishCollaborationConnection(token, props.collaborationName, props.collaborationColor); if (notifyUser === 'true') { - this.displayToast(); + displayToast(); window.history.replaceState({}, document.title, window.location.pathname + '?view=' + view); } break; @@ -194,27 +180,25 @@ class ApollonEditorComponent extends Component { } if (view !== DiagramView.COLLABORATE) { - // this check fails in development setting because webpack dev server url !== deployment url DiagramRepository.getDiagramFromServerByToken(token).then((diagram) => { if (diagram) { - this.props.importDiagram(JSON.stringify(diagram)); + props.importDiagram(JSON.stringify(diagram)); - // get query param - const queryParam = new URLSearchParams(this.props.location.search); + const queryParam = new URLSearchParams(props.location.search); const diagramView: DiagramView | null = queryParam.get('view') as DiagramView; if (diagramView) { switch (diagramView) { case DiagramView.SEE_FEEDBACK: - this.props.changeEditorMode(ApollonMode.Assessment); - this.props.changeReadonlyMode(true); + props.changeEditorMode(ApollonMode.Assessment); + props.changeReadonlyMode(true); break; case DiagramView.GIVE_FEEDBACK: - this.props.changeEditorMode(ApollonMode.Assessment); - this.props.changeReadonlyMode(false); + props.changeEditorMode(ApollonMode.Assessment); + props.changeReadonlyMode(false); break; case DiagramView.EDIT: - this.props.changeEditorMode(ApollonMode.Modelling); - this.props.changeReadonlyMode(false); + props.changeEditorMode(ApollonMode.Modelling); + props.changeReadonlyMode(false); break; } } @@ -223,9 +207,9 @@ class ApollonEditorComponent extends Component { } } } - } + }, [props.collaborationName, props.collaborationColor]); - displayToast = () => { + const displayToast = () => { toast.success( 'The link has been copied to your clipboard and can be shared to Collaborate, simply by pasting the link. You can re-access the link by going to share menu.', { @@ -234,49 +218,43 @@ class ApollonEditorComponent extends Component { ); }; - establishCollaborationConnection(token: string, name: string, color: string) { - this.client = new W3CWebSocket(`${WS_PROTOCOL}://${NO_HTTP_URL}`); - this.client.onopen = () => { - const collaborators = { name, color }; - this.client.send(JSON.stringify({ token, collaborators })); - }; - this.client.onmessage = (message: any) => { - const { originator, collaborators, diagram, patch, selection } = JSON.parse(message.data) as CollaborationMessage; - if (collaborators) { - this.props.updateCollaborators(collaborators); - this.props.editor?.pruneRemoteSelectors(collaborators); - } - if (diagram) { - this.props.importDiagram(JSON.stringify(diagram)); - } - if (patch) { - this.props.editor?.importPatch(patch); - } - if (selection && originator) { - this.props.editor?.remoteSelect(originator.name, originator.color, selection.selected, selection.deselected); - } - }; - } - - setCollaborationConnectionName() { - const { collaborationName, collaborationColor } = this.props; - this.client.send(JSON.stringify({ collaborators: { name: collaborationName, color: collaborationColor } })); - } + const key = useMemo(() => { + return (props.diagram?.diagram?.id || uuid()) + props.options.mode + props.options.type + props.options.readonly; + }, [props.diagram?.diagram?.id, props.options.mode, props.options.type, props.options.readonly]); - hideSelfFromSelectedList = () => { - const selfElementId = document.getElementById(this.props.collaborationName + '_' + this.props.collaborationColor)!; - if (selfElementId) selfElementId.style.display = 'none'; - }; + return ; +}; - render() { - // if diagram id or editor mode changes -> redraw - const key = - (this.props.diagram?.id || uuid()) + - this.props.options.mode + - this.props.options.type + - this.props.options.readonly; - return ; - } -} +const enhance = compose>( + withRouter, + withApollonEditor, + connect( + (state) => ({ + diagram: state.diagram, + options: { + type: state.editorOptions.type, + mode: state.editorOptions.mode, + readonly: state.editorOptions.readonly, + enablePopups: state.editorOptions.enablePopups, + copyPasteToClipboard: state.editorOptions.enableCopyPaste, + model: state.diagram?.diagram?.model, + theme: state.editorOptions.theme, + locale: state.editorOptions.locale, + colorEnabled: state.editorOptions.colorEnabled, + }, + fromServer: state.share.fromServer, + collaborationName: state.share.collaborationName, + collaborationColor: state.share.collaborationColor, + }), + { + updateDiagram: DiagramRepository.updateDiagram, + importDiagram: ImportRepository.importJSON, + changeEditorMode: EditorOptionsRepository.changeEditorMode, + changeReadonlyMode: EditorOptionsRepository.changeReadonlyMode, + updateCollaborators: ShareRepository.updateCollaborators, + openModal: ModalRepository.showModal, + }, + ), +); export const ApollonEditorWrapper = enhance(ApollonEditorComponent); diff --git a/packages/webapp/src/main/components/application-bar/application-bar.tsx b/packages/webapp/src/main/components/application-bar/application-bar.tsx index ac6a851..f8729ee 100644 --- a/packages/webapp/src/main/components/application-bar/application-bar.tsx +++ b/packages/webapp/src/main/components/application-bar/application-bar.tsx @@ -8,7 +8,6 @@ import { ApplicationState } from '../store/application-state'; import styled from 'styled-components'; import { DiagramRepository } from '../../services/diagram/diagram-repository'; import { appVersion } from '../../application-constants'; -import { Diagram } from '../../services/diagram/diagram-types'; import { APPLICATION_SERVER_VERSION } from '../../constant'; import { ModalRepository } from '../../services/modal/modal-repository'; import { ModalContentType } from '../modals/application-modal-types'; @@ -66,7 +65,7 @@ const getInitialState = (props: Props): State => { class ApplicationBarComponent extends Component { state = getInitialState(this.props); - + constructor(props: Props) { super(props); this.changeDiagramTitlePreview = this.changeDiagramTitlePreview.bind(this); diff --git a/packages/webapp/src/main/components/store/application-state.ts b/packages/webapp/src/main/components/store/application-state.ts index 20e4524..924b587 100644 --- a/packages/webapp/src/main/components/store/application-state.ts +++ b/packages/webapp/src/main/components/store/application-state.ts @@ -1,11 +1,11 @@ +import { Diagram, DiagramState } from '../../services/diagram/diagramSlice'; import { EditorOptions } from '../../services/editor-options/editor-options-types'; import { ApollonError } from '../../services/error-management/error-types'; import { ModalState } from '../../services/modal/modal-types'; -import { Diagram } from '../../services/diagram/diagram-types'; import { ShareState } from '../../services/share/share-types'; export interface ApplicationState { - diagram: Diagram | null; + diagram: DiagramState; editorOptions: EditorOptions; errors: ApollonError[]; modal: ModalState; diff --git a/packages/webapp/src/main/components/store/application-store.tsx b/packages/webapp/src/main/components/store/application-store.tsx index b0dac0a..3097b37 100644 --- a/packages/webapp/src/main/components/store/application-store.tsx +++ b/packages/webapp/src/main/components/store/application-store.tsx @@ -1,52 +1,81 @@ -import { ApplicationState } from './application-state'; -import React, { Component, PropsWithChildren } from 'react'; +import React from 'react'; +import { combineReducers, PreloadedState } from 'redux'; +import { Provider } from 'react-redux'; +import { configureStore } from '@reduxjs/toolkit'; +import moment from 'moment'; import { - applyMiddleware, - combineReducers, - compose, - createStore, - Dispatch, - PreloadedState, - Reducer, - Store, - StoreEnhancer, -} from 'redux'; -import { Actions } from '../../services/actions'; + localStorageLatest, + localStorageDiagramPrefix, + localStorageCollaborationName, + localStorageCollaborationColor, +} from '../../constant'; +import { defaultEditorOptions } from '../../services/editor-options/editor-options-reducer'; +import { EditorOptions } from '../../services/editor-options/editor-options-types'; +import { uuid } from '../../utils/uuid'; import { reducers } from '../../services/reducer'; -import { Provider } from 'react-redux'; -import { createEpicMiddleware, EpicMiddleware } from 'redux-observable'; -import epics from '../../services/epics'; - -type OwnProps = PropsWithChildren<{ - initialState: PreloadedState; -}>; +import { Diagram, DiagramState } from '../../services/diagram/diagramSlice'; +import { ApollonError } from '../../services/error-management/error-types'; +import { ModalState } from '../../services/modal/modal-types'; +import { ShareState } from '../../services/share/share-types'; -type Props = OwnProps; +interface ApplicationState { + diagram: DiagramState; + editorOptions: EditorOptions; + errors: ApollonError[]; + modal: ModalState; + share: ShareState; +} -const getInitialState = ( - initialState: PreloadedState, -): { store: Store } => { - const reducer: Reducer = combineReducers(reducers); - const epicMiddleware: EpicMiddleware = createEpicMiddleware(); +const getInitialStore = (): ApplicationState => { + const latestId: string | null = window.localStorage.getItem(localStorageLatest); + let diagram: Diagram; + const editorOptions: EditorOptions = defaultEditorOptions; - const middleware: StoreEnhancer<{ dispatch: Dispatch }, {}> = applyMiddleware(epicMiddleware); + if (latestId) { + const latestDiagram: Diagram = JSON.parse(window.localStorage.getItem(localStorageDiagramPrefix + latestId)!); + diagram = latestDiagram; + editorOptions.type = latestDiagram?.model?.type ? latestDiagram.model.type : editorOptions.type; + } else { + diagram = { id: uuid(), title: 'UMLClassDiagram', model: undefined, lastUpdate: moment() }; + } - const composeEnhancers: typeof compose = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; - const enhancer = composeEnhancers(middleware); + // initial application state + return { + diagram: { + diagram, + error: null, + loading: false, + }, + editorOptions, + errors: [], + modal: { + type: null, + size: 'sm', + }, + share: { + collaborationName: window.localStorage.getItem(localStorageCollaborationName) || '', + collaborationColor: window.localStorage.getItem(localStorageCollaborationColor) || '', + collaborators: [], + fromServer: false, + }, + }; +}; - const store: Store = createStore(reducer, initialState, enhancer); +const store = configureStore({ + reducer: combineReducers(reducers), + middleware: (getDefaultMiddleware) => getDefaultMiddleware(), + preloadedState: getInitialStore(), + devTools: process.env.NODE_ENV !== 'production', // Enable Redux DevTools in non-production environments +}); - epicMiddleware.run(epics); +interface Props { + children: React.ReactNode; +} - return { store }; +export const ApplicationStore: React.FC = ({ children }) => { + return {children}; }; -type State = ReturnType; - -export class ApplicationStore extends Component { - state = getInitialState(this.props.initialState); +export type AppDispatch = typeof store.dispatch; - render() { - return {this.props.children}; - } -} +export type RootState = ReturnType; diff --git a/packages/webapp/src/main/components/store/hooks.ts b/packages/webapp/src/main/components/store/hooks.ts new file mode 100644 index 0000000..7a9049e --- /dev/null +++ b/packages/webapp/src/main/components/store/hooks.ts @@ -0,0 +1,43 @@ +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' +import type { RootState, AppDispatch } from './application-store' + +// export const useAppDispatch = useDispatch.withTypes() +// export const useAppSelector = useSelector.() + +export const useAppDispatch = () => useDispatch(); + +export const useAppSelector: TypedUseSelectorHook = useSelector; + +// export type ThunkApi = object; + +// // Explicitly typing the return type of createAsyncAppThunk +// export const createAsyncAppThunk: ( +// typePrefix: string, +// payloadCreator: ( +// arg: ThunkArg, +// thunkAPI: { +// dispatch: AppDispatch; +// getState: () => RootState; +// extra: ThunkApi; +// requestId: string; +// signal: AbortSignal; +// rejectWithValue: (value: unknown) => unknown; +// } +// ) => Promise | Returned +// ) => (arg: ThunkArg) => any = createAsyncThunk.withTypes<{ +// dispatch: AppDispatch; +// extra: ThunkApi; +// state: RootState; +// }>(); + +// // Explicitly typing the return type of createAsyncAppThunkReject +// export const createAsyncAppThunkReject = () => +// createAsyncThunk.withTypes<{ +// dispatch: AppDispatch; +// extra: ThunkApi; +// rejectValue: T; +// state: RootState; +// }>(); + +// // Generic AppThunkAction type for additional use cases +// export type AppThunkAction = ThunkAction>; diff --git a/packages/webapp/src/main/services/diagram/diagram-reducer.ts b/packages/webapp/src/main/services/diagram/diagram-reducer.ts deleted file mode 100644 index 3ca6238..0000000 --- a/packages/webapp/src/main/services/diagram/diagram-reducer.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Reducer } from 'redux'; -import { Actions } from '../actions'; -import { Diagram, DiagramActionTypes } from './diagram-types'; - -export const DiagramReducer: Reducer = (state = null, action) => { - switch (action.type) { - case DiagramActionTypes.UPDATE_DIAGRAM: { - const { payload } = action; - // merges standalone state with apollon-editor state - return { - ...state, - ...payload.values, - } as Diagram; - } - } - - return state; -}; diff --git a/packages/webapp/src/main/services/diagram/diagram-types.ts b/packages/webapp/src/main/services/diagram/diagram-types.ts index 8748434..e0936d7 100644 --- a/packages/webapp/src/main/services/diagram/diagram-types.ts +++ b/packages/webapp/src/main/services/diagram/diagram-types.ts @@ -2,13 +2,6 @@ import { Action } from 'redux'; import { UMLDiagramType, UMLModel } from '@ls1intum/apollon'; import { Moment } from 'moment'; -export type Diagram = { - id: string; - title: string; - model?: UMLModel; - lastUpdate: Moment; -}; - export type DiagramActions = UpdateDiagramAction | CreateDiagramAction; export const enum DiagramActionTypes { diff --git a/packages/webapp/src/main/services/diagram/diagramSlice.ts b/packages/webapp/src/main/services/diagram/diagramSlice.ts new file mode 100644 index 0000000..67eae0f --- /dev/null +++ b/packages/webapp/src/main/services/diagram/diagramSlice.ts @@ -0,0 +1,131 @@ +import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit'; +import { UMLDiagramType, UMLModel } from '@ls1intum/apollon'; +import { DiagramRepository } from './diagram-repository'; +import { uuid } from '../../utils/uuid'; +import moment, { Moment } from 'moment'; +import { changeDiagramType } from '../editor-options/editorOptionSlice'; + +export type Diagram = { + id: string; + title: string; + model?: UMLModel; + lastUpdate: Moment; +}; + +// Define the initial state for the slice +export interface DiagramState { + diagram: Diagram | null; + loading: boolean; + error: string | null; +} + +const initialState: DiagramState = { + diagram: null, + loading: false, + error: null, +}; + +// Thunk to handle creating a diagram +export const createDiagram = createAsyncThunk( + 'diagram/create', + async ( + { diagramTitle, diagramType, template }: { diagramTitle: string; diagramType: UMLDiagramType; template?: UMLModel }, + { dispatch }, + ) => { + const diagram: Diagram = { + id: uuid(), + title: diagramTitle, + model: template, + lastUpdate: moment(), + }; + + // Dispatch the update diagram action after creation + dispatch(updateDiagram(diagram)); + dispatch(changeDiagramType(diagramType)); + + return diagram; + }, +); + +// Thunk to handle fetching a diagram from the server +export const fetchDiagramByToken = createAsyncThunk( + 'diagram/fetchByToken', + async (token: string, { rejectWithValue }) => { + try { + const diagram = await DiagramRepository.getDiagramFromServerByToken(token); + return diagram; + } catch (error) { + return rejectWithValue('Failed to fetch diagram'); + } + }, +); + +// Define the slice using Redux Toolkit +const diagramSlice = createSlice({ + name: 'diagram', + initialState, + reducers: { + updateDiagram: (state, action: PayloadAction>) => { + if (state.diagram) { + state.diagram = { ...state.diagram, ...action.payload }; + } + }, + createDiagram: (state, action: PayloadAction) => { + state.diagram!.id = action.payload; + }, + }, + extraReducers: (builder) => { + builder + // // Create Diagram + // .addCase(createDiagram.pending, (state) => { + // state.loading = true; + // state.error = null; + // }) + // .addCase(createDiagram.fulfilled, (state, action: PayloadAction) => { + // state.loading = false; + // state.diagram = action.payload; + // }) + // .addCase(createDiagram.rejected, (state, action) => { + // state.loading = false; + // state.error = action.error.message || 'Failed to create diagram'; + // }) + + // // Update Diagram + // .addCase(updateDiagram.pending, (state) => { + // state.loading = true; + // state.error = null; + // }) + // .addCase(updateDiagram.fulfilled, (state, action: PayloadAction>) => { + // state.loading = false; + // if (state.diagram) { + // state.diagram = { ...state.diagram, ...action.payload, lastUpdate: moment() }; + // } + // }) + // .addCase(updateDiagram.rejected, (state, action) => { + // state.loading = false; + // state.error = action.error.message || 'Failed to update diagram'; + // }) + + // Fetch Diagram + .addCase(fetchDiagramByToken.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(fetchDiagramByToken.fulfilled, (state, action: PayloadAction) => { + state.loading = false; + state.diagram = action.payload; + }) + .addCase(fetchDiagramByToken.rejected, (state, action) => { + state.loading = false; + state.error = action.payload as string; + }); + }, + selectors: { + selectDiagram: (state: DiagramState) => state.diagram, + }, +}); + +export const { updateDiagram } = diagramSlice.actions; +export const { selectDiagram } = diagramSlice.selectors; + +export const diagramReducer = diagramSlice.reducer; diff --git a/packages/webapp/src/main/services/editor-options/editorOptionSlice.ts b/packages/webapp/src/main/services/editor-options/editorOptionSlice.ts new file mode 100644 index 0000000..279aa3a --- /dev/null +++ b/packages/webapp/src/main/services/editor-options/editorOptionSlice.ts @@ -0,0 +1,54 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { UMLDiagramType } from '@ls1intum/apollon'; +import { ApollonMode, Locale } from '@ls1intum/apollon/lib/es6/services/editor/editor-types'; +import { Styles } from '@ls1intum/apollon/lib/es6/components/theme/styles'; +import { DeepPartial } from 'redux'; + +// Define the editor options type +export type EditorOptions = { + type: UMLDiagramType; + mode?: ApollonMode; + readonly?: boolean; + enablePopups?: boolean; + enableCopyPaste?: boolean; + theme?: DeepPartial; + locale: Locale; + colorEnabled?: boolean; +}; + +// Define the initial/default state +export const defaultEditorOptions: EditorOptions = { + type: UMLDiagramType.ClassDiagram, + mode: ApollonMode.Modelling, + readonly: false, + enablePopups: true, + enableCopyPaste: true, + locale: Locale.en, + colorEnabled: true, +}; + +// Create the slice +const editorOptionsSlice = createSlice({ + name: 'editorOptions', + initialState: defaultEditorOptions, + reducers: { + // Action to change the diagram type + changeDiagramType(state, action: PayloadAction< UMLDiagramType >) { + state.type = action.payload; + }, + // Action to change the editor mode + changeEditorMode(state, action: PayloadAction< ApollonMode >) { + state.mode = action.payload; + }, + // Action to change readonly mode + changeReadonlyMode(state, action: PayloadAction) { + state.readonly = action.payload; + }, + }, +}); + +// Export actions +export const { changeDiagramType, changeEditorMode, changeReadonlyMode } = editorOptionsSlice.actions; + +// Export the reducer to be used in the store configuration +export const editorOptionsReducer = editorOptionsSlice.reducer; diff --git a/packages/webapp/src/main/services/error-management/errorManagementSlice.ts b/packages/webapp/src/main/services/error-management/errorManagementSlice.ts new file mode 100644 index 0000000..fc02c5c --- /dev/null +++ b/packages/webapp/src/main/services/error-management/errorManagementSlice.ts @@ -0,0 +1,43 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { v4 as uuidv4 } from 'uuid'; // Assuming you're using uuid to generate unique error IDs + +// Define the error type +export type ApollonError = { + id: string; + headerText: string; + bodyText: string; +}; + +// Define the initial state as an array of errors +const initialState: ApollonError[] = []; + +// Create the error slice using Redux Toolkit's createSlice +const errorSlice = createSlice({ + name: 'error', + initialState, + reducers: { + // Action to display an error + displayError: { + reducer: (state, action: PayloadAction) => { + state.push(action.payload); + }, + prepare: (headerText: string, bodyText: string) => ({ + payload: { + id: uuidv4(), // Generate a unique ID for each error + headerText, + bodyText, + }, + }), + }, + // Action to dismiss an error by its ID + dismissError: (state, action: PayloadAction<{ id: string }>) => { + return state.filter((error) => error.id !== action.payload.id); + }, + }, +}); + +// Export actions to be used in components or thunks +export const { displayError, dismissError } = errorSlice.actions; + +// Export the reducer to be used in the Redux store +export const errorReducer = errorSlice.reducer; diff --git a/packages/webapp/src/main/services/export/useExportJson.ts b/packages/webapp/src/main/services/export/useExportJson.ts new file mode 100644 index 0000000..d40d9c8 --- /dev/null +++ b/packages/webapp/src/main/services/export/useExportJson.ts @@ -0,0 +1,28 @@ +import { useCallback } from 'react'; +import { ApollonEditor } from '@ls1intum/apollon'; +import { useFileDownload } from '../file-download/useFileDownload'; +import { Diagram } from '../diagram/diagramSlice'; + + +export const useExportJson = () => { + const downloadFile = useFileDownload(); + + const exportJson = useCallback((editor: ApollonEditor, diagram: Diagram) => { + // Prepare the file name + const fileName = `${diagram.title}.json`; + + // Extract the model from the ApollonEditor and merge it with the diagram data + const diagramData: Diagram = { ...diagram, model: editor.model }; + + // Convert the diagram data to a JSON string + const jsonContent = JSON.stringify(diagramData); + + // Create a Blob for the JSON content + const fileToDownload = new File([jsonContent], fileName, { type: 'application/json' }); + + // Trigger the file download using the useFileDownload hook + downloadFile({ file: fileToDownload, filename: fileName }); + }, [downloadFile]); + + return exportJson; +}; diff --git a/packages/webapp/src/main/services/export/useExportPdf.ts b/packages/webapp/src/main/services/export/useExportPdf.ts new file mode 100644 index 0000000..03c672a --- /dev/null +++ b/packages/webapp/src/main/services/export/useExportPdf.ts @@ -0,0 +1,36 @@ +import { useCallback } from 'react'; +import { ApollonEditor } from '@ls1intum/apollon'; +import { useFileDownload } from '../file-download/useFileDownload'; +import { DiagramRepository } from '../diagram/diagram-repository'; + + +export const useExportPdf = () => { + const downloadFile = useFileDownload(); + + const exportPdf = useCallback( + async (editor: ApollonEditor, diagramTitle: string) => { + // Step 1: Export the diagram as SVG from the ApollonEditor + const apollonSVG = await editor.exportAsSVG(); + + // Step 2: Extract the dimensions from the exported SVG + const { width, height } = apollonSVG.clip; + + // Step 3: Convert the SVG to a PDF using the DiagramRepository method + const pdfBlob = await DiagramRepository.convertSvgToPdf(apollonSVG.svg, width, height); + + if (pdfBlob) { + // Step 4: Create a Blob and trigger file download + const filename = `${diagramTitle}.pdf`; + const fileToDownload = new Blob([pdfBlob], { type: 'application/pdf' }); + + // Trigger the download using the useFileDownload hook + downloadFile({ file: fileToDownload, filename }); + } else { + console.error('Failed to convert SVG to PDF'); + } + }, + [downloadFile] + ); + + return exportPdf; +}; diff --git a/packages/webapp/src/main/services/export/useExportPng.ts b/packages/webapp/src/main/services/export/useExportPng.ts new file mode 100644 index 0000000..d6a2b37 --- /dev/null +++ b/packages/webapp/src/main/services/export/useExportPng.ts @@ -0,0 +1,70 @@ +import { useCallback } from 'react'; +import { ApollonEditor, SVG } from '@ls1intum/apollon'; +import { useFileDownload } from '../file-download/useFileDownload'; + +export const useExportPng = () => { + const downloadFile = useFileDownload(); + + const exportPng = useCallback( + async (editor: ApollonEditor, diagramTitle: string, setWhiteBackground: boolean) => { + // Step 1: Export the diagram as SVG from the ApollonEditor + const apollonSVG: SVG = await editor.exportAsSVG(); + + // Step 2: Convert the exported SVG to a PNG + const pngBlob: Blob = await convertRenderedSVGToPNG(apollonSVG, setWhiteBackground); + + // Step 3: Create a file name for the PNG + const fileName = `${diagramTitle}.png`; + + // Step 4: Create a Blob for the PNG and trigger file download + const fileToDownload = new File([pngBlob], fileName, { type: 'image/png' }); + + // Trigger the download using the useFileDownload hook + downloadFile({ file: fileToDownload, filename: fileName }); + }, + [downloadFile] + ); + + return exportPng; +}; + +// Helper function to convert SVG to PNG +function convertRenderedSVGToPNG(renderedSVG: SVG, whiteBackground: boolean): Promise { + return new Promise((resolve, reject) => { + const { width, height } = renderedSVG.clip; + + const blob = new Blob([renderedSVG.svg], { type: 'image/svg+xml' }); + const blobUrl = URL.createObjectURL(blob); + + const image = new Image(); + image.width = width; + image.height = height; + image.src = blobUrl; + + image.onload = () => { + const canvas = document.createElement('canvas'); + const scale = 1.5; // Adjust scale if necessary + canvas.width = width * scale; + canvas.height = height * scale; + + const context = canvas.getContext('2d')!; + + if (whiteBackground) { + context.fillStyle = 'white'; + context.fillRect(0, 0, canvas.width, canvas.height); + } + + context.scale(scale, scale); + context.drawImage(image, 0, 0); + + canvas.toBlob((blob) => { + URL.revokeObjectURL(blobUrl); // Cleanup the blob URL + resolve(blob as Blob); + }); + }; + + image.onerror = (error) => { + reject(error); + }; + }); +} diff --git a/packages/webapp/src/main/services/export/useExportSvg.ts b/packages/webapp/src/main/services/export/useExportSvg.ts new file mode 100644 index 0000000..b79f083 --- /dev/null +++ b/packages/webapp/src/main/services/export/useExportSvg.ts @@ -0,0 +1,27 @@ +import { useCallback } from 'react'; +import { ApollonEditor, SVG } from '@ls1intum/apollon'; +import { useFileDownload } from '../file-download/useFileDownload'; + + +export const useExportSvg = () => { + const downloadFile = useFileDownload(); + + const exportSvg = useCallback( + async (editor: ApollonEditor, diagramTitle: string) => { + // Step 1: Export the diagram as SVG from the ApollonEditor + const apollonSVG: SVG = await editor.exportAsSVG(); + + // Step 2: Create a file name for the SVG + const fileName = `${diagramTitle}.svg`; + + // Step 3: Create a Blob for the SVG and trigger file download + const fileToDownload = new File([apollonSVG.svg], fileName, { type: 'image/svg+xml' }); + + // Trigger the download using the useFileDownload hook + downloadFile({ file: fileToDownload, filename: fileName }); + }, + [downloadFile] + ); + + return exportSvg; +}; diff --git a/packages/webapp/src/main/services/file-download/useFileDownload.ts b/packages/webapp/src/main/services/file-download/useFileDownload.ts new file mode 100644 index 0000000..ace98a2 --- /dev/null +++ b/packages/webapp/src/main/services/file-download/useFileDownload.ts @@ -0,0 +1,32 @@ +import { useCallback } from 'react'; + +interface FileDownloadPayload { + file: File | Blob; + filename?: string; +} + +export const useFileDownload = () => { + const downloadFile = useCallback(({ file, filename }: FileDownloadPayload) => { + const link = document.createElement('a'); + link.href = window.URL.createObjectURL(file); + + // Set the file name for download + if (filename) { + link.download = filename; + } else if (file instanceof File) { + link.download = file.name; + } else { + link.download = 'file'; + } + + // Append the link to the body, trigger the download, and remove the link + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // Cleanup URL object + window.URL.revokeObjectURL(link.href); + }, []); + + return downloadFile; +}; diff --git a/packages/webapp/src/main/services/import/useImport.ts b/packages/webapp/src/main/services/import/useImport.ts new file mode 100644 index 0000000..23b0134 --- /dev/null +++ b/packages/webapp/src/main/services/import/useImport.ts @@ -0,0 +1,21 @@ +import { useCallback } from 'react'; +import { useAppDispatch } from '../../components/store/hooks'; +import { uuid } from '../../utils/uuid'; +import { Diagram, updateDiagram } from '../diagram/diagramSlice'; +import { changeDiagramType } from '../editor-options/editorOptionSlice'; + +export const useImport = () => { + const dispatch = useAppDispatch(); + + const importDiagram = useCallback((stringifiedJson: string) => { + const diagram: Diagram = JSON.parse(stringifiedJson); + diagram.id = uuid(); + + dispatch(updateDiagram(diagram)); + if (diagram.model) { + dispatch(changeDiagramType(diagram.model.type)); + } + }, []); + + return importDiagram; +}; diff --git a/packages/webapp/src/main/services/local-storage/useLocalStorage.ts b/packages/webapp/src/main/services/local-storage/useLocalStorage.ts new file mode 100644 index 0000000..4a05e53 --- /dev/null +++ b/packages/webapp/src/main/services/local-storage/useLocalStorage.ts @@ -0,0 +1,50 @@ +import { useCallback } from 'react'; +import moment from 'moment'; +import { localStorageDiagramPrefix, localStorageDiagramsList, localStorageLatest } from '../../constant'; +import { Diagram } from '../diagram/diagramSlice'; + +// Custom hook to handle local storage operations +export const useLocalStorage = () => { + // Store diagram in local storage after update + const storeDiagram = useCallback((diagram: Diagram) => { + // Save diagram and update the latest diagram entry in local storage + localStorage.setItem(localStorageDiagramPrefix + diagram.id, JSON.stringify(diagram)); + localStorage.setItem(localStorageLatest, diagram.id); + + // Create a new entry for the local storage diagram list + const localDiagramEntry = { + id: diagram.id, + title: diagram.title, + type: diagram.model?.type ?? 'UMLClassDiagram', + lastUpdate: moment(), + }; + + // Get the list of diagrams from local storage + const localStorageListJson = localStorage.getItem(localStorageDiagramsList); + let localDiagrams = localStorageListJson ? JSON.parse(localStorageListJson) : []; + + // Filter out the old value and add the new one + localDiagrams = localDiagrams.filter((entry: any) => entry.id !== diagram.id); + localDiagrams.push(localDiagramEntry); + + // Save the updated diagram list in local storage + localStorage.setItem(localStorageDiagramsList, JSON.stringify(localDiagrams)); + }, []); + + // Load a diagram from local storage + const loadDiagram = useCallback((id: string): Diagram | null => { + const localStorageContent = localStorage.getItem(localStorageDiagramPrefix + id); + if (localStorageContent) { + const diagram: Diagram = JSON.parse(localStorageContent); + return diagram; + } else { + console.error(`The diagram with id ${id} could not be found in local storage.`); + return null; + } + }, []); + + return { + storeDiagram, + loadDiagram, + }; +}; diff --git a/packages/webapp/src/main/services/modal/modalSlice.ts b/packages/webapp/src/main/services/modal/modalSlice.ts new file mode 100644 index 0000000..0aa577b --- /dev/null +++ b/packages/webapp/src/main/services/modal/modalSlice.ts @@ -0,0 +1,36 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { ModalContentType, ModalSize } from '../../components/modals/application-modal-types'; + +export type ModalState = { + type: ModalContentType | null; + size: ModalSize; +}; + +// Define the initial state for the modal +const initialState: ModalState = { + type: null, + size: 'sm', // Default size to 'sm' +}; + +const modalSlice = createSlice({ + name: 'modal', + initialState, + reducers: { + // Action to show the modal + showModal: (state, action: PayloadAction<{ type: ModalContentType; size?: ModalSize }>) => { + state.type = action.payload.type; + state.size = action.payload.size ?? 'sm'; // Default size is 'sm' if not provided + }, + // Action to hide the modal + hideModal: (state) => { + state.type = null; + state.size = 'sm'; // Reset size to 'sm' when modal is hidden + }, + }, +}); + +// Export the actions to be used in components +export const { showModal, hideModal } = modalSlice.actions; + +// Export the reducer to be used in the store +export const modalReducer = modalSlice.reducer; diff --git a/packages/webapp/src/main/services/reducer.ts b/packages/webapp/src/main/services/reducer.ts index a2da34a..5dbccce 100644 --- a/packages/webapp/src/main/services/reducer.ts +++ b/packages/webapp/src/main/services/reducer.ts @@ -2,13 +2,13 @@ import { Actions } from './actions'; import { ApplicationState } from '../components/store/application-state'; import { ReducersMapObject } from 'redux'; import { EditorOptionsReducer } from './editor-options/editor-options-reducer'; -import { DiagramReducer } from './diagram/diagram-reducer'; import { ErrorReducer } from './error-management/error-reducer'; import { ModalReducer } from './modal/modal-reducer'; import { ShareReducer } from './share/share-reducer'; +import { diagramReducer } from './diagram/diagramSlice'; -export const reducers: ReducersMapObject = { - diagram: DiagramReducer, +export const reducers = { + diagram: diagramReducer, editorOptions: EditorOptionsReducer, errors: ErrorReducer, modal: ModalReducer, diff --git a/packages/webapp/src/main/services/share/shareSlice.ts b/packages/webapp/src/main/services/share/shareSlice.ts new file mode 100644 index 0000000..3bcdda4 --- /dev/null +++ b/packages/webapp/src/main/services/share/shareSlice.ts @@ -0,0 +1,53 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { Collaborator } from 'shared/src/main/collaborator-dto'; + +// Define the ShareState type +export type ShareState = { + collaborationName: string; + collaborationColor: string; + collaborators: Collaborator[]; + fromServer: boolean; +}; + +// Define the initial state +const initialState: ShareState = { + collaborationName: '', + collaborationColor: '', + collaborators: [], + fromServer: false, +}; + +// Create the share slice +const shareSlice = createSlice({ + name: 'share', + initialState, + reducers: { + // Action to update the collaboration name + updateCollaborationName(state, action: PayloadAction<{ name: string }>) { + state.collaborationName = action.payload.name; + }, + // Action to update the collaboration color + updateCollaborationColor(state, action: PayloadAction<{ color: string }>) { + state.collaborationColor = action.payload.color; + }, + // Action to update collaborators + updateCollaborators(state, action: PayloadAction<{ collaborators: Collaborator[] }>) { + state.collaborators = action.payload.collaborators; + }, + // Action to set whether the data is from the server + gotFromServer(state, action: PayloadAction<{ fromServer: boolean }>) { + state.fromServer = action.payload.fromServer; + }, + }, +}); + +// Export actions to be used in components or thunks +export const { + updateCollaborationName, + updateCollaborationColor, + updateCollaborators, + gotFromServer, +} = shareSlice.actions; + +// Export the reducer to be used in the store +export const shareReducer = shareSlice.reducer; From c75e7ec0ee533acf19855473146cbf6d514afd33 Mon Sep 17 00:00:00 2001 From: egenerse Date: Tue, 1 Oct 2024 09:57:00 +0200 Subject: [PATCH 02/17] feature: refactor whole the frontend --- packages/webapp/src/main/application.tsx | 17 +- .../apollon-editor-component.tsx | 174 ++++++---------- .../apollon-editor-context.ts | 8 +- .../with-apollon-editor.tsx | 11 -- .../application-bar/application-bar.tsx | 165 ++++++---------- .../application-bar/menues/file-menu.tsx | 180 ++++++----------- .../application-bar/menues/help-menu.tsx | 88 ++------- .../menues/theme-switcher-menu.tsx | 186 +++++++----------- .../application-bar/menues/view-menu.tsx | 70 ++----- .../error-handling/error-message.tsx | 2 +- .../components/error-handling/error-panel.tsx | 30 +-- .../help-modeling-modal.tsx | 159 ++++++++------- .../load-diagram-modal/load-diagram-item.tsx | 31 +-- .../load-diagram-modal/load-diagram-modal.tsx | 98 +++------ .../components/store/application-state.ts | 13 -- .../components/store/application-store.tsx | 25 ++- packages/webapp/src/main/hocs/withRouter.tsx | 19 -- .../main/services/diagram/diagram-epics.ts | 56 ------ .../services/diagram/diagram-repository.ts | 17 +- .../main/services/diagram/diagram-types.ts | 24 --- .../src/main/services/diagram/diagramSlice.ts | 79 ++------ .../editor-options/editor-options-reducer.ts | 35 ---- .../editor-options-repository.ts | 30 --- .../editor-options/editor-options-types.ts | 41 ---- .../editor-options/editorOptionSlice.ts | 14 +- .../error-management/error-reducer.ts | 22 --- .../error-management/error-repository.ts | 19 -- .../services/error-management/error-types.ts | 17 -- .../error-management/errorManagementSlice.ts | 24 +-- .../src/main/services/export/export-epics.ts | 10 - .../main/services/export/export-repository.ts | 35 ---- .../src/main/services/export/export-types.ts | 43 ---- .../services/export/json/export-json-epics.ts | 31 --- .../services/export/pdf/export-pdf-epics.ts | 41 ---- .../services/export/png/export-png-epics.ts | 72 ------- .../services/export/svg/export-svg-epics.ts | 29 --- .../src/main/services/export/useExportJson.ts | 14 +- .../src/main/services/export/useExportPdf.ts | 15 +- .../src/main/services/export/useExportPng.ts | 16 +- .../src/main/services/export/useExportSvg.ts | 11 +- .../file-download/file-download-epics.ts | 35 ---- .../file-download/file-download-repository.ts | 10 - .../file-download/file-download-types.ts | 12 -- .../src/main/services/import/import-epic.ts | 47 ----- .../main/services/import/import-repository.ts | 10 - .../src/main/services/import/import-types.ts | 13 -- .../src/main/services/import/useImport.ts | 21 -- .../main/services/import/useImportDiagram.ts | 29 +++ .../local-storage/local-storage-epics.ts | 91 --------- .../local-storage/local-storage-repository.ts | 46 +++-- .../local-storage/local-storage-types.ts | 24 +-- .../services/local-storage/useLocalStorage.ts | 38 +--- .../src/main/services/modal/modalSlice.ts | 12 +- .../src/main/services/share/shareSlice.ts | 32 ++- 54 files changed, 582 insertions(+), 1809 deletions(-) delete mode 100644 packages/webapp/src/main/components/apollon-editor-component/with-apollon-editor.tsx delete mode 100644 packages/webapp/src/main/components/store/application-state.ts delete mode 100644 packages/webapp/src/main/hocs/withRouter.tsx delete mode 100644 packages/webapp/src/main/services/diagram/diagram-epics.ts delete mode 100644 packages/webapp/src/main/services/diagram/diagram-types.ts delete mode 100644 packages/webapp/src/main/services/editor-options/editor-options-reducer.ts delete mode 100644 packages/webapp/src/main/services/editor-options/editor-options-repository.ts delete mode 100644 packages/webapp/src/main/services/editor-options/editor-options-types.ts delete mode 100644 packages/webapp/src/main/services/error-management/error-reducer.ts delete mode 100644 packages/webapp/src/main/services/error-management/error-repository.ts delete mode 100644 packages/webapp/src/main/services/error-management/error-types.ts delete mode 100644 packages/webapp/src/main/services/export/export-epics.ts delete mode 100644 packages/webapp/src/main/services/export/export-repository.ts delete mode 100644 packages/webapp/src/main/services/export/export-types.ts delete mode 100644 packages/webapp/src/main/services/export/json/export-json-epics.ts delete mode 100644 packages/webapp/src/main/services/export/pdf/export-pdf-epics.ts delete mode 100644 packages/webapp/src/main/services/export/png/export-png-epics.ts delete mode 100644 packages/webapp/src/main/services/export/svg/export-svg-epics.ts delete mode 100644 packages/webapp/src/main/services/file-download/file-download-epics.ts delete mode 100644 packages/webapp/src/main/services/file-download/file-download-repository.ts delete mode 100644 packages/webapp/src/main/services/file-download/file-download-types.ts delete mode 100644 packages/webapp/src/main/services/import/import-epic.ts delete mode 100644 packages/webapp/src/main/services/import/import-repository.ts delete mode 100644 packages/webapp/src/main/services/import/import-types.ts delete mode 100644 packages/webapp/src/main/services/import/useImport.ts create mode 100644 packages/webapp/src/main/services/import/useImportDiagram.ts delete mode 100644 packages/webapp/src/main/services/local-storage/local-storage-epics.ts diff --git a/packages/webapp/src/main/application.tsx b/packages/webapp/src/main/application.tsx index 83f2856..c2f878c 100644 --- a/packages/webapp/src/main/application.tsx +++ b/packages/webapp/src/main/application.tsx @@ -1,14 +1,14 @@ import React, { useState } from 'react'; import { ApplicationBar } from './components/application-bar/application-bar'; -import { ApollonEditorWrapper } from './components/apollon-editor-component/apollon-editor-component'; +import { ApollonEditorComponent } from './components/apollon-editor-component/apollon-editor-component'; import { ApollonEditor } from '@ls1intum/apollon'; import { POSTHOG_HOST, POSTHOG_KEY, } from './constant'; import { - ApollonEditorContext, ApollonEditorProvider, + } from './components/apollon-editor-component/apollon-editor-context'; import { FirefoxIncompatibilityHint } from './components/incompatability-hints/firefox-incompatibility-hint'; import { ErrorPanel } from './components/error-handling/error-panel'; @@ -25,23 +25,20 @@ const postHogOptions = { export const Application = () => { const [editor, setEditor] = useState(); - const handleSetEditor = (ref: ApollonEditor) => { - if (ref) { - setEditor(ref); - } + const handleSetEditor = (newEditor: ApollonEditor) => { + setEditor(newEditor); }; - const isFirefox: boolean = /Firefox/i.test(navigator.userAgent); - const context: ApollonEditorContext | null = { editor, setEditor: handleSetEditor }; + const isFirefox = /Firefox/i.test(navigator.userAgent); return ( - + {isFirefox && } - + diff --git a/packages/webapp/src/main/components/apollon-editor-component/apollon-editor-component.tsx b/packages/webapp/src/main/components/apollon-editor-component/apollon-editor-component.tsx index 5124f0e..e187385 100644 --- a/packages/webapp/src/main/components/apollon-editor-component/apollon-editor-component.tsx +++ b/packages/webapp/src/main/components/apollon-editor-component/apollon-editor-component.tsx @@ -1,26 +1,25 @@ -import { ApollonEditor, ApollonMode, ApollonOptions, Patch, Selection, UMLModel } from '@ls1intum/apollon'; -import React, { useEffect, useRef, useState, useMemo } from 'react'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; +import { ApollonEditor, ApollonMode, Patch, Selection, UMLModel } from '@ls1intum/apollon'; +import React, { useEffect, useRef, useState, useMemo, useContext } from 'react'; import styled from 'styled-components'; import { DiagramView } from 'shared/src/main/diagram-view'; import { w3cwebsocket as W3CWebSocket } from 'websocket'; import { APPLICATION_SERVER_VERSION, DEPLOYMENT_URL, NO_HTTP_URL, WS_PROTOCOL } from '../../constant'; import { DiagramRepository } from '../../services/diagram/diagram-repository'; -import { EditorOptionsRepository } from '../../services/editor-options/editor-options-repository'; -import { ImportRepository } from '../../services/import/import-repository'; -import { ModalRepository } from '../../services/modal/modal-repository'; -import { ShareRepository } from '../../services/share/share-repository'; import { uuid } from '../../utils/uuid'; import { ModalContentType } from '../modals/application-modal-types'; -import { ApplicationState } from '../store/application-state'; import { toast } from 'react-toastify'; import { selectionDiff } from '../../utils/selection-diff'; import { CollaborationMessage } from '../../utils/collaboration-message-type'; -import { withRouter } from '../../hocs/withRouter'; -import { withApollonEditor } from './with-apollon-editor'; -import { Diagram, DiagramState } from '../../services/diagram/diagramSlice'; + +import { updateDiagramThunk } from '../../services/diagram/diagramSlice'; +import { useLocation, useParams } from 'react-router-dom'; +import { ApollonEditorContext } from './apollon-editor-context'; +import { useAppDispatch, useAppSelector } from '../store/hooks'; +import { updateCollaborators } from '../../services/share/shareSlice'; +import { useImportDiagram } from '../../services/import/useImportDiagram'; +import { changeEditorMode, changeReadonlyMode } from '../../services/editor-options/editorOptionSlice'; +import { showModal } from '../../services/modal/modalSlice'; const ApollonContainer = styled.div` display: flex; @@ -29,37 +28,26 @@ const ApollonContainer = styled.div` overflow: hidden; `; -type OwnProps = {}; - -type StateProps = { - diagram: DiagramState; - options: ApollonOptions; - fromServer: boolean; - collaborationName: string; - collaborationColor: string; -}; - -type DispatchProps = { - updateDiagram: typeof DiagramRepository.updateDiagram; - importDiagram: typeof ImportRepository.importJSON; - changeEditorMode: typeof EditorOptionsRepository.changeEditorMode; - changeReadonlyMode: typeof EditorOptionsRepository.changeReadonlyMode; - updateCollaborators: typeof ShareRepository.updateCollaborators; - openModal: typeof ModalRepository.showModal; -}; - -type Props = OwnProps & StateProps & DispatchProps & { params: any; location: any; setEditor: any; editor: any }; - -const ApollonEditorComponent: React.FC = (props) => { +export const ApollonEditorComponent: React.FC= () => { const containerRef = useRef(null); const editorRef = useRef(null); - const clientRef = useRef(null); // Ref to keep client stable + const clientRef = useRef(null); const [selection, setSelection] = useState({ elements: {}, relationships: {} }); + const location = useLocation(); + const params = useParams(); + const editorContext = useContext(ApollonEditorContext); + const { collaborationName, collaborationColor } = useAppSelector((state) => state.share); + const dispatch = useAppDispatch(); + const importDiagram = useImportDiagram(); + const {diagram: reduxDiagram} = useAppSelector((state)=>state.diagram) + const options = useAppSelector((state)=>state.editorOptions) + + const editor = editorContext?.editor; + const setEditor = editorContext?.setEditor; - // Memoize props.options to avoid unnecessary re-renders const memoizedOptions = useMemo( - () => props.options, - [props.options.type, props.options.mode, props.options.readonly], + () => options, + [options.type, options.mode, options.readonly], ); // Establish collaboration connection @@ -78,17 +66,17 @@ const ApollonEditorComponent: React.FC = (props) => { message.data, ) as CollaborationMessage; if (collaborators) { - props.updateCollaborators(collaborators); - props.editor?.pruneRemoteSelectors(collaborators); + dispatch(updateCollaborators(collaborators)); + editor?.pruneRemoteSelectors(collaborators); } if (diagram) { - props.importDiagram(JSON.stringify(diagram)); + importDiagram(JSON.stringify(diagram)); } if (patch) { - props.editor?.importPatch(patch); + editor?.importPatch(patch); } if (selection && originator) { - props.editor?.remoteSelect(originator.name, originator.color, selection.selected, selection.deselected); + editor?.remoteSelect(originator.name, originator.color, selection.selected, selection.deselected); } }; } @@ -97,13 +85,12 @@ const ApollonEditorComponent: React.FC = (props) => { // Initialize the editor once, when the container is available useEffect(() => { console.log('DEBUGF wwowoow'); - if (containerRef.current && !editorRef.current) { - editorRef.current = new ApollonEditor(containerRef.current, memoizedOptions); + if (containerRef.current && !editorRef.current && setEditor) { + editorRef.current = new ApollonEditor(containerRef.current, memoizedOptions); editorRef.current.subscribeToAllModelChangePatches((patch: Patch) => { if (clientRef.current) { - const { token } = props.params; - const { collaborationName, collaborationColor } = props; + const { token } = params; clientRef.current.send( JSON.stringify({ token, @@ -115,17 +102,16 @@ const ApollonEditorComponent: React.FC = (props) => { }); editorRef.current.subscribeToModelChange((model: UMLModel) => { - const diagram: Diagram = { ...props.diagram.diagram, model } as Diagram; - props.updateDiagram(diagram); + const diagram = { ...reduxDiagram, model } ; + dispatch(updateDiagramThunk(diagram)); }); - editorRef.current.subscribeToSelectionChange((newSelection: Selection) => { + editorRef.current.subscribeToSelectionChange((newSelection) => { const diff = selectionDiff(selection, newSelection); setSelection(newSelection); if (clientRef.current && (diff.selected.length > 0 || diff.deselected.length > 0)) { - const { collaborationName, collaborationColor } = props; - const { token } = props.params; + const { token } = params; clientRef.current.send( JSON.stringify({ @@ -137,40 +123,40 @@ const ApollonEditorComponent: React.FC = (props) => { } }); - props.setEditor(editorRef.current); + setEditor(editorRef.current); } - }, [containerRef, memoizedOptions, props]); + }, [containerRef, memoizedOptions, setEditor]); useEffect(() => { console.log('DEBUGF wwowoow'); if (APPLICATION_SERVER_VERSION && DEPLOYMENT_URL) { - const { token } = props.params; + const { token } = params; if (token) { - const query = new URLSearchParams(props.location.search); + const query = new URLSearchParams(location.search); const view: DiagramView | null = query.get('view') as DiagramView; const notifyUser: string | null = query.get('notifyUser'); if (view) { switch (view) { case DiagramView.SEE_FEEDBACK: - props.changeEditorMode(ApollonMode.Assessment); - props.changeReadonlyMode(true); + dispatch(changeEditorMode(ApollonMode.Assessment)); + dispatch(changeReadonlyMode(true)); break; case DiagramView.GIVE_FEEDBACK: - props.changeEditorMode(ApollonMode.Assessment); - props.changeReadonlyMode(false); + dispatch(changeEditorMode(ApollonMode.Assessment)); + dispatch(changeReadonlyMode(false)); break; case DiagramView.EDIT: - props.changeEditorMode(ApollonMode.Modelling); - props.changeReadonlyMode(false); + dispatch(changeEditorMode(ApollonMode.Modelling)); + dispatch(changeReadonlyMode(false)); break; case DiagramView.COLLABORATE: - props.changeEditorMode(ApollonMode.Modelling); - props.changeReadonlyMode(false); - if (!props.collaborationName || !props.collaborationColor) { - props.openModal(ModalContentType.CollaborationModal, 'lg'); + dispatch(changeEditorMode(ApollonMode.Modelling)); + dispatch(changeReadonlyMode(false)); + if (!collaborationName ||collaborationColor) { + dispatch(showModal({type:ModalContentType.CollaborationModal, size:'lg'})); } - establishCollaborationConnection(token, props.collaborationName, props.collaborationColor); + establishCollaborationConnection(token, collaborationName, collaborationColor); if (notifyUser === 'true') { displayToast(); window.history.replaceState({}, document.title, window.location.pathname + '?view=' + view); @@ -182,23 +168,23 @@ const ApollonEditorComponent: React.FC = (props) => { if (view !== DiagramView.COLLABORATE) { DiagramRepository.getDiagramFromServerByToken(token).then((diagram) => { if (diagram) { - props.importDiagram(JSON.stringify(diagram)); + importDiagram(JSON.stringify(diagram)); - const queryParam = new URLSearchParams(props.location.search); + const queryParam = new URLSearchParams(location.search); const diagramView: DiagramView | null = queryParam.get('view') as DiagramView; if (diagramView) { switch (diagramView) { case DiagramView.SEE_FEEDBACK: - props.changeEditorMode(ApollonMode.Assessment); - props.changeReadonlyMode(true); + dispatch(changeEditorMode(ApollonMode.Assessment)); + dispatch(changeReadonlyMode(true)); break; case DiagramView.GIVE_FEEDBACK: - props.changeEditorMode(ApollonMode.Assessment); - props.changeReadonlyMode(false); + dispatch(changeEditorMode(ApollonMode.Assessment)); + dispatch(changeReadonlyMode(false)); break; case DiagramView.EDIT: - props.changeEditorMode(ApollonMode.Modelling); - props.changeReadonlyMode(false); + dispatch(changeEditorMode(ApollonMode.Modelling)); + dispatch(changeReadonlyMode(false)); break; } } @@ -207,7 +193,7 @@ const ApollonEditorComponent: React.FC = (props) => { } } } - }, [props.collaborationName, props.collaborationColor]); + }, [collaborationName, collaborationColor]); const displayToast = () => { toast.success( @@ -219,42 +205,10 @@ const ApollonEditorComponent: React.FC = (props) => { }; const key = useMemo(() => { - return (props.diagram?.diagram?.id || uuid()) + props.options.mode + props.options.type + props.options.readonly; - }, [props.diagram?.diagram?.id, props.options.mode, props.options.type, props.options.readonly]); + return (reduxDiagram?.id || uuid()) + options.mode + options.type + options.readonly; + }, [reduxDiagram?.id, options.mode, options.type, options.readonly]); return ; }; -const enhance = compose>( - withRouter, - withApollonEditor, - connect( - (state) => ({ - diagram: state.diagram, - options: { - type: state.editorOptions.type, - mode: state.editorOptions.mode, - readonly: state.editorOptions.readonly, - enablePopups: state.editorOptions.enablePopups, - copyPasteToClipboard: state.editorOptions.enableCopyPaste, - model: state.diagram?.diagram?.model, - theme: state.editorOptions.theme, - locale: state.editorOptions.locale, - colorEnabled: state.editorOptions.colorEnabled, - }, - fromServer: state.share.fromServer, - collaborationName: state.share.collaborationName, - collaborationColor: state.share.collaborationColor, - }), - { - updateDiagram: DiagramRepository.updateDiagram, - importDiagram: ImportRepository.importJSON, - changeEditorMode: EditorOptionsRepository.changeEditorMode, - changeReadonlyMode: EditorOptionsRepository.changeReadonlyMode, - updateCollaborators: ShareRepository.updateCollaborators, - openModal: ModalRepository.showModal, - }, - ), -); -export const ApollonEditorWrapper = enhance(ApollonEditorComponent); diff --git a/packages/webapp/src/main/components/apollon-editor-component/apollon-editor-context.ts b/packages/webapp/src/main/components/apollon-editor-component/apollon-editor-context.ts index 02a1f5c..4422e60 100644 --- a/packages/webapp/src/main/components/apollon-editor-component/apollon-editor-context.ts +++ b/packages/webapp/src/main/components/apollon-editor-component/apollon-editor-context.ts @@ -1,10 +1,12 @@ import { ApollonEditor } from '@ls1intum/apollon'; import { createContext } from 'react'; -export type ApollonEditorContext = { +export type ApollonEditorContextType = { editor?: ApollonEditor; setEditor: (editor: ApollonEditor) => void; }; -export const { Consumer: ApollonEditorConsumer, Provider: ApollonEditorProvider } = - createContext(null); +export const ApollonEditorContext = createContext(null); + + +export const { Consumer: ApollonEditorConsumer, Provider: ApollonEditorProvider } = ApollonEditorContext \ No newline at end of file diff --git a/packages/webapp/src/main/components/apollon-editor-component/with-apollon-editor.tsx b/packages/webapp/src/main/components/apollon-editor-component/with-apollon-editor.tsx deleted file mode 100644 index d73fc89..0000000 --- a/packages/webapp/src/main/components/apollon-editor-component/with-apollon-editor.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { ApollonEditorConsumer, ApollonEditorContext } from './apollon-editor-context'; -import React, { Component, ComponentType, forwardRef } from 'react'; - -export const withApollonEditor =

( - WrappedComponent: ComponentType

, -) => - forwardRef>>((props, ref) => ( - - {(context) => } - - )); diff --git a/packages/webapp/src/main/components/application-bar/application-bar.tsx b/packages/webapp/src/main/components/application-bar/application-bar.tsx index f8729ee..13df700 100644 --- a/packages/webapp/src/main/components/application-bar/application-bar.tsx +++ b/packages/webapp/src/main/components/application-bar/application-bar.tsx @@ -1,25 +1,16 @@ -import React, { ChangeEvent, Component, ComponentType } from 'react'; +import React, { ChangeEvent, useEffect, useState } from 'react'; import { Nav, Navbar } from 'react-bootstrap'; import { FileMenu } from './menues/file-menu'; import { HelpMenu } from './menues/help-menu'; import { ThemeSwitcherMenu } from './menues/theme-switcher-menu'; -import { connect, ConnectedComponent } from 'react-redux'; -import { ApplicationState } from '../store/application-state'; import styled from 'styled-components'; -import { DiagramRepository } from '../../services/diagram/diagram-repository'; import { appVersion } from '../../application-constants'; import { APPLICATION_SERVER_VERSION } from '../../constant'; -import { ModalRepository } from '../../services/modal/modal-repository'; import { ModalContentType } from '../modals/application-modal-types'; import { ConnectClientsComponent } from './connected-clients-component'; -import { Collaborator } from 'shared/src/main/collaborator-dto'; - -type OwnProps = {}; - -type StateProps = { - diagram: Diagram | null; - collaborators: Collaborator[]; -}; +import { useAppDispatch, useAppSelector } from '../store/hooks'; +import { updateDiagramThunk } from '../../services/diagram/diagramSlice'; +import { showModal } from '../../services/modal/modalSlice'; const DiagramTitle = styled.input` font-size: x-large; @@ -35,96 +26,70 @@ const ApplicationVersion = styled.span` margin-right: 10px; `; -type DispatchProps = { - updateDiagram: typeof DiagramRepository.updateDiagram; - openModal: typeof ModalRepository.showModal; -}; +export const ApplicationBar: React.FC = () => { + const dispatch = useAppDispatch(); + + + const { diagram } = useAppSelector((state) => state.diagram); + const collaborators = useAppSelector((state) => state.share.collaborators); + + + const [diagramTitle, setDiagramTitle] = useState(diagram?.title || ''); -type Props = OwnProps & StateProps & DispatchProps; - -const enhance = connect( - (state) => { - return { - diagram: state.diagram, - collaborators: state.share.collaborators, - }; - }, - { - updateDiagram: DiagramRepository.updateDiagram, - openModal: ModalRepository.showModal, - }, -); - -type State = { diagramTitle: string }; - -const getInitialState = (props: Props): State => { - return { - diagramTitle: props.diagram?.title ? props.diagram.title : '', - }; -}; -class ApplicationBarComponent extends Component { - state = getInitialState(this.props); - - constructor(props: Props) { - super(props); - this.changeDiagramTitlePreview = this.changeDiagramTitlePreview.bind(this); - this.changeDiagramTitleApplicationState = this.changeDiagramTitleApplicationState.bind(this); - } - - componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any) { - if (this.props.diagram && prevProps.diagram?.title !== this.props.diagram?.title) { - this.setState({ diagramTitle: this.props.diagram.title }); + useEffect(() => { + if (diagram?.title) { + setDiagramTitle(diagram.title); } - } + }, [diagram?.title]); - changeDiagramTitlePreview(event: ChangeEvent) { - // changes only diagram title of this component not in global state, this happens on blur - this.setState({ diagramTitle: event.target.value }); - } - changeDiagramTitleApplicationState(event: ChangeEvent) { - if (this.props.diagram) { - this.props.updateDiagram({ title: this.state.diagramTitle }); + const changeDiagramTitlePreview = (event: ChangeEvent) => { + setDiagramTitle(event.target.value); + }; + + + const changeDiagramTitleApplicationState = () => { + if (diagram) { + dispatch(updateDiagramThunk({ title: diagramTitle })); } - } - - render() { - return ( - <> - - - {' '} - Apollon - - {appVersion} - - -

- - - - - - ); - } -} - -export const ApplicationBar: ConnectedComponent, OwnProps> = enhance(ApplicationBarComponent); + }; + + + const handleOpenModal = () => { + dispatch(showModal({ type: ModalContentType.ShareModal, size: 'lg' })); + }; + + return ( + <> + + + {' '} + Apollon + + {appVersion} + + + + + + + + + ); +}; diff --git a/packages/webapp/src/main/components/application-bar/menues/file-menu.tsx b/packages/webapp/src/main/components/application-bar/menues/file-menu.tsx index 59da39a..e4789bd 100644 --- a/packages/webapp/src/main/components/application-bar/menues/file-menu.tsx +++ b/packages/webapp/src/main/components/application-bar/menues/file-menu.tsx @@ -1,140 +1,78 @@ -import React, { Component, ComponentClass } from 'react'; +import React, { useContext } from 'react'; import { Dropdown, NavDropdown } from 'react-bootstrap'; -import { connect } from 'react-redux'; -import { ApplicationState } from '../../store/application-state'; -import { compose } from 'redux'; -import { withApollonEditor } from '../../apollon-editor-component/with-apollon-editor'; import { ApollonEditorContext } from '../../apollon-editor-component/apollon-editor-context'; -import { ExportRepository } from '../../../services/export/export-repository'; -import { Diagram } from '../../../services/diagram/diagram-types'; -import { ModalRepository } from '../../../services/modal/modal-repository'; import { ModalContentType } from '../../modals/application-modal-types'; +import { useExportSVG } from '../../../services/export/useExportSVG'; +import { useAppDispatch, useAppSelector } from '../../store/hooks'; +import { useExportPNG } from '../../../services/export/useExportPNG'; +import { useExportPDF } from '../../../services/export/useExportPDF'; +import { showModal } from '../../../services/modal/modalSlice'; +import { useExportJSON } from '../../../services/export/useExportJSON'; -type Props = {}; +export const FileMenu: React.FC = () => { + const apollonEditor = useContext(ApollonEditorContext); + const dispatch = useAppDispatch(); -type State = { - show: boolean; -}; - -type StateProps = { - diagram: Diagram | null; -}; - -type DispatchProps = { - exportAsSVG: typeof ExportRepository.exportAsSVG; - exportAsPNG: typeof ExportRepository.exportAsPNG; - exportAsJSON: typeof ExportRepository.exportAsJSON; - exportAsPDF: typeof ExportRepository.exportAsPDF; - openModal: typeof ModalRepository.showModal; -}; - -const enhance = compose>( - withApollonEditor, - connect( - (state, props) => { - return { - diagram: state.diagram, - }; - }, - { - exportAsSVG: ExportRepository.exportAsSVG, - exportAsPNG: ExportRepository.exportAsPNG, - exportAsJSON: ExportRepository.exportAsJSON, - exportAsPDF: ExportRepository.exportAsPDF, - openModal: ModalRepository.showModal, - }, - ), -); - -type OwnProps = StateProps & DispatchProps & Props & ApollonEditorContext; - -class FileMenuComponent extends Component { - constructor(props: OwnProps) { - super(props); - this.exportDiagram = this.exportDiagram.bind(this); - } - - componentDidMount() { - document.addEventListener('click', this.hideMenu); - } + const editor = apollonEditor?.editor; + const diagram = useAppSelector((state) => state.diagram.diagram); + const exportAsSVG = useExportSVG(); + const exportAsPNG = useExportPNG(); + const exportAsPDF = useExportPDF(); + const exportAsJSON = useExportJSON(); - componentWillUnmount() { - document.removeEventListener('click', this.hideMenu); - } - - showMenu = (event: React.MouseEvent) => { - this.setState({ show: true }); - event.stopPropagation(); - }; - - hideMenu = (event: MouseEvent) => { - this.setState({ show: false }); - event.stopPropagation(); - }; - - exportDiagram(exportType: 'PNG' | 'PNG_WHITE' | 'SVG' | 'JSON' | 'PDF'): void { - if (this.props.editor && this.props.diagram?.title) { + const exportDiagram = (exportType: 'PNG' | 'PNG_WHITE' | 'SVG' | 'JSON' | 'PDF'): void => { + if (editor && diagram?.title) { switch (exportType) { case 'SVG': - this.props.exportAsSVG(this.props.editor, this.props.diagram?.title); + exportAsSVG(editor, diagram?.title); break; case 'PNG_WHITE': - this.props.exportAsPNG(this.props.editor, this.props.diagram?.title, true); + exportAsPNG(editor, diagram?.title, true); break; case 'PNG': - this.props.exportAsPNG(this.props.editor, this.props.diagram?.title, false); + exportAsPNG(editor, diagram?.title, false); break; case 'PDF': - this.props.exportAsPDF(this.props.editor, this.props.diagram?.title); + exportAsPDF(editor, diagram?.title); break; case 'JSON': - this.props.exportAsJSON(this.props.editor, this.props.diagram); + exportAsJSON(editor, diagram); } } - } - - render() { - return ( - <> - - this.props.openModal(ModalContentType.CreateDiagramModal)}> - New - - this.props.openModal(ModalContentType.CreateDiagramFromTemplateModal, 'lg')} - > - Start from Template - - this.props.openModal(ModalContentType.LoadDiagramModal)}> - Load - - this.props.openModal(ModalContentType.ImportDiagramModal)}> - Import - - - - Export - - - this.exportDiagram('SVG')}>As SVG - this.exportDiagram('PNG_WHITE')}> - As PNG (White Background) - - this.exportDiagram('PNG')}> - As PNG (Transparent Background) - - this.exportDiagram('JSON')}>As JSON - this.exportDiagram('PDF')}>As PDF - - - - - ); - } -} + }; -export const FileMenu = enhance(FileMenuComponent); + return ( + + dispatch(showModal({ type: ModalContentType.CreateDiagramModal }))}> + New + + dispatch(showModal({ type: ModalContentType.CreateDiagramFromTemplateModal, size: 'lg' }))} + > + Start from Template + + dispatch(showModal({ type: ModalContentType.LoadDiagramModal }))}> + Load + + dispatch(showModal({ type: ModalContentType.ImportDiagramModal }))}> + Import + + + + Export + + + exportDiagram('SVG')}>As SVG + exportDiagram('PNG_WHITE')}>As PNG (White Background) + exportDiagram('PNG')}>As PNG (Transparent Background) + exportDiagram('JSON')}>As JSON + exportDiagram('PDF')}>As PDF + + + + ); +}; diff --git a/packages/webapp/src/main/components/application-bar/menues/help-menu.tsx b/packages/webapp/src/main/components/application-bar/menues/help-menu.tsx index 0fa3ed8..67efb50 100644 --- a/packages/webapp/src/main/components/application-bar/menues/help-menu.tsx +++ b/packages/webapp/src/main/components/application-bar/menues/help-menu.tsx @@ -1,72 +1,24 @@ -import React, { Component } from 'react'; +import React from 'react'; import { NavDropdown } from 'react-bootstrap'; import { bugReportURL } from '../../../constant'; -import { ModalRepository } from '../../../services/modal/modal-repository'; -import { connect } from 'react-redux'; -import { ApplicationState } from '../../store/application-state'; import { ModalContentType } from '../../modals/application-modal-types'; - -type OwnProps = {}; - -type State = { show: boolean }; - -type StateProps = {}; - -type DispatchProps = { - openModal: typeof ModalRepository.showModal; +import { useAppDispatch } from '../../store/hooks'; +import { showModal } from '../../../services/modal/modalSlice'; + +export const HelpMenu: React.FC = () => { + const dispatch = useAppDispatch(); + + return ( + + dispatch(showModal({ type: ModalContentType.HelpModelingModal, size: 'lg' }))}> + How does this Editor work? + + dispatch(showModal({ type: ModalContentType.InformationModal }))}> + About Apollon + + + Report a Problem + + + ); }; - -type Props = OwnProps & StateProps & DispatchProps; - -const enhance = connect(null, { - openModal: ModalRepository.showModal, -}); - -class HelpMenuComponent extends Component { - constructor(props: Props) { - super(props); - } - - componentDidMount() { - document.addEventListener('click', this.hideMenu); - } - - componentWillUnmount() { - document.removeEventListener('click', this.hideMenu); - } - - showMenu = (event: React.MouseEvent) => { - this.setState({ show: true }); - event.stopPropagation(); - }; - - hideMenu = (event: MouseEvent) => { - this.setState({ show: false }); - event.stopPropagation(); - }; - - render() { - return ( - <> - - this.props.openModal(ModalContentType.HelpModelingModal, 'lg')}> - How does this Editor work? - - this.props.openModal(ModalContentType.InformationModal)}> - About Apollon - - - Report a Problem - - - - ); - } -} - -export const HelpMenu = enhance(HelpMenuComponent); diff --git a/packages/webapp/src/main/components/application-bar/menues/theme-switcher-menu.tsx b/packages/webapp/src/main/components/application-bar/menues/theme-switcher-menu.tsx index 350cad4..e5a06be 100644 --- a/packages/webapp/src/main/components/application-bar/menues/theme-switcher-menu.tsx +++ b/packages/webapp/src/main/components/application-bar/menues/theme-switcher-menu.tsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, { useState, useEffect } from 'react'; import { connect } from 'react-redux'; import { ApplicationState } from '../../store/application-state'; import { setTheme, toggleTheme } from '../../../utils/theme-switcher'; @@ -6,146 +6,112 @@ import { LocalStorageRepository } from '../../../../main/services/local-storage/ import { OverlayTrigger, Tooltip } from 'react-bootstrap'; import ThemeSwitcherIcon from 'webapp/assets/theme-switcher.svg'; -type OwnProps = {}; -type State = { isDarkMode: boolean; showTooltip: boolean; overrideUserThemePreference: boolean }; -type StateProps = {}; +export const ThemeSwitcherMenu: React.FC = () => { + const [isDarkMode, setIsDarkMode] = useState(false); + const [showTooltip, setShowTooltip] = useState(false); + const [overrideUserThemePreference, setOverrideUserThemePreference] = useState(true); -type DispatchProps = {}; + useEffect(() => { + updateState(); + }, []); -type Props = OwnProps & StateProps & DispatchProps; - -const enhance = connect(null, {}); - -class ThemeSwitcherMenuComponent extends Component { - constructor(props: Props) { - super(props); - } - - componentDidMount() { - this.updateState(); - } - - updateTheme = () => { + const updateTheme = () => { toggleTheme(); - this.updateState(); + updateState(); }; - isDarkMode = () => { + const isDarkModeActive = (): boolean => { const systemTheme = LocalStorageRepository.getSystemThemePreference(); const preferredTheme = LocalStorageRepository.getUserThemePreference(); return (preferredTheme || systemTheme) === 'dark'; }; - updateState = () => { - this.setState({ - isDarkMode: this.isDarkMode(), - overrideUserThemePreference: LocalStorageRepository.getUserThemePreference() ? false : true, - }); + const updateState = () => { + setIsDarkMode(isDarkModeActive()); + setOverrideUserThemePreference(!LocalStorageRepository.getUserThemePreference()); }; - getSystemTheme = (): string => { - if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { - return 'dark'; - } else { - return 'light'; - } + const getSystemTheme = (): string => { + return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; }; - handleInputChange = () => { - if (!this.state.overrideUserThemePreference) { - LocalStorageRepository.setSystemThemePreference(this.getSystemTheme()); + const handleInputChange = () => { + if (!overrideUserThemePreference) { + LocalStorageRepository.setSystemThemePreference(getSystemTheme()); LocalStorageRepository.removeUserThemePreference(); - setTheme(this.getSystemTheme()); - this.updateState(); + setTheme(getSystemTheme()); + updateState(); } else { - this.setState({ overrideUserThemePreference: false }); - LocalStorageRepository.setUserThemePreference(this.getSystemTheme()); + setOverrideUserThemePreference(false); + LocalStorageRepository.setUserThemePreference(getSystemTheme()); } }; - onToggle = () => { - if (!this.state.showTooltip) { - this.setState({ showTooltip: true }); + const onToggle = () => { + if (!showTooltip) { + setShowTooltip(true); } else { - if (this.isPopoverHovered()) { - this.setState({ showTooltip: true }); - setTimeout(() => { - this.onToggle(); - }, 500); + if (isPopoverHovered()) { + setShowTooltip(true); + setTimeout(onToggle, 500); } else { - this.setState({ showTooltip: false }); + setShowTooltip(false); } } }; - isPopoverHovered = () => { + const isPopoverHovered = (): boolean => { const elem = document.getElementById('tooltip-bottom'); - if (elem?.parentElement?.querySelector(':hover') === elem) { - return true; - } - return false; + return elem?.parentElement?.querySelector(':hover') === elem; }; - render() { - return ( - <> - { - this.onToggle(); - }} - delay={{ show: 0, hide: 0 }} - overlay={ - -
-
- ☾ Dark Mode ☾ -
-
- Sync with Operating System - -
+ return ( + +
+
+ ☾ Dark Mode ☾ +
+
+ Sync with Operating System + +
- {this.isDarkMode() && ( -
- - You can click this icon at any time to disable the dark mode if you experience problems. - -
- )} - {!this.isDarkMode() && ( -
- Try the dark mode and relieve your eyes. -
- )} + {isDarkMode && ( +
+ You can click this icon at any time to disable the dark mode if you experience problems.
- - } - > -
{ - this.updateTheme(); - }} - > -
- -
+ )} + {!isDarkMode && ( +
+ Try the dark mode and relieve your eyes. +
+ )}
- - - ); - } -} + + } + > +
+
+ +
+
+ + ); +}; -export const ThemeSwitcherMenu = enhance(ThemeSwitcherMenuComponent); diff --git a/packages/webapp/src/main/components/application-bar/menues/view-menu.tsx b/packages/webapp/src/main/components/application-bar/menues/view-menu.tsx index 92615c4..7489eec 100644 --- a/packages/webapp/src/main/components/application-bar/menues/view-menu.tsx +++ b/packages/webapp/src/main/components/application-bar/menues/view-menu.tsx @@ -1,55 +1,21 @@ -import React, { Component } from 'react'; +import React from 'react'; import { NavDropdown } from 'react-bootstrap'; import { ApollonMode } from '@ls1intum/apollon'; -import { connect } from 'react-redux'; -import { ApplicationState } from '../../store/application-state'; -import { EditorOptionsRepository } from '../../../services/editor-options/editor-options-repository'; - -type OwnProps = {}; - -type State = {}; - -type StateProps = { - mode: ApollonMode; -}; - -type DispatchProps = { - changeMode: typeof EditorOptionsRepository.changeEditorMode; +import { useAppDispatch } from '../../store/hooks'; +import { changeEditorMode } from '../../../services/editor-options/editorOptionSlice'; + +export const ViewMenu = () => { + const dispatch = useAppDispatch(); + return ( + + {Object.keys(ApollonMode).map((mode) => ( + dispatch(changeEditorMode(ApollonMode[mode as keyof typeof ApollonMode]))} + > + {mode} + + ))} + + ); }; - -type Props = OwnProps & StateProps & DispatchProps; - -const enhance = connect( - (state) => { - return { - mode: state.editorOptions.mode!, - }; - }, - { - changeMode: EditorOptionsRepository.changeEditorMode, - }, -); -class ViewMenuComponent extends Component { - constructor(props: Props) { - super(props); - } - - render() { - return ( - <> - - {Object.keys(ApollonMode).map((mode) => ( - this.props.changeMode(ApollonMode[mode as keyof typeof ApollonMode])} - > - {mode} - - ))} - - - ); - } -} - -export const ViewMenu = enhance(ViewMenuComponent); diff --git a/packages/webapp/src/main/components/error-handling/error-message.tsx b/packages/webapp/src/main/components/error-handling/error-message.tsx index ec97691..566f773 100644 --- a/packages/webapp/src/main/components/error-handling/error-message.tsx +++ b/packages/webapp/src/main/components/error-handling/error-message.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { Alert } from 'react-bootstrap'; -import { ApollonError } from '../../services/error-management/error-types'; +import { ApollonError } from '../../services/error-management/errorManagementSlice'; type Props = { error: ApollonError; diff --git a/packages/webapp/src/main/components/error-handling/error-panel.tsx b/packages/webapp/src/main/components/error-handling/error-panel.tsx index cb952ec..2d62bc0 100644 --- a/packages/webapp/src/main/components/error-handling/error-panel.tsx +++ b/packages/webapp/src/main/components/error-handling/error-panel.tsx @@ -1,32 +1,20 @@ import React from 'react'; -import { connect } from 'react-redux'; -import { ApplicationState } from '../store/application-state'; -import { ApollonError } from '../../services/error-management/error-types'; -import { ErrorMessage } from './error-message'; -import { ErrorRepository } from '../../services/error-management/error-repository'; - -type OwnProps = {}; - -type DispatchProps = { dismissError: typeof ErrorRepository.dismissError }; -type StateProps = { errors: ApollonError[] }; +import { ErrorMessage } from './error-message'; +import { useAppDispatch, useAppSelector } from '../store/hooks'; +import { dismissError } from '../../services/error-management/errorManagementSlice'; -type Props = StateProps & DispatchProps & OwnProps; -const enhance = connect( - (state) => ({ - errors: state.errors, - }), - { dismissError: ErrorRepository.dismissError }, -); -function ErrorPanelComponent(props: Props) { +export const ErrorPanel:React.FC =()=> { + const errors = useAppSelector(state=>state.errors) +const dispatch = useAppDispatch(); return ( <> - {props.errors.map((error, index) => ( + {errors.map((error, index) => ( props.dismissError(apollonError.id)} + onClose={(apollonError) => dispatch(dismissError(apollonError.id))} key={index} /> ))} @@ -34,4 +22,4 @@ function ErrorPanelComponent(props: Props) { ); } -export const ErrorPanel = enhance(ErrorPanelComponent); + diff --git a/packages/webapp/src/main/components/modals/help-modeling-modal/help-modeling-modal.tsx b/packages/webapp/src/main/components/modals/help-modeling-modal/help-modeling-modal.tsx index 4ba94ef..8974e45 100644 --- a/packages/webapp/src/main/components/modals/help-modeling-modal/help-modeling-modal.tsx +++ b/packages/webapp/src/main/components/modals/help-modeling-modal/help-modeling-modal.tsx @@ -1,84 +1,79 @@ -import React, { Component } from 'react'; +import React from 'react'; import { Button, Modal } from 'react-bootstrap'; -import { ModalContentProps } from '../application-modal-types'; +import { useAppDispatch } from '../../store/hooks'; +import { hideModal } from '../../../services/modal/modalSlice'; -type Props = {} & ModalContentProps; - -type State = {}; - -export class HelpModelingModal extends Component { - render() { - return ( - <> - - How to use this editor? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Add Class - To add a class, simply drag and drop one of the elements on the right side into the editor area on the - left side. - - Image not found -
Add Association - To add an association, select the source class with a single click and you will see four blue circles. - Those are the possible connection points for associations. Click and hold on one of those and drag it - to another blue circle to create an association. - - Image not found -
Edit Class - To edit a class, double click it and a popup will open up, in which you can edit its components, e.g. - name, attributes, methods, etc. - - Image not found -
Delete Class - To delete a class, select it with a single click and either press Delete or{' '} - Backspace on your keyboard. -
Move Class - To move a class, select it with a single click and either use your keyboard arrows or drag and drop - it. - - Image not found -
Undo & Redo - With Ctrl+Z and Ctrl+Y you can undo and redo your changes. -
-
- - - - - ); - } -} +export const HelpModelingModal: React.FC = () => { + const dispatch = useAppDispatch(); + return ( + <> + + How to use this editor? + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Add Class + To add a class, simply drag and drop one of the elements on the right side into the editor area on the + left side. + + Image not found +
Add Association + To add an association, select the source class with a single click and you will see four blue circles. + Those are the possible connection points for associations. Click and hold on one of those and drag it to + another blue circle to create an association. + + Image not found +
Edit Class + To edit a class, double click it and a popup will open up, in which you can edit its components, e.g. + name, attributes, methods, etc. + + Image not found +
Delete Class + To delete a class, select it with a single click and either press Delete or{' '} + Backspace on your keyboard. +
Move Class + To move a class, select it with a single click and either use your keyboard arrows or drag and drop it. + + Image not found +
Undo & Redo + With Ctrl+Z and Ctrl+Y you can undo and redo your changes. +
+
+ + + + + ); +}; diff --git a/packages/webapp/src/main/components/modals/load-diagram-modal/load-diagram-item.tsx b/packages/webapp/src/main/components/modals/load-diagram-modal/load-diagram-item.tsx index 1e36f17..c207c2e 100644 --- a/packages/webapp/src/main/components/modals/load-diagram-modal/load-diagram-item.tsx +++ b/packages/webapp/src/main/components/modals/load-diagram-modal/load-diagram-item.tsx @@ -1,10 +1,8 @@ import React from 'react'; import { LocalStorageDiagramListItem } from '../../../services/local-storage/local-storage-types'; import styled from 'styled-components'; -import { connect } from 'react-redux'; -import { ApplicationState } from '../../store/application-state'; -import { Locale } from '@ls1intum/apollon'; import { longDate } from '../../../constant'; +import { useAppSelector } from '../../store/hooks'; const SubTitle = styled.span` display: block; @@ -12,37 +10,22 @@ const SubTitle = styled.span` color: #9e9e9e; `; -type OwnProps = { +type Props = { item: LocalStorageDiagramListItem; }; -type StateProps = { - locale: Locale; -}; - -type DispatchProps = {}; - -type Props = OwnProps & StateProps & DispatchProps; - -const enhance = connect((state) => { - return { - locale: state.editorOptions.locale, - }; -}); - -const LoadDiagramItemComponent = (props: Props) => { +export const LoadDiagramItem: React.FC = ({ item }) => { + const locale = useAppSelector((state) => state.editorOptions.locale); return (
- {props.item.title} - {props.item.type} + {item.title} + {item.type}
last updated: - {props.item.lastUpdate.locale(props.locale).format(longDate)} + {item.lastUpdate.locale(locale).format(longDate)}
); }; - -export const LoadDiagramItem = enhance(LoadDiagramItemComponent); diff --git a/packages/webapp/src/main/components/modals/load-diagram-modal/load-diagram-modal.tsx b/packages/webapp/src/main/components/modals/load-diagram-modal/load-diagram-modal.tsx index dbc3b1f..8dc7243 100644 --- a/packages/webapp/src/main/components/modals/load-diagram-modal/load-diagram-modal.tsx +++ b/packages/webapp/src/main/components/modals/load-diagram-modal/load-diagram-modal.tsx @@ -1,85 +1,35 @@ -import React, { Component, ComponentClass } from 'react'; +import React from 'react'; import { Modal } from 'react-bootstrap'; import { LocalStorageRepository } from '../../../services/local-storage/local-storage-repository'; -import { compose } from 'redux'; -import { withApollonEditor } from '../../apollon-editor-component/with-apollon-editor'; -import { connect } from 'react-redux'; -import { ApplicationState } from '../../store/application-state'; import { LocalStorageDiagramListItem } from '../../../services/local-storage/local-storage-types'; import { LoadDiagramContent } from './load-diagram-content'; -import { ModalContentProps } from '../application-modal-types'; +import { useAppDispatch, useAppSelector } from '../../store/hooks'; +import { hideModal } from '../../../services/modal/modalSlice'; +import { useLocalStorage } from '../../../services/local-storage/useLocalStorage'; -type OwnProps = {} & ModalContentProps; +export const LoadDiagramModal: React.FC = () => { + const { diagram } = useAppSelector((state) => state.diagram); + const dispatch = useAppDispatch(); + const loadDiagram = useLocalStorage(); -type State = {}; - -type DispatchProps = { - load: typeof LocalStorageRepository.load; -}; - -type StateProps = { - currentDiagramId?: string; -}; - -type Props = StateProps & DispatchProps & OwnProps; - -const enhance = compose>( - withApollonEditor, - connect( - (state) => ({ - currentDiagramId: state.diagram?.id, - }), - { - load: LocalStorageRepository.load, - }, - ), -); - -const getInitialState = (): State => { - return { - selectedDiagramId: undefined, - }; -}; - -class LoadDiagramModalComponent extends Component { - state = getInitialState(); - - constructor(props: Props) { - super(props); - this.loadDiagram = this.loadDiagram.bind(this); - } - - getSavedDiagrams(): LocalStorageDiagramListItem[] { - // load localStorageList + const getSavedDiagrams = (): LocalStorageDiagramListItem[] => { const localDiagrams = LocalStorageRepository.getStoredDiagrams(); - // return all diagrams, but the current displayed diagram - return localDiagrams.filter((storedDiagram) => storedDiagram.id !== this.props.currentDiagramId); - } - - handleClose = () => { - this.props.close(); - this.setState(getInitialState()); + return localDiagrams.filter((storedDiagram) => storedDiagram.id !== diagram?.id); }; - loadDiagram = (id: string) => { - if (id) { - this.props.load(id); - } - this.handleClose(); + const onSelect = (id: string) => { + loadDiagram(id); + dispatch(hideModal()); }; - render() { - return ( - <> - - Load Diagram - - - - - - ); - } -} - -export const LoadDiagramModal = enhance(LoadDiagramModalComponent); + return ( + <> + + Load Diagram + + + + + + ); +}; diff --git a/packages/webapp/src/main/components/store/application-state.ts b/packages/webapp/src/main/components/store/application-state.ts deleted file mode 100644 index 924b587..0000000 --- a/packages/webapp/src/main/components/store/application-state.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Diagram, DiagramState } from '../../services/diagram/diagramSlice'; -import { EditorOptions } from '../../services/editor-options/editor-options-types'; -import { ApollonError } from '../../services/error-management/error-types'; -import { ModalState } from '../../services/modal/modal-types'; -import { ShareState } from '../../services/share/share-types'; - -export interface ApplicationState { - diagram: DiagramState; - editorOptions: EditorOptions; - errors: ApollonError[]; - modal: ModalState; - share: ShareState; -} diff --git a/packages/webapp/src/main/components/store/application-store.tsx b/packages/webapp/src/main/components/store/application-store.tsx index 3097b37..4892be5 100644 --- a/packages/webapp/src/main/components/store/application-store.tsx +++ b/packages/webapp/src/main/components/store/application-store.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { combineReducers, PreloadedState } from 'redux'; import { Provider } from 'react-redux'; import { configureStore } from '@reduxjs/toolkit'; import moment from 'moment'; @@ -9,14 +8,19 @@ import { localStorageCollaborationName, localStorageCollaborationColor, } from '../../constant'; -import { defaultEditorOptions } from '../../services/editor-options/editor-options-reducer'; -import { EditorOptions } from '../../services/editor-options/editor-options-types'; import { uuid } from '../../utils/uuid'; -import { reducers } from '../../services/reducer'; -import { Diagram, DiagramState } from '../../services/diagram/diagramSlice'; -import { ApollonError } from '../../services/error-management/error-types'; +import { Diagram, diagramReducer, DiagramState } from '../../services/diagram/diagramSlice'; + import { ModalState } from '../../services/modal/modal-types'; import { ShareState } from '../../services/share/share-types'; +import { + defaultEditorOptions, + EditorOptions, + editorOptionsReducer, +} from '../../services/editor-options/editorOptionSlice'; +import { ApollonError, errorReducer } from '../../services/error-management/errorManagementSlice'; +import { modalReducer } from '../../services/modal/modalSlice'; +import { shareReducer } from '../../services/share/shareSlice'; interface ApplicationState { diagram: DiagramState; @@ -62,7 +66,14 @@ const getInitialStore = (): ApplicationState => { }; const store = configureStore({ - reducer: combineReducers(reducers), + // reducer: combineReducers(reducers), + reducer: { + diagram: diagramReducer, + editorOptions: editorOptionsReducer, + errors: errorReducer, + modal: modalReducer, + share: shareReducer, + }, middleware: (getDefaultMiddleware) => getDefaultMiddleware(), preloadedState: getInitialStore(), devTools: process.env.NODE_ENV !== 'production', // Enable Redux DevTools in non-production environments diff --git a/packages/webapp/src/main/hocs/withRouter.tsx b/packages/webapp/src/main/hocs/withRouter.tsx deleted file mode 100644 index 80bcc87..0000000 --- a/packages/webapp/src/main/hocs/withRouter.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React, { ComponentClass, FunctionComponent } from 'react'; -import { Location, NavigateFunction, Params, useLocation, useNavigate, useParams } from 'react-router-dom'; - -export interface RouterTypes { - location: Location; - navigate: NavigateFunction; - params: Readonly>; -} - -export function withRouter(WrappedComponent: ComponentClass): FunctionComponent { - function ComponentWithRouterProp(props: T) { - const location = useLocation(); - const navigate = useNavigate(); - const params = useParams(); - return ; - } - - return ComponentWithRouterProp as FunctionComponent; -} diff --git a/packages/webapp/src/main/services/diagram/diagram-epics.ts b/packages/webapp/src/main/services/diagram/diagram-epics.ts deleted file mode 100644 index 267bbf5..0000000 --- a/packages/webapp/src/main/services/diagram/diagram-epics.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { combineEpics, Epic, StateObservable } from 'redux-observable'; -import { Action } from 'redux'; -import { ChangeDiagramTypeAction } from '../editor-options/editor-options-types'; -import { ApplicationState } from '../../components/store/application-state'; -import { filter, map, mergeMap } from 'rxjs/operators'; -import { CreateDiagramAction, Diagram, DiagramActionTypes, UpdateDiagramAction } from './diagram-types'; -import { uuid } from '../../utils/uuid'; -import moment from 'moment'; -import { DiagramRepository } from './diagram-repository'; -import { StoreAction } from '../local-storage/local-storage-types'; -import { LocalStorageRepository } from '../local-storage/local-storage-repository'; -import { Observable, of } from 'rxjs'; -import { EditorOptionsRepository } from '../editor-options/editor-options-repository'; - -export const createDiagramEpic: Epic = ( - action$: Observable>, -) => { - return action$.pipe( - filter((action) => action.type === DiagramActionTypes.CREATE_DIAGRAM), - map((action) => action as CreateDiagramAction), - mergeMap((action: CreateDiagramAction) => { - const { diagramTitle, diagramType, template } = action.payload; - const diagram: Diagram = { - id: uuid(), - title: diagramTitle, - model: template, - lastUpdate: moment(), - }; - return of(DiagramRepository.updateDiagram(diagram), EditorOptionsRepository.changeDiagramType(diagramType)); - }), - ); -}; - -/** - * side effect after Reducer for CREATE_DIAGRAM action has received message -> change diagram type of editor to new diagram - * @param action$ - * @param store - */ -export const updateDiagramEpic: Epic< - Action, - StoreAction | UpdateDiagramAction | ChangeDiagramTypeAction, - ApplicationState -> = (action$: Observable>, store: StateObservable) => { - return action$.pipe( - filter((action) => action.type === DiagramActionTypes.UPDATE_DIAGRAM), - map((action) => action as UpdateDiagramAction), - map((action: UpdateDiagramAction) => { - if (!store.value.diagram) { - throw Error('Updated diagram is not undefined or null'); - } - return LocalStorageRepository.store(store.value.diagram); - }), - ); -}; - -export const diagramEpics = combineEpics(createDiagramEpic, updateDiagramEpic) as any; diff --git a/packages/webapp/src/main/services/diagram/diagram-repository.ts b/packages/webapp/src/main/services/diagram/diagram-repository.ts index f6f02c2..21e9fd8 100644 --- a/packages/webapp/src/main/services/diagram/diagram-repository.ts +++ b/packages/webapp/src/main/services/diagram/diagram-repository.ts @@ -1,23 +1,8 @@ -import { CreateDiagramAction, Diagram, DiagramActionTypes, UpdateDiagramAction } from './diagram-types'; -import { UMLDiagramType, UMLModel } from '@ls1intum/apollon'; import { BASE_URL } from '../../constant'; import { DiagramDTO } from 'shared/src/main/diagram-dto'; +import { Diagram } from './diagramSlice'; export const DiagramRepository = { - createDiagram: (diagramTitle: string, diagramType: UMLDiagramType, template?: UMLModel): CreateDiagramAction => ({ - type: DiagramActionTypes.CREATE_DIAGRAM, - payload: { - diagramType, - diagramTitle, - template, - }, - }), - updateDiagram: (values: Partial): UpdateDiagramAction => ({ - type: DiagramActionTypes.UPDATE_DIAGRAM, - payload: { - values, - }, - }), getDiagramFromServerByToken(token: string): Promise { const resourceUrl = `${BASE_URL}/diagrams/${token}`; return fetch(resourceUrl, { diff --git a/packages/webapp/src/main/services/diagram/diagram-types.ts b/packages/webapp/src/main/services/diagram/diagram-types.ts deleted file mode 100644 index e0936d7..0000000 --- a/packages/webapp/src/main/services/diagram/diagram-types.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Action } from 'redux'; -import { UMLDiagramType, UMLModel } from '@ls1intum/apollon'; -import { Moment } from 'moment'; - -export type DiagramActions = UpdateDiagramAction | CreateDiagramAction; - -export const enum DiagramActionTypes { - CREATE_DIAGRAM = '@@diagram/create_diagram', - UPDATE_DIAGRAM = '@@diagram/update', -} - -export type CreateDiagramAction = Action & { - payload: { - diagramTitle: string; - diagramType: UMLDiagramType; - template?: UMLModel; - }; -}; - -export type UpdateDiagramAction = Action & { - payload: { - values: Partial; - }; -}; diff --git a/packages/webapp/src/main/services/diagram/diagramSlice.ts b/packages/webapp/src/main/services/diagram/diagramSlice.ts index 67eae0f..bcdef4b 100644 --- a/packages/webapp/src/main/services/diagram/diagramSlice.ts +++ b/packages/webapp/src/main/services/diagram/diagramSlice.ts @@ -4,6 +4,7 @@ import { DiagramRepository } from './diagram-repository'; import { uuid } from '../../utils/uuid'; import moment, { Moment } from 'moment'; import { changeDiagramType } from '../editor-options/editorOptionSlice'; +import { LocalStorageRepository } from '../local-storage/local-storage-repository'; export type Diagram = { id: string; @@ -25,7 +26,15 @@ const initialState: DiagramState = { error: null, }; -// Thunk to handle creating a diagram +export const updateDiagramThunk = createAsyncThunk( + 'diagram/updateWithLocalStorage', + async (diagram: Partial, { dispatch }) => { + console.log('DEBUG updateDiagramThunk before await'); + await dispatch(updateDiagram(diagram)); + console.log('DEBUG updateDiagramThunk after await'); + }, +); + export const createDiagram = createAsyncThunk( 'diagram/create', async ( @@ -39,28 +48,13 @@ export const createDiagram = createAsyncThunk( lastUpdate: moment(), }; - // Dispatch the update diagram action after creation - dispatch(updateDiagram(diagram)); + dispatch(updateDiagramThunk(diagram)); dispatch(changeDiagramType(diagramType)); return diagram; }, ); -// Thunk to handle fetching a diagram from the server -export const fetchDiagramByToken = createAsyncThunk( - 'diagram/fetchByToken', - async (token: string, { rejectWithValue }) => { - try { - const diagram = await DiagramRepository.getDiagramFromServerByToken(token); - return diagram; - } catch (error) { - return rejectWithValue('Failed to fetch diagram'); - } - }, -); - -// Define the slice using Redux Toolkit const diagramSlice = createSlice({ name: 'diagram', initialState, @@ -75,51 +69,14 @@ const diagramSlice = createSlice({ }, }, extraReducers: (builder) => { - builder - // // Create Diagram - // .addCase(createDiagram.pending, (state) => { - // state.loading = true; - // state.error = null; - // }) - // .addCase(createDiagram.fulfilled, (state, action: PayloadAction) => { - // state.loading = false; - // state.diagram = action.payload; - // }) - // .addCase(createDiagram.rejected, (state, action) => { - // state.loading = false; - // state.error = action.error.message || 'Failed to create diagram'; - // }) - - // // Update Diagram - // .addCase(updateDiagram.pending, (state) => { - // state.loading = true; - // state.error = null; - // }) - // .addCase(updateDiagram.fulfilled, (state, action: PayloadAction>) => { - // state.loading = false; - // if (state.diagram) { - // state.diagram = { ...state.diagram, ...action.payload, lastUpdate: moment() }; - // } - // }) - // .addCase(updateDiagram.rejected, (state, action) => { - // state.loading = false; - // state.error = action.error.message || 'Failed to update diagram'; - // }) - - // Fetch Diagram - .addCase(fetchDiagramByToken.pending, (state) => { - state.loading = true; - state.error = null; - }) - .addCase(fetchDiagramByToken.fulfilled, (state, action: PayloadAction) => { - state.loading = false; - state.diagram = action.payload; - }) - .addCase(fetchDiagramByToken.rejected, (state, action) => { - state.loading = false; - state.error = action.payload as string; - }); + builder.addCase(updateDiagramThunk.fulfilled, (state) => { + if (state.diagram) { + console.log('DEBUG extraReducers LocalStorageRepository storeDiagram is called'); + LocalStorageRepository.storeDiagram(state.diagram); + } + }); }, + selectors: { selectDiagram: (state: DiagramState) => state.diagram, }, diff --git a/packages/webapp/src/main/services/editor-options/editor-options-reducer.ts b/packages/webapp/src/main/services/editor-options/editor-options-reducer.ts deleted file mode 100644 index 3298604..0000000 --- a/packages/webapp/src/main/services/editor-options/editor-options-reducer.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Reducer } from 'redux'; -import { Actions } from '../actions'; -import { EditorOptions, EditorOptionsActionTypes } from './editor-options-types'; -import { UMLDiagramType } from '@ls1intum/apollon'; -import { ApollonMode, Locale } from '@ls1intum/apollon/lib/es6/services/editor/editor-types'; - -export const defaultEditorOptions: EditorOptions = { - type: UMLDiagramType.ClassDiagram, - mode: ApollonMode.Modelling, - readonly: false, - enablePopups: true, - enableCopyPaste: true, - locale: Locale.en, - colorEnabled: true, -}; - -export const EditorOptionsReducer: Reducer = (state = defaultEditorOptions, action) => { - // state is set in application.tsx -> always a state - switch (action.type) { - case EditorOptionsActionTypes.CHANGE_DIAGRAM_TYPE: { - const type = action.payload.type as UMLDiagramType; - return { ...state, type }; - } - case EditorOptionsActionTypes.CHANGE_EDITOR_MODE: { - const mode = action.payload.mode; - return { ...state, mode }; - } - case EditorOptionsActionTypes.CHANGE_READONLY_MODE: { - const readonly = action.payload.readonly; - return { ...state, readonly }; - } - } - - return state; -}; diff --git a/packages/webapp/src/main/services/editor-options/editor-options-repository.ts b/packages/webapp/src/main/services/editor-options/editor-options-repository.ts deleted file mode 100644 index 8afe006..0000000 --- a/packages/webapp/src/main/services/editor-options/editor-options-repository.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ApollonMode, UMLDiagramType } from '@ls1intum/apollon'; -import { - ChangeDiagramTypeAction, - ChangeEditorModeAction, - ChangeReadonlyModeAction, - EditorOptionsActionTypes, -} from './editor-options-types'; - -export const EditorOptionsRepository = { - changeDiagramType: (diagramType: UMLDiagramType): ChangeDiagramTypeAction => ({ - type: EditorOptionsActionTypes.CHANGE_DIAGRAM_TYPE, - payload: { - type: diagramType, - }, - }), - - changeEditorMode: (editorMode: ApollonMode): ChangeEditorModeAction => ({ - type: EditorOptionsActionTypes.CHANGE_EDITOR_MODE, - payload: { - mode: editorMode, - }, - }), - - changeReadonlyMode: (readonly: boolean): ChangeReadonlyModeAction => ({ - type: EditorOptionsActionTypes.CHANGE_READONLY_MODE, - payload: { - readonly, - }, - }), -}; diff --git a/packages/webapp/src/main/services/editor-options/editor-options-types.ts b/packages/webapp/src/main/services/editor-options/editor-options-types.ts deleted file mode 100644 index d5f67b3..0000000 --- a/packages/webapp/src/main/services/editor-options/editor-options-types.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Action, DeepPartial } from 'redux'; -import { UMLDiagramType } from '@ls1intum/apollon'; -import { ApollonMode, Locale } from '@ls1intum/apollon/lib/es6/services/editor/editor-types'; -import { Styles } from '@ls1intum/apollon/lib/es6/components/theme/styles'; - -export type EditorOptions = { - type: UMLDiagramType; - mode?: ApollonMode; - readonly?: boolean; - enablePopups?: boolean; - enableCopyPaste?: boolean; - theme?: DeepPartial; - locale: Locale; - colorEnabled?: boolean; -}; - -export type EditorOptionsActions = ChangeDiagramTypeAction | ChangeEditorModeAction | ChangeReadonlyModeAction; - -export const enum EditorOptionsActionTypes { - CHANGE_DIAGRAM_TYPE = '@@editor-options/change_diagram_type', - CHANGE_EDITOR_MODE = '@@editor-options/change_editor_mode', - CHANGE_READONLY_MODE = '@@editor-options/change_readonly_mode', -} - -export type ChangeDiagramTypeAction = Action & { - payload: { - type: UMLDiagramType; - }; -}; - -export type ChangeEditorModeAction = Action & { - payload: { - mode: ApollonMode; - }; -}; - -export type ChangeReadonlyModeAction = Action & { - payload: { - readonly: boolean; - }; -}; diff --git a/packages/webapp/src/main/services/editor-options/editorOptionSlice.ts b/packages/webapp/src/main/services/editor-options/editorOptionSlice.ts index 279aa3a..1951507 100644 --- a/packages/webapp/src/main/services/editor-options/editorOptionSlice.ts +++ b/packages/webapp/src/main/services/editor-options/editorOptionSlice.ts @@ -4,7 +4,6 @@ import { ApollonMode, Locale } from '@ls1intum/apollon/lib/es6/services/editor/e import { Styles } from '@ls1intum/apollon/lib/es6/components/theme/styles'; import { DeepPartial } from 'redux'; -// Define the editor options type export type EditorOptions = { type: UMLDiagramType; mode?: ApollonMode; @@ -16,7 +15,6 @@ export type EditorOptions = { colorEnabled?: boolean; }; -// Define the initial/default state export const defaultEditorOptions: EditorOptions = { type: UMLDiagramType.ClassDiagram, mode: ApollonMode.Modelling, @@ -27,28 +25,22 @@ export const defaultEditorOptions: EditorOptions = { colorEnabled: true, }; -// Create the slice const editorOptionsSlice = createSlice({ name: 'editorOptions', initialState: defaultEditorOptions, reducers: { - // Action to change the diagram type - changeDiagramType(state, action: PayloadAction< UMLDiagramType >) { + changeDiagramType(state, action: PayloadAction) { state.type = action.payload; }, - // Action to change the editor mode - changeEditorMode(state, action: PayloadAction< ApollonMode >) { + changeEditorMode(state, action: PayloadAction) { state.mode = action.payload; }, - // Action to change readonly mode - changeReadonlyMode(state, action: PayloadAction) { + changeReadonlyMode(state, action: PayloadAction) { state.readonly = action.payload; }, }, }); -// Export actions export const { changeDiagramType, changeEditorMode, changeReadonlyMode } = editorOptionsSlice.actions; -// Export the reducer to be used in the store configuration export const editorOptionsReducer = editorOptionsSlice.reducer; diff --git a/packages/webapp/src/main/services/error-management/error-reducer.ts b/packages/webapp/src/main/services/error-management/error-reducer.ts deleted file mode 100644 index 5f59e09..0000000 --- a/packages/webapp/src/main/services/error-management/error-reducer.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Reducer } from 'redux'; -import { Actions } from '../actions'; -import { ApollonError, ErrorActionType } from './error-types'; - -export const ErrorReducer: Reducer = (state = [], action) => { - switch (action.type) { - case ErrorActionType.DISPLAY_ERROR: { - const { payload } = action; - const errors: ApollonError[] = state ? [...state] : []; - errors.push({ ...payload }); - return errors; - } - case ErrorActionType.DISMISS_ERROR: { - const { payload } = action; - let errors: ApollonError[] = state ? [...state] : []; - errors = errors.filter((error) => error.id !== payload.id); - return errors; - } - } - - return state; -}; diff --git a/packages/webapp/src/main/services/error-management/error-repository.ts b/packages/webapp/src/main/services/error-management/error-repository.ts deleted file mode 100644 index dac4842..0000000 --- a/packages/webapp/src/main/services/error-management/error-repository.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ErrorActionType } from './error-types'; -import { uuid } from '../../utils/uuid'; - -export const ErrorRepository = { - createError: (type: ErrorActionType, headerText: string, bodyText: string) => ({ - type, - payload: { - id: uuid(), - headerText, - bodyText, - }, - }), - dismissError: (errorId: string) => ({ - type: ErrorActionType.DISMISS_ERROR, - payload: { - id: errorId, - }, - }), -}; diff --git a/packages/webapp/src/main/services/error-management/error-types.ts b/packages/webapp/src/main/services/error-management/error-types.ts deleted file mode 100644 index e76d04e..0000000 --- a/packages/webapp/src/main/services/error-management/error-types.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Action } from 'redux'; - -export type ErrorActions = DisplayErrorAction | DismissErrorAction; - -export const enum ErrorActionType { - DISPLAY_ERROR = '@@error/display', - DISMISS_ERROR = '@@error/dismiss', -} - -export type ApollonError = { id: string; headerText: string; bodyText: string }; - -export type ErrorAction = { - payload: ApollonError; -}; - -export type DisplayErrorAction = Action & ErrorAction; -export type DismissErrorAction = Action & ErrorAction; diff --git a/packages/webapp/src/main/services/error-management/errorManagementSlice.ts b/packages/webapp/src/main/services/error-management/errorManagementSlice.ts index fc02c5c..d8ba37f 100644 --- a/packages/webapp/src/main/services/error-management/errorManagementSlice.ts +++ b/packages/webapp/src/main/services/error-management/errorManagementSlice.ts @@ -1,43 +1,37 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { v4 as uuidv4 } from 'uuid'; // Assuming you're using uuid to generate unique error IDs +import { v4 as uuidv4 } from 'uuid'; -// Define the error type -export type ApollonError = { - id: string; - headerText: string; - bodyText: string; +export type ApollonError = { + id: string; + headerText: string; + bodyText: string; }; -// Define the initial state as an array of errors const initialState: ApollonError[] = []; -// Create the error slice using Redux Toolkit's createSlice const errorSlice = createSlice({ name: 'error', initialState, reducers: { - // Action to display an error displayError: { reducer: (state, action: PayloadAction) => { state.push(action.payload); }, prepare: (headerText: string, bodyText: string) => ({ payload: { - id: uuidv4(), // Generate a unique ID for each error + id: uuidv4(), headerText, bodyText, }, }), }, - // Action to dismiss an error by its ID - dismissError: (state, action: PayloadAction<{ id: string }>) => { - return state.filter((error) => error.id !== action.payload.id); + + dismissError: (state, action: PayloadAction) => { + return state.filter((error) => error.id !== action.payload); }, }, }); -// Export actions to be used in components or thunks export const { displayError, dismissError } = errorSlice.actions; -// Export the reducer to be used in the Redux store export const errorReducer = errorSlice.reducer; diff --git a/packages/webapp/src/main/services/export/export-epics.ts b/packages/webapp/src/main/services/export/export-epics.ts deleted file mode 100644 index 02106ae..0000000 --- a/packages/webapp/src/main/services/export/export-epics.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { exportSVGEpic } from './svg/export-svg-epics'; -import { combineEpics } from 'redux-observable'; -import { exportPNGEpic } from './png/export-png-epics'; -import { exportJSONEpic } from './json/export-json-epics'; -import { exportPDFEpic } from './pdf/export-pdf-epics'; - -// TODO: Fix the types when library fixes it -const exportEpics = combineEpics(exportSVGEpic, exportPNGEpic, exportJSONEpic, exportPDFEpic) as any; - -export default exportEpics; diff --git a/packages/webapp/src/main/services/export/export-repository.ts b/packages/webapp/src/main/services/export/export-repository.ts deleted file mode 100644 index 735fa51..0000000 --- a/packages/webapp/src/main/services/export/export-repository.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ApollonEditor } from '@ls1intum/apollon'; -import { ExportActionTypes, ExportJSONAction, ExportPDFAction, ExportPNGAction, ExportSVGAction } from './export-types'; -import { Diagram } from '../diagram/diagram-types'; - -export const ExportRepository = { - exportAsSVG: (editor: ApollonEditor, diagramTitle: string): ExportSVGAction => ({ - type: ExportActionTypes.EXPORT_SVG, - payload: { - editor, - diagramTitle, - }, - }), - exportAsPNG: (editor: ApollonEditor, diagramTitle: string, setWhiteBackground: boolean): ExportPNGAction => ({ - type: ExportActionTypes.EXPORT_PNG, - payload: { - editor, - diagramTitle, - setWhiteBackground, - }, - }), - exportAsJSON: (editor: ApollonEditor, diagram: Diagram): ExportJSONAction => ({ - type: ExportActionTypes.EXPORT_JSON, - payload: { - editor, - diagram, - }, - }), - exportAsPDF: (editor: ApollonEditor, diagramTitle: string): ExportPDFAction => ({ - type: ExportActionTypes.EXPORT_PDF, - payload: { - editor, - diagramTitle, - }, - }), -}; diff --git a/packages/webapp/src/main/services/export/export-types.ts b/packages/webapp/src/main/services/export/export-types.ts deleted file mode 100644 index 4dbeb7f..0000000 --- a/packages/webapp/src/main/services/export/export-types.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Action } from 'redux'; -import { ApollonEditor } from '@ls1intum/apollon'; -import { Diagram } from '../diagram/diagram-types'; - -export const enum ExportActionTypes { - EXPORT_SVG = '@@export/svg', - EXPORT_PNG = '@@export/png', - EXPORT_JSON = '@@export/json', - EXPORT_PDF = '@@export/pdf', -} - -export type ExportSVGAction = Action & { - payload: { - // needs reference to ApollonEditor to perform export - editor: ApollonEditor; - diagramTitle: string; - }; -}; - -export type ExportPNGAction = Action & { - payload: { - // needs reference to ApollonEditor to perform export - editor: ApollonEditor; - diagramTitle: string; - setWhiteBackground: boolean; - }; -}; - -export type ExportJSONAction = Action & { - payload: { - // needs reference to ApollonEditor to perform export - editor: ApollonEditor; - diagram: Diagram; - }; -}; - -export type ExportPDFAction = Action & { - payload: { - // needs reference to ApollonEditor to perform export - editor: ApollonEditor; - diagramTitle: string; - }; -}; diff --git a/packages/webapp/src/main/services/export/json/export-json-epics.ts b/packages/webapp/src/main/services/export/json/export-json-epics.ts deleted file mode 100644 index 4105e3d..0000000 --- a/packages/webapp/src/main/services/export/json/export-json-epics.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Epic } from 'redux-observable'; -import { Action } from 'redux'; -import { FileDownloadAction, FileDownloadActionTypes } from '../../file-download/file-download-types'; -import { ApplicationState } from '../../../components/store/application-state'; -import { filter, map } from 'rxjs/operators'; -import { ExportActionTypes, ExportJSONAction } from '../export-types'; -import { ApollonEditor } from '@ls1intum/apollon'; -import { Diagram } from '../../diagram/diagram-types'; -import { Observable } from 'rxjs'; - -export const exportJSONEpic: Epic = ( - action$: Observable>, -) => { - return action$.pipe( - filter((action) => action.type === ExportActionTypes.EXPORT_JSON), - map((action) => action as ExportJSONAction), - map((action: ExportJSONAction) => { - const apollonEditor: ApollonEditor = action.payload.editor; - const fileName: string = `${action.payload.diagram.title}.json`; - const diagram: Diagram = { ...action.payload.diagram, model: apollonEditor.model }; - const apollonJSON: string = JSON.stringify(diagram); - const fileToDownload = new File([apollonJSON], fileName); - return { - type: FileDownloadActionTypes.FILE_DOWNLOAD, - payload: { - file: fileToDownload, - }, - }; - }), - ); -}; diff --git a/packages/webapp/src/main/services/export/pdf/export-pdf-epics.ts b/packages/webapp/src/main/services/export/pdf/export-pdf-epics.ts deleted file mode 100644 index d83cbfd..0000000 --- a/packages/webapp/src/main/services/export/pdf/export-pdf-epics.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { ApollonEditor, SVG } from '@ls1intum/apollon'; -import { Action } from 'redux'; -import { Epic, ofType } from 'redux-observable'; -import { Observable } from 'rxjs'; -import { map, switchMap } from 'rxjs/operators'; -import { ApplicationState } from '../../../components/store/application-state'; -import { StopAction, StopActionType } from '../../actions'; -import { DiagramRepository } from '../../diagram/diagram-repository'; -import { FileDownloadAction, FileDownloadActionTypes } from '../../file-download/file-download-types'; -import { ExportActionTypes, ExportPDFAction } from '../export-types'; - -export const exportPDFEpic: Epic = ( - action$: Observable>, -) => { - return action$.pipe( - ofType(ExportActionTypes.EXPORT_PDF), - map((action) => action as ExportPDFAction), - switchMap(async (action: ExportPDFAction) => { - const apollonEditor: ApollonEditor = action.payload.editor; - const filename: string = `${action.payload.diagramTitle}.pdf`; - const apollonSVG: SVG = await apollonEditor.exportAsSVG(); - const { width, height } = apollonSVG.clip; - const blob = await DiagramRepository.convertSvgToPdf(apollonSVG.svg, width, height); - - if (blob) { - const fileToDownload = new Blob([blob]); - return { - type: FileDownloadActionTypes.FILE_DOWNLOAD, - payload: { - file: fileToDownload, - filename, - }, - }; - } - - return { - type: StopActionType.STOP_ACTION, - }; - }), - ); -}; diff --git a/packages/webapp/src/main/services/export/png/export-png-epics.ts b/packages/webapp/src/main/services/export/png/export-png-epics.ts deleted file mode 100644 index a20ea15..0000000 --- a/packages/webapp/src/main/services/export/png/export-png-epics.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Epic } from 'redux-observable'; -import { Action } from 'redux'; -import { ApplicationState } from '../../../components/store/application-state'; -import { filter, map, switchMap } from 'rxjs/operators'; -import { ExportActionTypes, ExportPNGAction } from '../export-types'; -import { ApollonEditor, SVG } from '@ls1intum/apollon'; -import { FileDownloadAction, FileDownloadActionTypes } from '../../file-download/file-download-types'; -import { Observable } from 'rxjs'; - -export const exportPNGEpic: Epic = ( - action$: Observable>, -) => { - return action$.pipe( - filter((action) => action.type === ExportActionTypes.EXPORT_PNG), - map((action) => action as ExportPNGAction), - switchMap(async (action: ExportPNGAction) => { - const apollonEditor: ApollonEditor = action.payload.editor; - const fileName: string = `${action.payload.diagramTitle}.png`; - const apollonSVG: SVG = await apollonEditor.exportAsSVG(); - const setWhiteBackground: boolean = action.payload.setWhiteBackground; - const png: Blob = await convertRenderedSVGToPNG(apollonSVG, setWhiteBackground); - const fileToDownload = new File([png], fileName); - return { - type: FileDownloadActionTypes.FILE_DOWNLOAD, - payload: { - file: fileToDownload, - }, - }; - }), - ); -}; - -export function convertRenderedSVGToPNG(renderedSVG: SVG, whiteBackground: boolean): Promise { - return new Promise((resolve, reject) => { - const { width, height } = renderedSVG.clip; - - const blob = new Blob([renderedSVG.svg], { type: 'image/svg+xml' }); - const blobUrl = URL.createObjectURL(blob); - - const image = new Image(); - image.width = width; - image.height = height; - image.src = blobUrl; - - image.onload = () => { - let canvas: HTMLCanvasElement; - canvas = document.createElement('canvas'); - canvas.style.width = `${width}px`; - canvas.style.height = `${height}px`; - - const scale = 1.5; - canvas.width = width * scale; - canvas.height = height * scale; - - const context = canvas.getContext('2d')!; - - if (whiteBackground) { - context.fillStyle = 'white'; - context.fillRect(0, 0, canvas.width, canvas.height); - } - - context.scale(scale, scale); - context.drawImage(image, 0, 0); - - canvas.toBlob(resolve as BlobCallback); - }; - - image.onerror = (error) => { - reject(error); - }; - }); -} diff --git a/packages/webapp/src/main/services/export/svg/export-svg-epics.ts b/packages/webapp/src/main/services/export/svg/export-svg-epics.ts deleted file mode 100644 index 3257f12..0000000 --- a/packages/webapp/src/main/services/export/svg/export-svg-epics.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Epic } from 'redux-observable'; -import { Action } from 'redux'; -import { ApplicationState } from '../../../components/store/application-state'; -import { filter, map, switchMap } from 'rxjs/operators'; -import { ExportActionTypes, ExportSVGAction } from '../export-types'; -import { ApollonEditor, SVG } from '@ls1intum/apollon'; -import { FileDownloadAction, FileDownloadActionTypes } from '../../file-download/file-download-types'; -import { Observable } from 'rxjs'; - -export const exportSVGEpic: Epic = ( - action$: Observable>, -) => { - return action$.pipe( - filter((action) => action.type === ExportActionTypes.EXPORT_SVG), - map((action) => action as ExportSVGAction), - switchMap(async (action: ExportSVGAction) => { - const apollonEditor: ApollonEditor = action.payload.editor; - const fileName: string = `${action.payload.diagramTitle}.svg`; - const apollonSVG: SVG = await apollonEditor.exportAsSVG(); - const fileToDownload = new File([apollonSVG.svg], fileName); - return { - type: FileDownloadActionTypes.FILE_DOWNLOAD, - payload: { - file: fileToDownload, - }, - }; - }), - ); -}; diff --git a/packages/webapp/src/main/services/export/useExportJson.ts b/packages/webapp/src/main/services/export/useExportJson.ts index d40d9c8..07153d5 100644 --- a/packages/webapp/src/main/services/export/useExportJson.ts +++ b/packages/webapp/src/main/services/export/useExportJson.ts @@ -4,25 +4,19 @@ import { useFileDownload } from '../file-download/useFileDownload'; import { Diagram } from '../diagram/diagramSlice'; -export const useExportJson = () => { +export const useExportJSON = () => { const downloadFile = useFileDownload(); - const exportJson = useCallback((editor: ApollonEditor, diagram: Diagram) => { - // Prepare the file name - const fileName = `${diagram.title}.json`; + const exportJSON = useCallback((editor: ApollonEditor, diagram: Diagram) => { - // Extract the model from the ApollonEditor and merge it with the diagram data + const fileName = `${diagram.title}.json`; const diagramData: Diagram = { ...diagram, model: editor.model }; - - // Convert the diagram data to a JSON string const jsonContent = JSON.stringify(diagramData); - // Create a Blob for the JSON content const fileToDownload = new File([jsonContent], fileName, { type: 'application/json' }); - // Trigger the file download using the useFileDownload hook downloadFile({ file: fileToDownload, filename: fileName }); }, [downloadFile]); - return exportJson; + return exportJSON; }; diff --git a/packages/webapp/src/main/services/export/useExportPdf.ts b/packages/webapp/src/main/services/export/useExportPdf.ts index 03c672a..37a8890 100644 --- a/packages/webapp/src/main/services/export/useExportPdf.ts +++ b/packages/webapp/src/main/services/export/useExportPdf.ts @@ -4,26 +4,23 @@ import { useFileDownload } from '../file-download/useFileDownload'; import { DiagramRepository } from '../diagram/diagram-repository'; -export const useExportPdf = () => { +export const useExportPDF = () => { const downloadFile = useFileDownload(); - const exportPdf = useCallback( + const exportPDF = useCallback( async (editor: ApollonEditor, diagramTitle: string) => { - // Step 1: Export the diagram as SVG from the ApollonEditor - const apollonSVG = await editor.exportAsSVG(); - // Step 2: Extract the dimensions from the exported SVG + const apollonSVG = await editor.exportAsSVG(); const { width, height } = apollonSVG.clip; - // Step 3: Convert the SVG to a PDF using the DiagramRepository method const pdfBlob = await DiagramRepository.convertSvgToPdf(apollonSVG.svg, width, height); if (pdfBlob) { - // Step 4: Create a Blob and trigger file download + const filename = `${diagramTitle}.pdf`; const fileToDownload = new Blob([pdfBlob], { type: 'application/pdf' }); - // Trigger the download using the useFileDownload hook + downloadFile({ file: fileToDownload, filename }); } else { console.error('Failed to convert SVG to PDF'); @@ -32,5 +29,5 @@ export const useExportPdf = () => { [downloadFile] ); - return exportPdf; + return exportPDF; }; diff --git a/packages/webapp/src/main/services/export/useExportPng.ts b/packages/webapp/src/main/services/export/useExportPng.ts index d6a2b37..a142936 100644 --- a/packages/webapp/src/main/services/export/useExportPng.ts +++ b/packages/webapp/src/main/services/export/useExportPng.ts @@ -2,30 +2,24 @@ import { useCallback } from 'react'; import { ApollonEditor, SVG } from '@ls1intum/apollon'; import { useFileDownload } from '../file-download/useFileDownload'; -export const useExportPng = () => { +export const useExportPNG = () => { const downloadFile = useFileDownload(); - const exportPng = useCallback( + const exportPNG = useCallback( async (editor: ApollonEditor, diagramTitle: string, setWhiteBackground: boolean) => { - // Step 1: Export the diagram as SVG from the ApollonEditor - const apollonSVG: SVG = await editor.exportAsSVG(); - // Step 2: Convert the exported SVG to a PNG + const apollonSVG: SVG = await editor.exportAsSVG(); const pngBlob: Blob = await convertRenderedSVGToPNG(apollonSVG, setWhiteBackground); - - // Step 3: Create a file name for the PNG const fileName = `${diagramTitle}.png`; - - // Step 4: Create a Blob for the PNG and trigger file download + const fileToDownload = new File([pngBlob], fileName, { type: 'image/png' }); - // Trigger the download using the useFileDownload hook downloadFile({ file: fileToDownload, filename: fileName }); }, [downloadFile] ); - return exportPng; + return exportPNG; }; // Helper function to convert SVG to PNG diff --git a/packages/webapp/src/main/services/export/useExportSvg.ts b/packages/webapp/src/main/services/export/useExportSvg.ts index b79f083..fb87b85 100644 --- a/packages/webapp/src/main/services/export/useExportSvg.ts +++ b/packages/webapp/src/main/services/export/useExportSvg.ts @@ -3,25 +3,20 @@ import { ApollonEditor, SVG } from '@ls1intum/apollon'; import { useFileDownload } from '../file-download/useFileDownload'; -export const useExportSvg = () => { +export const useExportSVG = () => { const downloadFile = useFileDownload(); - const exportSvg = useCallback( + const exportSVG = useCallback( async (editor: ApollonEditor, diagramTitle: string) => { - // Step 1: Export the diagram as SVG from the ApollonEditor const apollonSVG: SVG = await editor.exportAsSVG(); - - // Step 2: Create a file name for the SVG const fileName = `${diagramTitle}.svg`; - // Step 3: Create a Blob for the SVG and trigger file download const fileToDownload = new File([apollonSVG.svg], fileName, { type: 'image/svg+xml' }); - // Trigger the download using the useFileDownload hook downloadFile({ file: fileToDownload, filename: fileName }); }, [downloadFile] ); - return exportSvg; + return exportSVG; }; diff --git a/packages/webapp/src/main/services/file-download/file-download-epics.ts b/packages/webapp/src/main/services/file-download/file-download-epics.ts deleted file mode 100644 index f1e2e79..0000000 --- a/packages/webapp/src/main/services/file-download/file-download-epics.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Epic } from 'redux-observable'; -import { Action } from 'redux'; -import { filter, map } from 'rxjs/operators'; -import { FileDownloadAction, FileDownloadActionTypes } from './file-download-types'; -import { ApplicationState } from '../../components/store/application-state'; -import { StopAction, StopActionType } from '../actions'; -import { Observable } from 'rxjs'; - -export const fileDownloadEpic: Epic = ( - action$: Observable>, -) => { - return action$.pipe( - filter((action) => action.type === FileDownloadActionTypes.FILE_DOWNLOAD), - map((action) => action as FileDownloadAction), - map((action: FileDownloadAction) => { - const file: File | Blob = action.payload.file; - // TODO: find better way to download - const link = document.createElement('a'); - link.href = window.URL.createObjectURL(file); - if (action.payload.filename) { - link.download = action.payload.filename; - } else if (file instanceof File) { - link.download = file.name; - } else { - link.download = 'file'; - } - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - return { - type: StopActionType.STOP_ACTION, - }; - }), - ); -}; diff --git a/packages/webapp/src/main/services/file-download/file-download-repository.ts b/packages/webapp/src/main/services/file-download/file-download-repository.ts deleted file mode 100644 index da63dee..0000000 --- a/packages/webapp/src/main/services/file-download/file-download-repository.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { FileDownloadAction, FileDownloadActionTypes } from './file-download-types'; - -export const FileDownloadRepository = { - downloadFile: (file: File): FileDownloadAction => ({ - type: FileDownloadActionTypes.FILE_DOWNLOAD, - payload: { - file, - }, - }), -}; diff --git a/packages/webapp/src/main/services/file-download/file-download-types.ts b/packages/webapp/src/main/services/file-download/file-download-types.ts deleted file mode 100644 index 22d3c3a..0000000 --- a/packages/webapp/src/main/services/file-download/file-download-types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Action } from 'redux'; - -export const enum FileDownloadActionTypes { - FILE_DOWNLOAD = '@@file/download', -} - -export type FileDownloadAction = Action & { - payload: { - file: File | Blob; - filename?: string; - }; -}; diff --git a/packages/webapp/src/main/services/import/import-epic.ts b/packages/webapp/src/main/services/import/import-epic.ts deleted file mode 100644 index 4570ad8..0000000 --- a/packages/webapp/src/main/services/import/import-epic.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Action } from 'redux'; -import { Epic, ofType } from 'redux-observable'; -import { Observable, of } from 'rxjs'; -import { catchError, map, mergeMap } from 'rxjs/operators'; -import { ApplicationState } from '../../components/store/application-state'; -import { uuid } from '../../utils/uuid'; -import { StopAction } from '../actions'; -import { DiagramRepository } from '../diagram/diagram-repository'; -import { Diagram, UpdateDiagramAction } from '../diagram/diagram-types'; -import { EditorOptionsRepository } from '../editor-options/editor-options-repository'; -import { ChangeDiagramTypeAction } from '../editor-options/editor-options-types'; -import { ErrorRepository } from '../error-management/error-repository'; -import { DisplayErrorAction, ErrorActionType } from '../error-management/error-types'; -import { ImportActionTypes, ImportJSONAction } from './import-types'; - -export const importEpic: Epic< - Action, - UpdateDiagramAction | ChangeDiagramTypeAction | DisplayErrorAction | StopAction, - ApplicationState -> = (action$: Observable>) => { - return action$.pipe( - ofType(ImportActionTypes.IMPORT_JSON), - map((action) => action as ImportJSONAction), - mergeMap((action: ImportJSONAction) => { - return of(action).pipe( - mergeMap((importAction: ImportJSONAction) => { - const { json } = importAction.payload; - const diagram: Diagram = JSON.parse(json); - diagram.id = uuid(); - return of( - DiagramRepository.updateDiagram({ ...diagram, ...{ diagramType: diagram.model?.type } }), - EditorOptionsRepository.changeDiagramType(diagram.model!.type), - ); - }), - catchError((error) => - of( - ErrorRepository.createError( - ErrorActionType.DISPLAY_ERROR, - 'Import failed', - 'Could not import selected file. Are you sure it contains a diagram.json?', - ) as DisplayErrorAction, - ), - ), - ); - }), - ); -}; diff --git a/packages/webapp/src/main/services/import/import-repository.ts b/packages/webapp/src/main/services/import/import-repository.ts deleted file mode 100644 index 98cde3f..0000000 --- a/packages/webapp/src/main/services/import/import-repository.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ImportActionTypes, ImportJSONAction } from './import-types'; - -export const ImportRepository = { - importJSON: (diagramJSON: string): ImportJSONAction => ({ - type: ImportActionTypes.IMPORT_JSON, - payload: { - json: diagramJSON, - }, - }), -}; diff --git a/packages/webapp/src/main/services/import/import-types.ts b/packages/webapp/src/main/services/import/import-types.ts deleted file mode 100644 index 8696b7a..0000000 --- a/packages/webapp/src/main/services/import/import-types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Action } from 'redux'; - -export type ImportActions = ImportJSONAction; - -export const enum ImportActionTypes { - IMPORT_JSON = '@@import/json', -} - -export type ImportJSONAction = Action & { - payload: { - json: string; - }; -}; diff --git a/packages/webapp/src/main/services/import/useImport.ts b/packages/webapp/src/main/services/import/useImport.ts deleted file mode 100644 index 23b0134..0000000 --- a/packages/webapp/src/main/services/import/useImport.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useCallback } from 'react'; -import { useAppDispatch } from '../../components/store/hooks'; -import { uuid } from '../../utils/uuid'; -import { Diagram, updateDiagram } from '../diagram/diagramSlice'; -import { changeDiagramType } from '../editor-options/editorOptionSlice'; - -export const useImport = () => { - const dispatch = useAppDispatch(); - - const importDiagram = useCallback((stringifiedJson: string) => { - const diagram: Diagram = JSON.parse(stringifiedJson); - diagram.id = uuid(); - - dispatch(updateDiagram(diagram)); - if (diagram.model) { - dispatch(changeDiagramType(diagram.model.type)); - } - }, []); - - return importDiagram; -}; diff --git a/packages/webapp/src/main/services/import/useImportDiagram.ts b/packages/webapp/src/main/services/import/useImportDiagram.ts new file mode 100644 index 0000000..29fc314 --- /dev/null +++ b/packages/webapp/src/main/services/import/useImportDiagram.ts @@ -0,0 +1,29 @@ +import { useCallback } from 'react'; +import { useAppDispatch } from '../../components/store/hooks'; +import { uuid } from '../../utils/uuid'; +import { Diagram, updateDiagramThunk } from '../diagram/diagramSlice'; +import { changeDiagramType } from '../editor-options/editorOptionSlice'; +import { showModal } from '../modal/modalSlice'; +import { displayError } from '../error-management/errorManagementSlice'; + +export const useImportDiagram = () => { + const dispatch = useAppDispatch(); + + const importDiagram = useCallback((stringifiedJson: string) => { + try { + const diagram: Diagram = JSON.parse(stringifiedJson); + diagram.id = uuid(); + + dispatch(updateDiagramThunk(diagram)); + if (diagram.model) { + dispatch(changeDiagramType(diagram.model.type)); + } + } catch { + dispatch( + displayError('Import failed', 'Could not import selected file. Are you sure it contains a diagram.json?'), + ); + } + }, []); + + return importDiagram; +}; diff --git a/packages/webapp/src/main/services/local-storage/local-storage-epics.ts b/packages/webapp/src/main/services/local-storage/local-storage-epics.ts deleted file mode 100644 index c5c0dc5..0000000 --- a/packages/webapp/src/main/services/local-storage/local-storage-epics.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { LoadAction, LocalStorageActionTypes, LocalStorageDiagramListItem, StoreAction } from './local-storage-types'; -import { ApplicationState } from '../../components/store/application-state'; -import { combineEpics, Epic, ofType, StateObservable } from 'redux-observable'; -import { Action } from 'redux'; -import { filter, map, mergeMap } from 'rxjs/operators'; -import { localStorageDiagramPrefix, localStorageDiagramsList, localStorageLatest } from '../../constant'; -import { StopAction, StopActionType } from '../actions'; -import moment from 'moment'; -import { Diagram, UpdateDiagramAction } from '../diagram/diagram-types'; -import { DiagramRepository } from '../diagram/diagram-repository'; -import { DisplayErrorAction, ErrorActionType } from '../error-management/error-types'; -import { ErrorRepository } from '../error-management/error-repository'; -import { Observable, of } from 'rxjs'; -import { EditorOptionsRepository } from '../editor-options/editor-options-repository'; -import { ChangeDiagramTypeAction } from '../editor-options/editor-options-types'; - -export const storeEpic: Epic = ( - action$: Observable>, - store: StateObservable, -) => { - return action$.pipe( - filter((action) => action.type === LocalStorageActionTypes.STORE), - map((action) => action as StoreAction), - map((action: StoreAction) => { - const { diagram } = action.payload; - - // save diagram and update latest diagram entry - localStorage.setItem(localStorageDiagramPrefix + diagram.id, JSON.stringify(diagram)); - localStorage.setItem(localStorageLatest, diagram.id); - - // new entry for local storage list - const type = diagram.model?.type ? diagram.model.type : store.value.editorOptions.type; - const localDiagramEntry: LocalStorageDiagramListItem = { - id: diagram.id, - title: diagram.title, - type, - lastUpdate: moment(), - }; - - // list with diagrams in local storage - const localStorageListJson = localStorage.getItem(localStorageDiagramsList); - let localDiagrams: LocalStorageDiagramListItem[]; - if (localStorageListJson) { - localDiagrams = JSON.parse(localStorageListJson); - // filter old value - localDiagrams = localDiagrams.filter((entry) => entry.id !== diagram.id); - } else { - localDiagrams = []; - } - // add new value and save - localDiagrams.push(localDiagramEntry); - localStorage.setItem(localStorageDiagramsList, JSON.stringify(localDiagrams)); - return { type: StopActionType.STOP_ACTION }; - }), - ); -}; - -export const loadDiagramEpic: Epic< - Action, - UpdateDiagramAction | ChangeDiagramTypeAction | DisplayErrorAction | StopAction, - ApplicationState -> = (action$: Observable>) => { - return action$.pipe( - ofType(LocalStorageActionTypes.LOAD), - map((action) => action as LoadAction), - mergeMap((action: LoadAction) => { - const { id } = action.payload; - const localStorageContent: string | null = window.localStorage.getItem(localStorageDiagramPrefix + id); - if (localStorageContent) { - const diagram: Diagram = JSON.parse(localStorageContent); - if (diagram.model?.type) { - return of( - DiagramRepository.updateDiagram({ ...diagram }), - EditorOptionsRepository.changeDiagramType(diagram.model?.type), - ); - } - return of(DiagramRepository.updateDiagram({ ...diagram })); - } else { - return of( - ErrorRepository.createError( - ErrorActionType.DISPLAY_ERROR, - 'Could not load diagram', - `The key for diagram with id ${id} could not be found. Maybe you deleted it from your local storage?`, - ) as DisplayErrorAction, - ); - } - }), - ); -}; - -export const localStorageEpics = combineEpics(storeEpic, loadDiagramEpic); diff --git a/packages/webapp/src/main/services/local-storage/local-storage-repository.ts b/packages/webapp/src/main/services/local-storage/local-storage-repository.ts index 61348d5..3877b5a 100644 --- a/packages/webapp/src/main/services/local-storage/local-storage-repository.ts +++ b/packages/webapp/src/main/services/local-storage/local-storage-repository.ts @@ -1,28 +1,44 @@ -import { LoadAction, LocalStorageActionTypes, LocalStorageDiagramListItem, StoreAction } from './local-storage-types'; -import { Diagram } from '../diagram/diagram-types'; +import { LocalStorageDiagramListItem } from './local-storage-types'; import { localStorageCollaborationColor, localStorageCollaborationName, + localStorageDiagramPrefix, localStorageDiagramsList, + localStorageLatest, localStorageSystemThemePreference, localStorageUserThemePreference, } from '../../constant'; import moment from 'moment'; +import { Diagram } from '../diagram/diagramSlice'; +import { UMLDiagramType, UMLModel } from '@ls1intum/apollon'; + +type LocalDiagramEntry = { + id:string, + title:string, + type: UMLDiagramType, + lastUpdate: moment.Moment +} export const LocalStorageRepository = { - load: (id: string): LoadAction => ({ - type: LocalStorageActionTypes.LOAD, - payload: { - id, - }, - }), - - store: (diagram: Diagram): StoreAction => ({ - type: LocalStorageActionTypes.STORE, - payload: { - diagram, - }, - }), + storeDiagram: (diagram: Diagram) => { + localStorage.setItem(localStorageDiagramPrefix + diagram.id, JSON.stringify(diagram)); + localStorage.setItem(localStorageLatest, diagram.id); + + const localDiagramEntry :LocalDiagramEntry= { + id: diagram.id, + title: diagram.title, + type: diagram.model?.type ?? 'ClassDiagram', + lastUpdate: moment(), + }; + + const localStorageListJson = localStorage.getItem(localStorageDiagramsList); + let localDiagrams: LocalDiagramEntry[] = localStorageListJson ? JSON.parse(localStorageListJson) : []; + + localDiagrams = localDiagrams.filter((entry) => entry.id !== diagram.id); + localDiagrams.push(localDiagramEntry); + + localStorage.setItem(localStorageDiagramsList, JSON.stringify(localDiagrams)); + }, getStoredDiagrams: () => { const localStorageDiagramList = window.localStorage.getItem(localStorageDiagramsList); diff --git a/packages/webapp/src/main/services/local-storage/local-storage-types.ts b/packages/webapp/src/main/services/local-storage/local-storage-types.ts index 12abaac..291f49b 100644 --- a/packages/webapp/src/main/services/local-storage/local-storage-types.ts +++ b/packages/webapp/src/main/services/local-storage/local-storage-types.ts @@ -1,7 +1,6 @@ import { UMLDiagramType } from '@ls1intum/apollon'; -import { Action } from 'redux'; import { Moment } from 'moment'; -import { Diagram } from '../diagram/diagram-types'; + export type LocalStorageDiagramListItem = { id: string; @@ -10,25 +9,4 @@ export type LocalStorageDiagramListItem = { lastUpdate: Moment; }; -export type LocalStorageActions = LoadAction | StoreAction; - -export const enum LocalStorageActionTypes { - LOAD = '@@local_storage/load', - LOAD_LATEST = '@@local_storage/load_latest', - STORE = '@@local_storage/store', - LIST_STORED = '@@local_storage/list_stored', -} - -export type LoadAction = Action & { - payload: { - id: string; - }; -}; - -type StorePayload = { - payload: { - diagram: Diagram; - }; -}; -export type StoreAction = Action & StorePayload; diff --git a/packages/webapp/src/main/services/local-storage/useLocalStorage.ts b/packages/webapp/src/main/services/local-storage/useLocalStorage.ts index 4a05e53..dbaae9e 100644 --- a/packages/webapp/src/main/services/local-storage/useLocalStorage.ts +++ b/packages/webapp/src/main/services/local-storage/useLocalStorage.ts @@ -2,36 +2,12 @@ import { useCallback } from 'react'; import moment from 'moment'; import { localStorageDiagramPrefix, localStorageDiagramsList, localStorageLatest } from '../../constant'; import { Diagram } from '../diagram/diagramSlice'; +import { useAppDispatch } from '../../components/store/hooks'; +import { displayError } from '../error-management/errorManagementSlice'; -// Custom hook to handle local storage operations export const useLocalStorage = () => { - // Store diagram in local storage after update - const storeDiagram = useCallback((diagram: Diagram) => { - // Save diagram and update the latest diagram entry in local storage - localStorage.setItem(localStorageDiagramPrefix + diagram.id, JSON.stringify(diagram)); - localStorage.setItem(localStorageLatest, diagram.id); + const dispatch = useAppDispatch(); - // Create a new entry for the local storage diagram list - const localDiagramEntry = { - id: diagram.id, - title: diagram.title, - type: diagram.model?.type ?? 'UMLClassDiagram', - lastUpdate: moment(), - }; - - // Get the list of diagrams from local storage - const localStorageListJson = localStorage.getItem(localStorageDiagramsList); - let localDiagrams = localStorageListJson ? JSON.parse(localStorageListJson) : []; - - // Filter out the old value and add the new one - localDiagrams = localDiagrams.filter((entry: any) => entry.id !== diagram.id); - localDiagrams.push(localDiagramEntry); - - // Save the updated diagram list in local storage - localStorage.setItem(localStorageDiagramsList, JSON.stringify(localDiagrams)); - }, []); - - // Load a diagram from local storage const loadDiagram = useCallback((id: string): Diagram | null => { const localStorageContent = localStorage.getItem(localStorageDiagramPrefix + id); if (localStorageContent) { @@ -39,12 +15,12 @@ export const useLocalStorage = () => { return diagram; } else { console.error(`The diagram with id ${id} could not be found in local storage.`); + dispatch( + displayError('Could not load diagram', `The diagram with id ${id} could not be found in local storage.`), + ); return null; } }, []); - return { - storeDiagram, - loadDiagram, - }; + return loadDiagram; }; diff --git a/packages/webapp/src/main/services/modal/modalSlice.ts b/packages/webapp/src/main/services/modal/modalSlice.ts index 0aa577b..b3c283f 100644 --- a/packages/webapp/src/main/services/modal/modalSlice.ts +++ b/packages/webapp/src/main/services/modal/modalSlice.ts @@ -6,31 +6,27 @@ export type ModalState = { size: ModalSize; }; -// Define the initial state for the modal const initialState: ModalState = { type: null, - size: 'sm', // Default size to 'sm' + size: 'sm', }; const modalSlice = createSlice({ name: 'modal', initialState, reducers: { - // Action to show the modal showModal: (state, action: PayloadAction<{ type: ModalContentType; size?: ModalSize }>) => { state.type = action.payload.type; - state.size = action.payload.size ?? 'sm'; // Default size is 'sm' if not provided + state.size = action.payload.size ?? 'sm'; }, - // Action to hide the modal + hideModal: (state) => { state.type = null; - state.size = 'sm'; // Reset size to 'sm' when modal is hidden + state.size = 'sm'; }, }, }); -// Export the actions to be used in components export const { showModal, hideModal } = modalSlice.actions; -// Export the reducer to be used in the store export const modalReducer = modalSlice.reducer; diff --git a/packages/webapp/src/main/services/share/shareSlice.ts b/packages/webapp/src/main/services/share/shareSlice.ts index 3bcdda4..1cc7b3d 100644 --- a/packages/webapp/src/main/services/share/shareSlice.ts +++ b/packages/webapp/src/main/services/share/shareSlice.ts @@ -1,7 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { Collaborator } from 'shared/src/main/collaborator-dto'; -// Define the ShareState type export type ShareState = { collaborationName: string; collaborationColor: string; @@ -9,7 +8,6 @@ export type ShareState = { fromServer: boolean; }; -// Define the initial state const initialState: ShareState = { collaborationName: '', collaborationColor: '', @@ -17,37 +15,29 @@ const initialState: ShareState = { fromServer: false, }; -// Create the share slice const shareSlice = createSlice({ name: 'share', initialState, reducers: { - // Action to update the collaboration name - updateCollaborationName(state, action: PayloadAction<{ name: string }>) { - state.collaborationName = action.payload.name; + updateCollaborationName(state, action: PayloadAction) { + state.collaborationName = action.payload; }, - // Action to update the collaboration color - updateCollaborationColor(state, action: PayloadAction<{ color: string }>) { - state.collaborationColor = action.payload.color; + + updateCollaborationColor(state, action: PayloadAction) { + state.collaborationColor = action.payload; }, - // Action to update collaborators - updateCollaborators(state, action: PayloadAction<{ collaborators: Collaborator[] }>) { - state.collaborators = action.payload.collaborators; + + updateCollaborators(state, action: PayloadAction) { + state.collaborators = action.payload; }, - // Action to set whether the data is from the server + gotFromServer(state, action: PayloadAction<{ fromServer: boolean }>) { state.fromServer = action.payload.fromServer; }, }, }); -// Export actions to be used in components or thunks -export const { - updateCollaborationName, - updateCollaborationColor, - updateCollaborators, - gotFromServer, -} = shareSlice.actions; +export const { updateCollaborationName, updateCollaborationColor, updateCollaborators, gotFromServer } = + shareSlice.actions; -// Export the reducer to be used in the store export const shareReducer = shareSlice.reducer; From 653fc508127c005566088e6b6d973e41ea694983 Mon Sep 17 00:00:00 2001 From: egenerse Date: Tue, 1 Oct 2024 10:21:37 +0200 Subject: [PATCH 03/17] feature: remove unused redux and redux-observable packages --- package-lock.json | 19 -------- packages/webapp/package.json | 2 - .../application-bar/menues/file-menu.tsx | 10 ++-- .../menues/theme-switcher-menu.tsx | 2 - packages/webapp/src/main/services/actions.ts | 23 ---------- packages/webapp/src/main/services/epics.ts | 10 ---- .../src/main/services/modal/modal-reducer.ts | 17 ------- .../main/services/modal/modal-repository.ts | 12 ----- .../src/main/services/modal/modal-types.ts | 22 --------- packages/webapp/src/main/services/reducer.ts | 16 ------- .../src/main/services/share/share-reducer.ts | 41 ----------------- .../main/services/share/share-repository.ts | 27 ----------- .../src/main/services/share/share-types.ts | 46 ------------------- 13 files changed, 6 insertions(+), 241 deletions(-) delete mode 100644 packages/webapp/src/main/services/actions.ts delete mode 100644 packages/webapp/src/main/services/epics.ts delete mode 100644 packages/webapp/src/main/services/modal/modal-reducer.ts delete mode 100644 packages/webapp/src/main/services/modal/modal-repository.ts delete mode 100644 packages/webapp/src/main/services/modal/modal-types.ts delete mode 100644 packages/webapp/src/main/services/reducer.ts delete mode 100644 packages/webapp/src/main/services/share/share-reducer.ts delete mode 100644 packages/webapp/src/main/services/share/share-repository.ts delete mode 100644 packages/webapp/src/main/services/share/share-types.ts diff --git a/package-lock.json b/package-lock.json index f9ce8f2..3bfd42f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9604,23 +9604,6 @@ "@babel/runtime": "^7.9.2" } }, - "node_modules/redux-observable": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/redux-observable/-/redux-observable-2.0.0.tgz", - "integrity": "sha512-FJz4rLXX+VmDDwZS/LpvQsKnSanDOe8UVjiLryx1g3seZiS69iLpMrcvXD5oFO7rtkPyRdo/FmTqldnT3X3m+w==", - "dependencies": { - "rxjs": "^7.0.0", - "tslib": "~2.1.0" - }, - "peerDependencies": { - "redux": ">=4 <5" - } - }, - "node_modules/redux-observable/node_modules/tslib": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", - "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" - }, "node_modules/redux-saga": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.3.0.tgz", @@ -13603,8 +13586,6 @@ "react-dom": "18.2.0", "react-redux": "8.1.3", "react-router-dom": "6.22.3", - "redux": "^4.2.1", - "redux-observable": "2.0.0", "rxjs": "7.8.1", "shared": "2.1.3", "styled-components": "6.1.8", diff --git a/packages/webapp/package.json b/packages/webapp/package.json index 8576adb..7cfc3dc 100644 --- a/packages/webapp/package.json +++ b/packages/webapp/package.json @@ -28,8 +28,6 @@ "react-dom": "18.2.0", "react-redux": "8.1.3", "react-router-dom": "6.22.3", - "redux": "^4.2.1", - "redux-observable": "2.0.0", "rxjs": "7.8.1", "shared": "2.1.3", "styled-components": "6.1.8", diff --git a/packages/webapp/src/main/components/application-bar/menues/file-menu.tsx b/packages/webapp/src/main/components/application-bar/menues/file-menu.tsx index e4789bd..f5fccea 100644 --- a/packages/webapp/src/main/components/application-bar/menues/file-menu.tsx +++ b/packages/webapp/src/main/components/application-bar/menues/file-menu.tsx @@ -2,12 +2,14 @@ import React, { useContext } from 'react'; import { Dropdown, NavDropdown } from 'react-bootstrap'; import { ApollonEditorContext } from '../../apollon-editor-component/apollon-editor-context'; import { ModalContentType } from '../../modals/application-modal-types'; -import { useExportSVG } from '../../../services/export/useExportSVG'; + import { useAppDispatch, useAppSelector } from '../../store/hooks'; -import { useExportPNG } from '../../../services/export/useExportPNG'; -import { useExportPDF } from '../../../services/export/useExportPDF'; + import { showModal } from '../../../services/modal/modalSlice'; -import { useExportJSON } from '../../../services/export/useExportJSON'; +import { useExportJSON } from '../../../services/export/useExportJson'; +import { useExportPDF } from '../../../services/export/useExportPdf'; +import { useExportPNG } from '../../../services/export/useExportPng'; +import { useExportSVG } from '../../../services/export/useExportSvg'; export const FileMenu: React.FC = () => { const apollonEditor = useContext(ApollonEditorContext); diff --git a/packages/webapp/src/main/components/application-bar/menues/theme-switcher-menu.tsx b/packages/webapp/src/main/components/application-bar/menues/theme-switcher-menu.tsx index e5a06be..129f806 100644 --- a/packages/webapp/src/main/components/application-bar/menues/theme-switcher-menu.tsx +++ b/packages/webapp/src/main/components/application-bar/menues/theme-switcher-menu.tsx @@ -1,6 +1,4 @@ import React, { useState, useEffect } from 'react'; -import { connect } from 'react-redux'; -import { ApplicationState } from '../../store/application-state'; import { setTheme, toggleTheme } from '../../../utils/theme-switcher'; import { LocalStorageRepository } from '../../../../main/services/local-storage/local-storage-repository'; import { OverlayTrigger, Tooltip } from 'react-bootstrap'; diff --git a/packages/webapp/src/main/services/actions.ts b/packages/webapp/src/main/services/actions.ts deleted file mode 100644 index 44a2b35..0000000 --- a/packages/webapp/src/main/services/actions.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { LocalStorageActions } from './local-storage/local-storage-types'; -import { Action } from 'redux'; -import { EditorOptionsActions } from './editor-options/editor-options-types'; -import { ImportActions } from './import/import-types'; -import { DiagramActions } from './diagram/diagram-types'; -import { ErrorActions } from './error-management/error-types'; -import { ModalActions } from './modal/modal-types'; -import { ShareActions } from './share/share-types'; - -export type Actions = - | LocalStorageActions - | StopAction - | EditorOptionsActions - | ImportActions - | DiagramActions - | ErrorActions - | ModalActions - | ShareActions; - -export const enum StopActionType { - STOP_ACTION = '@@stop_action', -} -export type StopAction = Action; diff --git a/packages/webapp/src/main/services/epics.ts b/packages/webapp/src/main/services/epics.ts deleted file mode 100644 index 1742aa9..0000000 --- a/packages/webapp/src/main/services/epics.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { combineEpics } from 'redux-observable'; -import { localStorageEpics } from './local-storage/local-storage-epics'; -import exportEpics from './export/export-epics'; -import { fileDownloadEpic } from './file-download/file-download-epics'; -import { importEpic } from './import/import-epic'; -import { diagramEpics } from './diagram/diagram-epics'; - -const epics = combineEpics(fileDownloadEpic, localStorageEpics, exportEpics, importEpic, diagramEpics); - -export default epics; diff --git a/packages/webapp/src/main/services/modal/modal-reducer.ts b/packages/webapp/src/main/services/modal/modal-reducer.ts deleted file mode 100644 index 32bbbd8..0000000 --- a/packages/webapp/src/main/services/modal/modal-reducer.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Reducer } from 'redux'; -import { Actions } from '../actions'; -import { ModalActionType, ModalState } from './modal-types'; - -export const ModalReducer: Reducer = (state: ModalState = { type: null, size: 'sm' }, action) => { - switch (action.type) { - case ModalActionType.SHOW_MODAL: { - const { payload } = action; - return payload; - } - case ModalActionType.HIDE_MODAL: { - return { type: null, size: 'sm' }; - } - } - - return state; -}; diff --git a/packages/webapp/src/main/services/modal/modal-repository.ts b/packages/webapp/src/main/services/modal/modal-repository.ts deleted file mode 100644 index 13fa8c5..0000000 --- a/packages/webapp/src/main/services/modal/modal-repository.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { HideModalAction, ModalActionType, ShowModalAction } from './modal-types'; -import { ModalContentType, ModalSize } from '../../components/modals/application-modal-types'; - -export const ModalRepository = { - showModal: (type: ModalContentType, size: ModalSize = undefined): ShowModalAction => ({ - type: ModalActionType.SHOW_MODAL, - payload: { type, size }, - }), - hideModal: (): HideModalAction => ({ - type: ModalActionType.HIDE_MODAL, - }), -}; diff --git a/packages/webapp/src/main/services/modal/modal-types.ts b/packages/webapp/src/main/services/modal/modal-types.ts deleted file mode 100644 index 3927b86..0000000 --- a/packages/webapp/src/main/services/modal/modal-types.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Action } from 'redux'; -import { ModalContentType, ModalSize } from '../../components/modals/application-modal-types'; - -export type ModalState = { - type: ModalContentType | null; - size: ModalSize; -}; - -export type ModalActions = ShowModalAction | HideModalAction; - -export const enum ModalActionType { - SHOW_MODAL = '@@modal/show', - HIDE_MODAL = '@@modal/hide', -} - -export type ShowModalAction = Action & { - payload: { - type: ModalContentType; - size: ModalSize; - }; -}; -export type HideModalAction = Action; diff --git a/packages/webapp/src/main/services/reducer.ts b/packages/webapp/src/main/services/reducer.ts deleted file mode 100644 index 5dbccce..0000000 --- a/packages/webapp/src/main/services/reducer.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Actions } from './actions'; -import { ApplicationState } from '../components/store/application-state'; -import { ReducersMapObject } from 'redux'; -import { EditorOptionsReducer } from './editor-options/editor-options-reducer'; -import { ErrorReducer } from './error-management/error-reducer'; -import { ModalReducer } from './modal/modal-reducer'; -import { ShareReducer } from './share/share-reducer'; -import { diagramReducer } from './diagram/diagramSlice'; - -export const reducers = { - diagram: diagramReducer, - editorOptions: EditorOptionsReducer, - errors: ErrorReducer, - modal: ModalReducer, - share: ShareReducer, -}; diff --git a/packages/webapp/src/main/services/share/share-reducer.ts b/packages/webapp/src/main/services/share/share-reducer.ts deleted file mode 100644 index dd3701f..0000000 --- a/packages/webapp/src/main/services/share/share-reducer.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Reducer } from 'redux'; -import { Actions } from '../actions'; -import { ShareActionTypes, ShareState } from './share-types'; - -export const ShareReducer: Reducer = ( - state = { collaborationName: '', collaborationColor: '', collaborators: [], fromServer: false }, - action, -) => { - switch (action.type) { - case ShareActionTypes.UPDATE_COLLABORATION_NAME: { - const { payload } = action; - return { - ...state, - collaborationName: payload.name, - } as ShareState; - } - case ShareActionTypes.UPDATE_COLLABORATION_COLOR: { - const { payload } = action; - return { - ...state, - collaborationColor: payload.color, - } as ShareState; - } - case ShareActionTypes.UPDATE_COLLABORATORS: { - const { payload } = action; - return { - ...state, - collaborators: payload.collaborators, - } as ShareState; - } - case ShareActionTypes.GOT_FROM_SERVER: { - const { payload } = action; - return { - ...state, - fromServer: payload.fromServer, - } as ShareState; - } - } - - return state; -}; diff --git a/packages/webapp/src/main/services/share/share-repository.ts b/packages/webapp/src/main/services/share/share-repository.ts deleted file mode 100644 index 011c037..0000000 --- a/packages/webapp/src/main/services/share/share-repository.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { - GotFromServerAction, - ShareActionTypes, - UpdateCollaborationColorAction, - UpdateCollaborationNameAction, - UpdateCollaboratorsAction, -} from './share-types'; -import { Collaborator } from 'shared/src/main/collaborator-dto'; - -export const ShareRepository = { - updateCollaborationName: (name: string): UpdateCollaborationNameAction => ({ - type: ShareActionTypes.UPDATE_COLLABORATION_NAME, - payload: { name }, - }), - updateCollaborationColor: (color: string): UpdateCollaborationColorAction => ({ - type: ShareActionTypes.UPDATE_COLLABORATION_COLOR, - payload: { color }, - }), - updateCollaborators: (collaborators: Collaborator[]): UpdateCollaboratorsAction => ({ - type: ShareActionTypes.UPDATE_COLLABORATORS, - payload: { collaborators }, - }), - gotFromServer: (fromServer: boolean): GotFromServerAction => ({ - type: ShareActionTypes.GOT_FROM_SERVER, - payload: { fromServer }, - }), -}; diff --git a/packages/webapp/src/main/services/share/share-types.ts b/packages/webapp/src/main/services/share/share-types.ts deleted file mode 100644 index a1cbac2..0000000 --- a/packages/webapp/src/main/services/share/share-types.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Action } from 'redux'; -import { Collaborator } from 'shared/src/main/collaborator-dto'; - -export type ShareActions = - | UpdateCollaborationNameAction - | UpdateCollaboratorsAction - | GotFromServerAction - | UpdateCollaborationColorAction; - -export type ShareState = { - collaborationName: string; - collaborationColor: string; - collaborators: Collaborator[]; - fromServer: boolean; -}; - -export const enum ShareActionTypes { - UPDATE_COLLABORATION_NAME = '@@share/update_collaboration_name', - UPDATE_COLLABORATION_COLOR = '@@share/update_collaboration_color', - UPDATE_COLLABORATORS = '@@share/update_collaborators', - GOT_FROM_SERVER = '@@share/got_from_server', -} - -export type UpdateCollaborationNameAction = Action & { - payload: { - name: string; - }; -}; - -export type UpdateCollaborationColorAction = Action & { - payload: { - color: string; - }; -}; - -export type UpdateCollaboratorsAction = Action & { - payload: { - collaborators: Collaborator[]; - }; -}; - -export type GotFromServerAction = Action & { - payload: { - fromServer: boolean; - }; -}; From acd15982dd9559518c5efdcce92d8a050bd6515a Mon Sep 17 00:00:00 2001 From: egenerse Date: Thu, 3 Oct 2024 22:14:32 +0200 Subject: [PATCH 04/17] feat: convert modals functional component --- .../application-bar/application-bar.tsx | 64 ++-- .../modals/application-modal-content.ts | 5 +- .../components/modals/application-modal.tsx | 15 +- .../collaboration-modal.tsx | 145 ++------ .../create-from-template-modal.tsx | 193 +++++------ ...create-from-software-pattern-modal-tab.tsx | 105 +++--- .../create-diagram-modal.tsx | 149 +++----- .../help-modeling-modal.tsx | 148 ++++---- .../import-diagram-modal.tsx | 125 +++---- .../information-modal/information-modal.tsx | 100 +++--- .../load-diagram-modal/load-diagram-modal.tsx | 19 +- .../modals/share-modal/share-modal.tsx | 323 ++++++++---------- .../components/store/application-store.tsx | 6 +- .../src/main/services/diagram/diagramSlice.ts | 1 - .../main/utils/collaboration-message-type.ts | 2 +- 15 files changed, 556 insertions(+), 844 deletions(-) diff --git a/packages/webapp/src/main/components/application-bar/application-bar.tsx b/packages/webapp/src/main/components/application-bar/application-bar.tsx index 13df700..7aeaa99 100644 --- a/packages/webapp/src/main/components/application-bar/application-bar.tsx +++ b/packages/webapp/src/main/components/application-bar/application-bar.tsx @@ -29,67 +29,59 @@ const ApplicationVersion = styled.span` export const ApplicationBar: React.FC = () => { const dispatch = useAppDispatch(); - const { diagram } = useAppSelector((state) => state.diagram); const collaborators = useAppSelector((state) => state.share.collaborators); - const [diagramTitle, setDiagramTitle] = useState(diagram?.title || ''); - useEffect(() => { if (diagram?.title) { setDiagramTitle(diagram.title); } }, [diagram?.title]); - const changeDiagramTitlePreview = (event: ChangeEvent) => { setDiagramTitle(event.target.value); }; - const changeDiagramTitleApplicationState = () => { if (diagram) { dispatch(updateDiagramThunk({ title: diagramTitle })); } }; - const handleOpenModal = () => { dispatch(showModal({ type: ModalContentType.ShareModal, size: 'lg' })); }; return ( - <> - - - {' '} - Apollon - - {appVersion} - - - - - - - - + + + {' '} + Apollon + + {appVersion} + + + + + + + ); }; diff --git a/packages/webapp/src/main/components/modals/application-modal-content.ts b/packages/webapp/src/main/components/modals/application-modal-content.ts index 1a4c193..a7cb1b0 100644 --- a/packages/webapp/src/main/components/modals/application-modal-content.ts +++ b/packages/webapp/src/main/components/modals/application-modal-content.ts @@ -1,5 +1,4 @@ -import { ComponentType } from 'react'; -import { ModalContentType } from './application-modal-types'; +import { ModalContentProps, ModalContentType } from './application-modal-types'; import { HelpModelingModal } from './help-modeling-modal/help-modeling-modal'; import { ImportDiagramModal } from './import-diagram-modal/import-diagram-modal'; import { InformationModal } from './information-modal/information-modal'; @@ -9,7 +8,7 @@ import { CreateFromTemplateModal } from './create-diagram-from-template-modal/cr import { ShareModal } from './share-modal/share-modal'; import { CollaborationModal } from './collaboration-modal/collaboration-modal'; -export const ApplicationModalContent: { [key in ModalContentType]: ComponentType } = { +export const ApplicationModalContent: { [key in ModalContentType]: React.FC } = { [ModalContentType.HelpModelingModal]: HelpModelingModal, [ModalContentType.ImportDiagramModal]: ImportDiagramModal, [ModalContentType.InformationModal]: InformationModal, diff --git a/packages/webapp/src/main/components/modals/application-modal.tsx b/packages/webapp/src/main/components/modals/application-modal.tsx index fe9af9b..5010e00 100644 --- a/packages/webapp/src/main/components/modals/application-modal.tsx +++ b/packages/webapp/src/main/components/modals/application-modal.tsx @@ -1,16 +1,15 @@ import React, { useState } from 'react'; import { createPortal } from 'react-dom'; import { Modal } from 'react-bootstrap'; -import { useDispatch, useSelector } from 'react-redux'; -import { ApplicationState } from '../store/application-state'; import { ApplicationModalContent } from './application-modal-content'; -import { ModalRepository } from '../../services/modal/modal-repository'; +import { useAppDispatch, useAppSelector } from '../store/hooks'; +import { hideModal } from '../../services/modal/modalSlice'; export const ApplicationModal = () => { const [isClosable, setIsClosable] = useState(true); - const displayModal = useSelector((state: ApplicationState) => state.modal.type); - const modalSize = useSelector((state: ApplicationState) => state.modal.size); - const dispatch = useDispatch(); + const displayModal = useAppSelector((state) => state.modal.type); + const modalSize = useAppSelector((state) => state.modal.size); + const dispatch = useAppDispatch(); const onClosableChange = (closable: boolean) => { setIsClosable(closable); @@ -18,12 +17,12 @@ export const ApplicationModal = () => { const handleClose = () => { if (isClosable) { - dispatch(ModalRepository.hideModal()); + dispatch(hideModal()); } }; const closeModal = () => { - dispatch(ModalRepository.hideModal()); + dispatch(hideModal()); }; if (!displayModal) { diff --git a/packages/webapp/src/main/components/modals/collaboration-modal/collaboration-modal.tsx b/packages/webapp/src/main/components/modals/collaboration-modal/collaboration-modal.tsx index 90dde99..7e14c54 100644 --- a/packages/webapp/src/main/components/modals/collaboration-modal/collaboration-modal.tsx +++ b/packages/webapp/src/main/components/modals/collaboration-modal/collaboration-modal.tsx @@ -1,119 +1,46 @@ -import React, { ChangeEvent, Component, MouseEvent } from 'react'; +import React, { ChangeEvent, MouseEvent, useRef, useState } from 'react'; import { Button, FormControl, InputGroup, Modal } from 'react-bootstrap'; -import { connect } from 'react-redux'; -import { ErrorRepository } from '../../../services/error-management/error-repository'; +import { useDispatch } from 'react-redux'; import { LocalStorageRepository } from '../../../services/local-storage/local-storage-repository'; -import { ShareRepository } from '../../../services/share/share-repository'; import { generateRandomName } from '../../../utils/random-name-generator/random-name-generator'; -import { ApplicationState } from '../../store/application-state'; -import { ModalContentProps } from '../application-modal-types'; +import { updateCollaborationColor, updateCollaborationName } from '../../../services/share/shareSlice'; -type OwnProps = {} & ModalContentProps; +export const CollaborationModal: React.FC = () => { + const dispatch = useDispatch(); -type StateProps = { - collaborationName: string; -}; - -type DispatchProps = { - createError: typeof ErrorRepository.createError; - updateCollaborationName: typeof ShareRepository.updateCollaborationName; - updateCollaborationColor: typeof ShareRepository.updateCollaborationColor; -}; - -type Props = OwnProps & StateProps & DispatchProps; + const [name, setName] = useState(generateRandomName()); + const [color] = useState('#' + Math.floor(Math.random() * 16777215).toString(16)); -const enhance = connect( - (state) => { - return { - collaborationName: state.share.collaborationName, - collaborationColor: state.share.collaborationColor, - }; - }, - { - createError: ErrorRepository.createError, - updateCollaborationName: ShareRepository.updateCollaborationName, - updateCollaborationColor: ShareRepository.updateCollaborationColor, - }, -); - -const getInitialState = () => { - return { - name: generateRandomName(), - color: '#' + Math.floor(Math.random() * 16777215).toString(16), + const handleChange = (e: ChangeEvent) => { + setName(e.currentTarget.value); }; -}; - -type State = typeof getInitialState; - -class CollaborationModalComponent extends Component { - innerRef: React.RefObject; - constructor(props: Props) { - super(props); - this.innerRef = React.createRef(); - } - state = getInitialState(); - componentDidMount() { - const collaborationName = this.props.collaborationName; - if (!collaborationName) { - this.props.onClosableChange?.(false); - } else { - this.setState({ name: collaborationName }); - } - setTimeout(() => { - this.innerRef.current?.focus(); - }, 1); - } - - handleClose = () => { - if (this.state.name) { - this.props.close(); - this.setState(getInitialState()); - } - }; - - handleChange = (e: ChangeEvent) => { - this.setState({ - name: e.currentTarget.value, - }); + const setCollaborationNameAndColor = (e: MouseEvent) => { + LocalStorageRepository.setCollaborationName(name); + LocalStorageRepository.setCollaborationColor(color); + dispatch(updateCollaborationName(name)); + dispatch(updateCollaborationColor(color)); + close(); }; - setCollaborationNameAndColor = (e: MouseEvent) => { - LocalStorageRepository.setCollaborationName(this.state.name); - LocalStorageRepository.setCollaborationColor(this.state.color); - this.props.updateCollaborationName(this.state.name); - this.props.updateCollaborationColor(this.state.color); - this.props.close(); - }; - - render() { - return ( - <> - - Collaboration Name - - - Please enter your name to highlight elements you are interacting with for other collaborators. - - - <> - - - - - - - - ); - } -} - -export const CollaborationModal = enhance(CollaborationModalComponent); + return ( + <> + + Collaboration Name + + + Please enter your name to highlight elements you are interacting with for other collaborators. + + + <> + + + + + + + + ); +}; diff --git a/packages/webapp/src/main/components/modals/create-diagram-from-template-modal/create-from-template-modal.tsx b/packages/webapp/src/main/components/modals/create-diagram-from-template-modal/create-from-template-modal.tsx index 7db365d..07a51ac 100644 --- a/packages/webapp/src/main/components/modals/create-diagram-from-template-modal/create-from-template-modal.tsx +++ b/packages/webapp/src/main/components/modals/create-diagram-from-template-modal/create-from-template-modal.tsx @@ -1,131 +1,90 @@ -import React, { Component, ComponentClass } from 'react'; -import { compose } from 'redux'; -import { withApollonEditor } from '../../apollon-editor-component/with-apollon-editor'; -import { connect } from 'react-redux'; -import { ApplicationState } from '../../store/application-state'; +import React, { useState } from 'react'; import { Button, Col, FormControl, InputGroup, Modal, Nav, Row, Tab } from 'react-bootstrap'; -import { DiagramRepository } from '../../../services/diagram/diagram-repository'; import { Template, TemplateCategory } from './template-types'; -import { SoftwarePatternTemplate, SoftwarePatternType } from './software-pattern/software-pattern-types'; +import { SoftwarePatternType } from './software-pattern/software-pattern-types'; import { CreateFromSoftwarePatternModalTab } from './software-pattern/create-from-software-pattern-modal-tab'; import { TemplateFactory } from './template-factory'; import { ModalContentProps } from '../application-modal-types'; +import { useAppDispatch } from '../../store/hooks'; +import { createDiagram } from '../../../services/diagram/diagramSlice'; -type OwnProps = {} & ModalContentProps; +export const CreateFromTemplateModal: React.FC = ({ close }) => { + const [selectedTemplate, setSelectedTemplate] = useState