Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/ Major Refactor, Feature Enhancements, and UI/UX Improvements #99

Merged
merged 30 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0b4e99a
feature: slices created
egenerse Oct 1, 2024
c75e7ec
feature: refactor whole the frontend
egenerse Oct 1, 2024
653fc50
feature: remove unused redux and redux-observable packages
egenerse Oct 1, 2024
acd1598
feat: convert modals functional component
egenerse Oct 3, 2024
fc0fdb7
fix: remove console.logs set new title after changing the diagram type
egenerse Oct 3, 2024
d724739
feat: convert lastUpdate value from moment isntance to ISO date string
egenerse Oct 6, 2024
7837689
fix creating new editor for when user creates new diagram
egenerse Oct 6, 2024
073a339
create 2 different component alone and collaboration
egenerse Oct 13, 2024
077dafc
feat: separtion of diagrams and double click handle in create new dia…
egenerse Oct 15, 2024
f3f39ee
fix: render collaboration
egenerse Oct 15, 2024
c160894
fix: render editor from token
egenerse Oct 15, 2024
adb85f8
fix: subscripte to editor patchers are not working
egenerse Oct 17, 2024
6f98e97
fix: collaborator name modal handle
egenerse Oct 18, 2024
3160340
fix: fixing collaboration
egenerse Oct 18, 2024
0abe5a5
feat: use .nvmrc in the project for node version
egenerse Oct 18, 2024
19c7db4
fix: selection fix
egenerse Oct 18, 2024
f339591
fix: rerender issue while changing sgared view version
egenerse Oct 18, 2024
e137040
feat: new apollon version is used
egenerse Oct 22, 2024
b8f5d41
Merge branch 'main' into feature/redux-toolkit-migration
egenerse Oct 22, 2024
b915658
fix: modal input handle
egenerse Oct 22, 2024
5798bca
chore: npm run prettier:write
egenerse Oct 22, 2024
8e68a13
Merge branch 'main' into feature/redux-toolkit-migration
egenerse Oct 22, 2024
f497f1e
fix: hide yourself
egenerse Oct 22, 2024
cd56db0
fix: hide user label
egenerse Oct 22, 2024
47b84e3
fix: return to initial path if there is no diagram
egenerse Oct 22, 2024
e603751
fix: diagram not found handled
egenerse Oct 22, 2024
61b68d8
fix: navigate to route in load diagram
egenerse Oct 22, 2024
cdb242c
fix: import diagram in collaboration fix
egenerse Oct 22, 2024
5f148aa
fix: set initial modal redux state
egenerse Oct 22, 2024
3616e62
fix: remove ds and commented code
egenerse Oct 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,4 @@ build
/.idea/

/diagrams/
.DS_Store
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v20.18.0
7,231 changes: 3,469 additions & 3,762 deletions package-lock.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { DiagramStorageRateLimiter, DiagramStorageRequest } from './diagram-stor

type SaveRequest = DiagramStorageRequest & {
path: string;
}
};

/**
* Service for storing diagrams on the file system.
Expand Down Expand Up @@ -39,7 +39,7 @@ export class DiagramFileStorageService implements DiagramStorageService {
/**
* The rate limiter for saving diagrams.
*/
private limiter: DiagramStorageRateLimiter<SaveRequest>
private limiter: DiagramStorageRateLimiter<SaveRequest>;

constructor() {
this.limiter = new DiagramStorageRateLimiter<SaveRequest>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import { applyPatchToRedisValue } from './redis-patch';

type SaveRequest = DiagramStorageRequest & {
key: string;
}

};

/**
* Options for the Redis storage service.
Expand All @@ -32,7 +31,6 @@ export interface RedisStorageOptions {
ttl?: string;
}


/**
* Diagram storage service that uses Redis as a storage backend.
*/
Expand Down Expand Up @@ -121,7 +119,7 @@ export class DiagramRedisStorageService implements DiagramStorageService {
const key = this.getKeyForToken(token);
const client = await this.redisClient;

return await client.exists(key) > 0;
return (await client.exists(key)) > 0;
}

async getDiagramByLink(token: string): Promise<DiagramDTO | undefined> {
Expand All @@ -147,7 +145,7 @@ export class DiagramRedisStorageService implements DiagramStorageService {
await client.expire(key, ms(this.options.ttl) / 1000);
}
}

