Skip to content

Commit 2c0e5d7

Browse files
author
Auto Version Bump
committed
feat: create api and websocket managers
1 parent e771189 commit 2c0e5d7

File tree

5 files changed

+253
-197
lines changed

5 files changed

+253
-197
lines changed

standalone/server/src/models/Diagram.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
/* eslint-disable @typescript-eslint/no-explicit-any */
21
import mongoose, { Document, Schema } from "mongoose"
32

43
export interface IDiagram extends Document {
54
_id: string // Custom ID from library
65
version: string
76
title: string
87
type: string
9-
nodes: any[] // JSON array for nodes
10-
edges: any[] // JSON array for edges
11-
assessments: Record<string, any> // Optional JSON object for assessments
8+
nodes: Schema.Types.Mixed[] // JSON array for nodes
9+
edges: Schema.Types.Mixed[] // JSON array for edges
10+
assessments: Record<string, Schema.Types.Mixed> // Optional JSON object for assessments
1211
}
1312

1413
const diagramSchema: Schema = new Schema(
@@ -23,6 +22,15 @@ const diagramSchema: Schema = new Schema(
2322
},
2423
{
2524
minimize: false, // Disable minimization of the document
25+
versionKey: false,
26+
timestamps: true, // Automatically manage createdAt and updatedAt fields
27+
toJSON: {
28+
virtuals: true, // expose `id`
29+
transform: (_doc, ret) => {
30+
ret.id = ret._id // copy _id to id
31+
delete ret._id // optional: remove _id
32+
},
33+
},
2634
}
2735
)
2836

standalone/webapp/src/components/modals/ShareModal.tsx

Lines changed: 17 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { toast } from "react-toastify"
55
import { useEditorContext, useModalContext } from "@/contexts"
66
import { useNavigate } from "react-router"
77
import { DiagramView } from "@/types"
8-
import { backendURL } from "@/constants"
8+
import { DiagramAPIManager } from "@/services/DiagramAPIManager"
99