/**
* Returns the redis key to use for a diagram with given token.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { DiagramDTO } from 'shared/src/main/diagram-dto';
import { debounceTime, from, groupBy, mergeMap, Observable, Subject, switchMap } from 'rxjs';
import { auditDebounceTime } from 'audit-debounce';


/**
* Request for saving or patching a diagram.
*/
Expand Down Expand Up @@ -78,15 +77,17 @@ export interface DiagramStorageRateLimiterConfig {
* Denotes a function that can save a diagram. Return an observable when each operation
* can be cancelled, so that the limiter cancels pending operations before initiating a new one.
*/
export type SaveDiagramFunction<T extends DiagramStorageRequest> =
(request: T & DiagramSaveRequest) => Promise<void> | Observable<void>;
export type SaveDiagramFunction<T extends DiagramStorageRequest> = (
request: T & DiagramSaveRequest,
) => Promise<void> | Observable<void>;

/**
* Denotes a function that can patch a diagram. Return an observable when each operation
* can be cancelled, so that the limiter cancels pending operations before initiating a new one.
*/
export type PatchDiagramFunction<T extends DiagramStorageRequest> =
(request: T & DiagramPatchRequest) => Promise<void> | Observable<void>;
export type PatchDiagramFunction<T extends DiagramStorageRequest> = (
request: T & DiagramPatchRequest,
) => Promise<void> | Observable<void>;

/**
* A rate limiter for saving diagrams. This limiter ensures that persistence requests
Expand All @@ -113,7 +114,7 @@ export class DiagramStorageRateLimiter<T extends DiagramStorageRequest> {
constructor(
saveDiagram: SaveDiagramFunction<T>,
patchDiagram: PatchDiagramFunction<T>,
readonly config: DiagramStorageRateLimiterConfig
readonly config: DiagramStorageRateLimiterConfig,
) {
//
// I do realize complex rxjs pipelines are not the easiest to read.
Expand Down Expand Up @@ -180,7 +181,7 @@ export class DiagramStorageRateLimiter<T extends DiagramStorageRequest> {
* Request to save or patch a diagram. The limiter will
* schedule this operation according to previous and subsequent requests,
* ensuring that the storage system is not overloaded.
*/
*/
request(request: T) {
this.router.next(request);
}
Expand Down
1 change: 0 additions & 1 deletion packages/server/src/main/services/diagram-storage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { DiagramStorageService } from './diagram-storage-service';
import { DiagramRedisStorageService } from './diagram-redis-storage-service';
import { DiagramFileStorageService } from './diagram-file-storage-service';


/**
* Factory for creating a diagram storage service. Will determine
* the correct service to use based on the environment.
Expand Down
12 changes: 4 additions & 8 deletions packages/server/src/main/services/diagram-storage/redis-patch.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import { RedisClientType } from 'redis';
import { Operation } from 'fast-json-patch';


/**
* Converts a [JSONPointer](https://tools.ietf.org/html/rfc6901) to a Redis JSON path.
* JSONPointer is the standard format for addressing various parts of a JSON document used by
* [JSONPatch](https://jsonpatch.com).
* @param jsonPointer
* @returns
* @param jsonPointer
* @returns
*/
export function convertJSONPointerToRedisJSONPath(jsonPointer: string): string {
return jsonPointer
.replace(/\//g, '.')
.replace(/~1/g, '/')
.replace(/~0/g, '~');
return jsonPointer.replace(/\//g, '.').replace(/~1/g, '/').replace(/~0/g, '~');
}

/**
Expand All @@ -31,7 +27,7 @@ export async function applyPatchToRedisValue(client: RedisClientType, key: strin
switch (operation.op) {
case 'add':
case 'replace':
await client.json.set(key, path, operation.value);
await client.json.set(key, path, operation.value);
break;
case 'remove':
await client.json.del(key, path);
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"main": "index.js",
"license": "MIT",
"dependencies": {
"@ls1intum/apollon": "3.3.14",
"@ls1intum/apollon": "3.3.15",
"moment": "2.30.1",
"ws": "8.17.1"
},
Expand Down
5 changes: 2 additions & 3 deletions packages/shared/src/main/diagram-dto.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { UMLModel } from '@ls1intum/apollon';
import { Moment } from 'moment';

export class DiagramDTO {
id: string;
title: string;
model: UMLModel;
lastUpdate: Moment;
lastUpdate: string;

constructor(id: string, title: string, model: UMLModel, lastUpdate: Moment) {
constructor(id: string, title: string, model: UMLModel, lastUpdate: string) {
this.id = id;
this.title = title;
this.model = model;
Expand Down
8 changes: 4 additions & 4 deletions packages/webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"node": ">=18.17.0"
},
"dependencies": {
"@ls1intum/apollon": "3.3.14",
"@ls1intum/apollon": "3.3.15",
"@reduxjs/toolkit": "2.2.7",
"@sentry/react": "7.110.1",
"bootstrap": "5.3.3",
"moment": "2.30.1",
Expand All @@ -27,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",
Expand All @@ -50,9 +49,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",
Expand Down
108 changes: 25 additions & 83 deletions packages/webapp/src/main/application.tsx
Original file line number Diff line number Diff line change
@@ -1,104 +1,46 @@
import React, { useState } from 'react';
import React, { useMemo, 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/ApollonEditorComponent';
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';
import {
ApollonEditorContext,
ApollonEditorProvider,
} from './components/apollon-editor-component/apollon-editor-context';
import { uuid } from './utils/uuid';
import moment from 'moment';
import { POSTHOG_HOST, POSTHOG_KEY } from './constant';
import { ApollonEditorProvider } from './components/apollon-editor-component/apollon-editor-context';
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';
import { ApollonEditorComponentWithConnection } from './components/apollon-editor-component/ApollonEditorComponentWithConnection';

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 = () => {
export function RoutedApplication() {
const [editor, setEditor] = useState<ApollonEditor>();
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 = useMemo(() => /Firefox/i.test(navigator.userAgent), []);

return (
<PostHogProvider apiKey={POSTHOG_KEY} options={postHogOptions}>
<ApollonEditorProvider value={context}>
<ApplicationStore initialState={initialStore}>
<ApplicationBar />
<ApplicationModal />
{isFirefox && <FirefoxIncompatibilityHint />}
<ErrorPanel />
<ApollonEditorWrapper />
</ApplicationStore>
<ToastContainer />
</ApollonEditorProvider>
<ApplicationStore>
<BrowserRouter>
<ApollonEditorProvider value={{ editor, setEditor: handleSetEditor }}>
<ApplicationBar />
<ApplicationModal />
{isFirefox && <FirefoxIncompatibilityHint />}
<Routes>
<Route path={'/:token'} element={<ApollonEditorComponentWithConnection />} />
<Route path={'/'} element={<ApollonEditorComponent />} />
</Routes>
<ErrorPanel />
<ToastContainer />
</ApollonEditorProvider>
</BrowserRouter>
</ApplicationStore>
</PostHogProvider>
);
};

export function RoutedApplication() {
return (
<BrowserRouter>
<Routes>
<Route path={'/:token'} element={<Application />} />
<Route path={'/'} element={<Application />} />
</Routes>
</BrowserRouter>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { ApollonEditor, UMLModel } from '@ls1intum/apollon';
import React, { useEffect, useRef, useContext } from 'react';
import styled from 'styled-components';
import { uuid } from '../../utils/uuid';

import { setCreateNewEditor, updateDiagramThunk, selectCreatenewEditor } from '../../services/diagram/diagramSlice';
import { ApollonEditorContext } from './apollon-editor-context';
import { useAppDispatch, useAppSelector } from '../store/hooks';

const ApollonContainer = styled.div`
display: flex;
flex-direction: column;
flex-grow: 2;
overflow: hidden;
`;

export const ApollonEditorComponent: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<ApollonEditor | null>(null);
const dispatch = useAppDispatch();
const { diagram: reduxDiagram } = useAppSelector((state) => state.diagram);
const options = useAppSelector((state) => state.diagram.editorOptions);
const createNewEditor = useAppSelector(selectCreatenewEditor);
const editorContext = useContext(ApollonEditorContext);
const setEditor = editorContext?.setEditor;

useEffect(() => {
const initializeEditor = async () => {
if (containerRef.current != null && createNewEditor) {
editorRef.current = new ApollonEditor(containerRef.current, options);
await editorRef.current?.nextRender;

if (reduxDiagram.model) {
editorRef.current.model = reduxDiagram.model;
}
editorRef.current.subscribeToModelChange((model: UMLModel) => {
const diagram = { ...reduxDiagram, model };
dispatch(updateDiagramThunk(diagram));
});

setEditor!(editorRef.current);
dispatch(setCreateNewEditor(false));
}
};

initializeEditor();
}, [containerRef.current, createNewEditor]);

const key = reduxDiagram?.id || uuid();

return <ApollonContainer key={key} ref={containerRef} />;
};
Loading