1010
export const ShareModal = () => {
1111
const { editor } = useEditorContext()
@@ -17,36 +17,26 @@ export const ShareModal = () => {
1717
toast.error("Editor instance is not available.")
1818
return
1919
}
20-
const model = editor.model
2120

22-
await fetch(`${backendURL}/api/`, {
23-
method: "POST",
24-
headers: { "Content-Type": "application/json" },
25-
body: JSON.stringify(model),
26-
})
27-
.then(async (res) => {
28-
if (res.ok) {
29-
const diagramID = (await res.json())._id
21+
try {
22+
const model = editor.model
23+
const { id: diagramID } = await DiagramAPIManager.createDiagram(model)
3024

31-
const newurl = `${window.location.origin}/${diagramID}?view=${viewType}`
32-
copyToClipboard(newurl)
33-
navigate(`/${diagramID}?view=${viewType}`)
25+
const newurl = `${window.location.origin}/${diagramID}?view=${viewType}`
26+
copyToClipboard(newurl)
27+
navigate(`/${diagramID}?view=${viewType}`)
3428

35-
toast.success(
36-
`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.`,
37-
{
38-
autoClose: 10000,
39-
}
40-
)
41-
closeModal()
42-
} else {
43-
throw new Error("Network response was not ok")
29+
toast.success(
30+
`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.`,
31+
{
32+
autoClose: 10000,
4433
}
45-
})
46-
.catch((err) => {
47-
console.error("Error in setDiagram endpoint:", err)
48-
toast.error("Error in setDiagram endpoint:", err)
49-
})
34+
)
35+
closeModal()
36+
} catch (err) {
37+
console.error("Error creating diagram:", err)
38+
toast.error("Could not create diagram.")
39+
}
5040
}
5141

5242
const copyToClipboard = (link: string) => {

standalone/webapp/src/pages/ApollonWithConnection.tsx

Lines changed: 72 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,11 @@
1-
/* eslint-disable @typescript-eslint/no-explicit-any */
21
import React, { useEffect, useRef, useState } from "react"
32
import { useEditorContext } from "@/contexts"
4-
import {
5-
ApollonEditor,
6-
ApollonMode,
7-
ApollonOptions,
8-
UMLModel,
9-
} from "@tumaet/apollon"
3+
import { ApollonEditor, ApollonMode, ApollonOptions } from "@tumaet/apollon"
104
import { useNavigate, useParams, useSearchParams } from "react-router"
115
import { toast } from "react-toastify"
12-
import { backendURL, backendWSSUrl } from "@/constants"
13-
import { DiagramView, WebSocketMessage } from "@/types"
14-
15-
const fetchDiagramData = (diagramId: string): Promise<any> => {
16-
return fetch(`${backendURL}/api/${diagramId}`, {
17-
method: "GET",
18-
headers: {
19-
"Content-Type": "application/json",
20-
},
21-
}).then((res) => {
22-
if (res.ok) {
23-
return res.json()
24-
} else {
25-
throw new Error("Failed to fetch diagram data")
26-
}
27-
})
28-
}
29-
30-
const sendPutRequest = async (diagramId: string, data: UMLModel) => {
31-
try {
32-
const response = await fetch(`${backendURL}/api/${diagramId}`, {
33-
method: "PUT",
34-
headers: {
35-
"Content-Type": "application/json",
36-
},
37-
body: JSON.stringify(data),
38-
})
39-
if (!response.ok) {
40-
throw new Error("Failed to send PUT request")
41-
}
42-
} catch (error) {
43-
console.error("Error in PUT request:", error)
44-
toast.error("Failed to sync diagram data")
45-
}
46-
}
6+
import { DiagramView } from "@/types"
7+
import { WebSocketManager } from "@/services/WebSocketManager"
8+
import { DiagramAPIManager } from "@/services/DiagramAPIManager"
479

4810
export const ApollonWithConnection: React.FC = () => {
4911
const { diagramId } = useParams()
@@ -52,149 +14,93 @@ export const ApollonWithConnection: React.FC = () => {
5214
const { setEditor } = useEditorContext()
5315
const [isLoading, setIsLoading] = useState(true)
5416
const containerRef = useRef<HTMLDivElement | null>(null)
55-
const websocketRef = useRef<WebSocket | null>(null)
56-
const intervalRef = useRef<NodeJS.Timeout | null>(null) // Ref to store interval ID
17+
const wsManagerRef = useRef<WebSocketManager | null>(null)
18+
const syncIntervalRef = useRef<NodeJS.Timeout | null>(null)
5719
const diagramIsUpdated = useRef(false)
5820

5921
useEffect(() => {
6022
let instance: ApollonEditor | null = null
61-
if (containerRef.current && diagramId) {
62-
const initializeApollon = async () => {
63-
try {
64-
const viewType = searchParams.get("view")
65-
const validViewTypes: string[] = [
66-
DiagramView.COLLABORATE,
67-
DiagramView.GIVE_FEEDBACK,
68-
DiagramView.SEE_FEEDBACK,
69-
DiagramView.EDIT,
70-
]
71-
const isValidView = viewType && validViewTypes.includes(viewType)
7223

73-
if (!isValidView) {
74-
toast.error("Invalid view type")
75-
navigate("/")
76-
return
77-
}
78-
79-
const diagram = await fetchDiagramData(diagramId)
24+
const initialize = async () => {
25+
if (!containerRef.current || !diagramId) return
8026

81-
const editorOptions: ApollonOptions = {
82-
model: diagram,
83-
}
84-
85-
if (viewType === DiagramView.GIVE_FEEDBACK) {
86-
editorOptions.mode = ApollonMode.Assessment
87-
editorOptions.readonly = false
88-
} else if (viewType === DiagramView.SEE_FEEDBACK) {
89-
editorOptions.mode = ApollonMode.Assessment
90-
editorOptions.readonly = true
91-
} else if (viewType === DiagramView.EDIT) {
92-
editorOptions.mode = ApollonMode.Modelling
93-
editorOptions.readonly = false
94-
} else {
95-
editorOptions.mode = ApollonMode.Modelling
96-
editorOptions.readonly = false
97-
}
98-
99-
instance = new ApollonEditor(containerRef.current!, editorOptions)
100-
setEditor(instance)
101-
setIsLoading(false)
102-
103-
const makeConnection =
104-
viewType === DiagramView.COLLABORATE ||
105-
viewType === DiagramView.GIVE_FEEDBACK ||
106-
viewType === DiagramView.SEE_FEEDBACK
27+
try {
28+
const viewType = searchParams.get("view")
29+
const validViews = Object.values(DiagramView)
30+
if (!viewType || !validViews.includes(viewType as DiagramView)) {
31+
toast.error("Invalid view type")
32+
navigate("/")
33+
return
34+
}
35+
console.log("Initializing Apollon editor with view type:", viewType)
10736

108-
if (makeConnection) {
109-
// Set up the WebSocket connection
110-
websocketRef.current = new WebSocket(
111-
`${backendWSSUrl}?diagramId=${diagramId}`
112-
)
37+
const diagram = await DiagramAPIManager.fetchDiagramData(diagramId)
38+
console.log("Fetched diagram data:", diagram)
11339

114-
// Handle incoming Yjs updates
115-
websocketRef.current.onmessage = (event: MessageEvent<string>) => {
116-
const receiveWebSocketMessage = JSON.parse(
117-
event.data
118-
) as WebSocketMessage
119-
instance?.receiveBroadcastedMessage(
120-
receiveWebSocketMessage.diagramData
121-
)
122-
}
40+
const editorOptions: ApollonOptions = {
41+
model: diagram,
42+
}
12343

124-
// Wait until socket is open before starting sync
125-
websocketRef.current.onopen = () => {
126-
instance?.sendBroadcastMessage((diagramData) => {
127-
if (websocketRef.current?.readyState === WebSocket.OPEN) {
128-
const sendData = { diagramData }
129-
websocketRef.current.send(JSON.stringify(sendData))
130-
} else {
131-
console.warn("Tried to send while WebSocket not open")
132-
}
133-
})
44+
if (viewType === DiagramView.GIVE_FEEDBACK) {
45+
editorOptions.mode = ApollonMode.Assessment
46+
editorOptions.readonly = false
47+
} else if (viewType === DiagramView.SEE_FEEDBACK) {
48+
editorOptions.mode = ApollonMode.Assessment
49+
editorOptions.readonly = true
50+
} else if (viewType === DiagramView.EDIT) {
51+
editorOptions.mode = ApollonMode.Modelling
52+
editorOptions.readonly = false
53+
} else {
54+
editorOptions.mode = ApollonMode.Modelling
55+
editorOptions.readonly = false
56+
}
13457

135-
const initialSyncMessage =
136-
ApollonEditor.generateInitialSyncMessage()
137-
const initialMessage = JSON.stringify({
138-
diagramData: initialSyncMessage,
139-
})
140-
websocketRef.current?.send(initialMessage)
141-
}
58+
instance = new ApollonEditor(containerRef.current!, editorOptions)
59+
setEditor(instance)
60+
setIsLoading(false)
14261

143-
websocketRef.current.onerror = (err) => {
144-
console.error("WebSocket error", err)
145-
toast.error("WebSocket connection error")
146-
}
62+
if (
63+
[
64+
DiagramView.COLLABORATE,
65+
DiagramView.GIVE_FEEDBACK,
66+
DiagramView.SEE_FEEDBACK,
67+
].includes(viewType as DiagramView)
68+
) {
69+
wsManagerRef.current = new WebSocketManager(diagramId, instance, () =>
70+
toast.error("WebSocket error")
71+
)
72+
wsManagerRef.current.startConnection()
73+
}
14774

148-
intervalRef.current = setInterval(() => {
149-
if (instance && diagramId && diagramIsUpdated.current) {
150-
const diagramData = instance.model
151-
sendPutRequest(diagramId, diagramData)
152-
diagramIsUpdated.current = false
153-
}
154-
}, 5000)
75+
syncIntervalRef.current = setInterval(() => {
76+
if (diagramIsUpdated.current && diagramId) {
77+
DiagramAPIManager.sendDiagramUpdate(
78+
diagramId,
79+
instance!.model
80+
).catch(() => toast.error("Failed to sync changes"))
81+
diagramIsUpdated.current = false
15582
}
156-
157-
instance.subscribeToModelChange(() => {
158-
diagramIsUpdated.current = true
159-
})
160-
161-
// Return cleanup function for Apollon2 and WebSocket
162-
} catch (error) {
163-
toast.error("Error loading diagram. Please try again.")
164-
navigate("/")
165-
console.error("Error setting up Apollon2:", error)
166-
}
83+
}, 5000)
84+
85+
instance.subscribeToModelChange(() => {
86+
diagramIsUpdated.current = true
87+
})
88+
} catch {
89+
toast.error("Failed to initialize diagram")
90+
navigate("/")
16791
}
168-
169-
initializeApollon()
17092
}
17193

172-
return () => {
173-
setEditor(undefined) // Clear context
174-
175-
// Clear interval if it exists
176-
if (intervalRef.current) {
177-
clearInterval(intervalRef.current)
178-
intervalRef.current = null
179-
}
94+
initialize()
18095

181-
// Clean up WebSocket
182-
if (
183-
websocketRef.current &&
184-
websocketRef.current.readyState === WebSocket.OPEN
185-
) {
186-
console.log("Clearing WebSocket connection")
187-
websocketRef.current.close()
188-
websocketRef.current = null
189-
}
190-
191-
if (instance) {
192-
instance.destroy()
193-
instance = null
96+
return () => {
97+
setEditor(undefined)
98+
wsManagerRef.current?.cleanup()
99+
if (syncIntervalRef.current) {
100+
clearInterval(syncIntervalRef.current)
194101
}
102+
instance?.destroy()
195103
}
196-
197-
// Implicitly return undefined if conditions are not met
198104
}, [diagramId, searchParams, setEditor])
199105

200106
return (

0 commit comments

Comments
 (0)