From 12af28c1c910ece00afaf4e054bd9ff18b147cb0 Mon Sep 17 00:00:00 2001 From: Guilherme_F Date: Fri, 17 May 2024 18:58:55 +0100 Subject: [PATCH] History Editor fixes * Added richer logging --- code/client/src/App.tsx | 98 ++++++++++--------- .../handlers/history/toHistoryOperations.ts | 47 ++++++--- .../plugins/markdown/rendering/renderers.tsx | 10 +- .../domain/editor/slate/utils/selection.ts | 7 +- .../client/src/ui/pages/document/Document.tsx | 4 +- .../document/components/editor/Editor.tsx | 2 +- code/client/src/utils/logging.ts | 9 ++ .../controllers/http/handlers/errorHandler.ts | 2 +- .../ws/events/document/onCursorChange.ts | 4 +- .../ws/events/document/onLeaveDocument.ts | 7 +- .../ws/events/document/onOperation.ts | 2 +- .../ws/events/workspace/onLeaveWorkspace.ts | 4 + .../src/ts/controllers/ws/initSocketEvents.ts | 19 +++- .../ts/controllers/ws/rooms/roomOperations.ts | 6 +- .../src/ts/controllers/ws/rooms/rooms.ts | 11 ++- .../postgres/PostgresWorkspacesDB.ts | 1 - code/server/src/ts/server.ts | 9 +- code/server/src/ts/utils/logging.ts | 12 +++ code/shared/package.json | 2 +- code/shared/src/utils/logging.ts | 37 +++++++ 20 files changed, 203 insertions(+), 90 deletions(-) create mode 100644 code/client/src/utils/logging.ts create mode 100644 code/server/src/ts/utils/logging.ts create mode 100644 code/shared/src/utils/logging.ts diff --git a/code/client/src/App.tsx b/code/client/src/App.tsx index b4f2fea4..1d7adec1 100644 --- a/code/client/src/App.tsx +++ b/code/client/src/App.tsx @@ -1,5 +1,4 @@ import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; -import { CommunicationProvider } from '@ui/contexts/communication/CommunicationContext.tsx'; import Document from '@ui/pages/document/Document'; import Header from '@ui/components/header/Header'; import Workspace from '@ui/pages/workspace/Workspace'; @@ -8,53 +7,60 @@ import './App.scss'; import { ErrorProvider } from '@ui/contexts/error/ErrorContext'; import Sidebar from '@ui/components/sidebar/Sidebar'; import { WorkspaceProvider } from '@ui/contexts/workspace/WorkspaceContext'; -import Home from '@ui/pages/home/Home.tsx'; +import Home from "@ui/pages/home/Home"; +import {ClientLogCaller} from "@/utils/logging"; +import getLogger from '@notespace/shared/src/utils/logging'; +import {CommunicationProvider} from "@ui/contexts/communication/CommunicationContext"; + + +const logger = getLogger(ClientLogCaller.React); function App() { - return ( -
- - - -
-
- - } /> - - - - - - - } - /> - - - - - } - /> - - - } - /> - } /> - -
- - - -
- ); + logger.logSuccess('App started'); + return ( +
+ + + +
+
+ + } /> + + + + + + + } + /> + + + + + } + /> + + + } + /> + } /> + +
+ + + +
+ ); } export default App; diff --git a/code/client/src/domain/editor/slate/handlers/history/toHistoryOperations.ts b/code/client/src/domain/editor/slate/handlers/history/toHistoryOperations.ts index edb49761..5a4cd416 100644 --- a/code/client/src/domain/editor/slate/handlers/history/toHistoryOperations.ts +++ b/code/client/src/domain/editor/slate/handlers/history/toHistoryOperations.ts @@ -9,8 +9,8 @@ import { BaseSetNodeOperation, BaseSplitNodeOperation, Editor, - Range, Element, + Range, Text, } from 'slate'; import { @@ -24,7 +24,7 @@ import { SplitNodeOperation, UnsetNodeOperation, } from '@domain/editor/operations/history/types'; -import { pointToCursor } from '@domain/editor/slate/utils/selection'; +import {pointToCursor} from '@domain/editor/slate/utils/selection'; const reverseTypes: { [key: string]: HistoryOperation['type'] } = { insert_text: 'remove_text', @@ -69,9 +69,9 @@ function toHistoryOperations(editor: Editor, operations: Batch | undefined, reve case 'remove_text': return removeTextOperation(operation as BaseRemoveTextOperation); case 'insert_node': - return nodeOperation(operation as BaseInsertNodeOperation, true); + return nodeOperation(operation as BaseInsertNodeOperation, selectionBefore, true); case 'remove_node': - return nodeOperation(operation as BaseRemoveNodeOperation, false); + return nodeOperation(operation as BaseRemoveNodeOperation, selectionBefore, false); case 'merge_node': return handleNodeOperation(operation as BaseMergeNodeOperation, selectionBefore?.anchor.offset, true); case 'split_node': @@ -104,15 +104,15 @@ function toHistoryOperations(editor: Editor, operations: Batch | undefined, reve if (operation.text === '') return undefined; - const cursor = pointToCursor(editor, { path: operation.path, offset: 0 }); + const cursor = pointToCursor(editor, { path: operation.path, offset: operation.offset }); const start = { line: operation.path[0], - column: cursor.column + operation.offset, + column: cursor.column + offset(cursor.line), }; const end = { line: start.line, - column: start.column + operation.text.length - 1 + offset(start.line), + column: start.column + operation.text.length, }; const selection = { start, end }; @@ -122,23 +122,45 @@ function toHistoryOperations(editor: Editor, operations: Batch | undefined, reve /** * Handles a slate insert or remove node operation * @param operation + * @param selectionBefore * @param insert_mode */ function nodeOperation( operation: BaseInsertNodeOperation | BaseRemoveNodeOperation, + selectionBefore: BaseRange | null, insert_mode: boolean ): InsertNodeOperation | RemoveNodeOperation | undefined { + const lineOffset = (line: number) => (line === 0 ? 0 : 1); + + // Remove whole line + if(operation.path.length === 1) { + const start = pointToCursor(editor, {path: operation.path, offset: 0}); + const end = pointToCursor(editor, {path: [operation.path[0] + 1, 0], offset: 0}); + + const selection = { start, end }; + return { + type: insert_mode ? 'insert_node' : 'remove_node', + selection, + node: operation.node, + }; + } + if (!Text.isText(operation.node)) return; if (operation.node.text === '') return undefined; - const offset = (line: number) => (line === 0 ? 0 : 1); + if(!selectionBefore) return undefined - const start = pointToCursor(editor, { path: operation.path, offset: 0 }); + const cursor = pointToCursor(editor, selectionBefore.anchor); + + const start = { + ...cursor, + column: cursor.column + lineOffset(cursor.line), + }; const end = { ...start, - column: start.column + operation.node.text.length - 1 + offset(start.line), + column: start.column + operation.node.text.length, }; const selection = { start, end }; @@ -161,9 +183,8 @@ function toHistoryOperations(editor: Editor, operations: Batch | undefined, reve offset: number | undefined, merge_mode: boolean ): MergeNodeOperation | SplitNodeOperation | undefined { - if (!Element.isElement(operation.properties)) return undefined; - - if (!operation.properties.type) return undefined; + if (operation.path.length > 1) return undefined; + if (!(operation.properties as Element).type) return undefined; return merge_mode ? { diff --git a/code/client/src/domain/editor/slate/plugins/markdown/rendering/renderers.tsx b/code/client/src/domain/editor/slate/plugins/markdown/rendering/renderers.tsx index 126324fc..bdd1ffd5 100644 --- a/code/client/src/domain/editor/slate/plugins/markdown/rendering/renderers.tsx +++ b/code/client/src/domain/editor/slate/plugins/markdown/rendering/renderers.tsx @@ -52,13 +52,11 @@ export const getLeafRenderer = ({ attributes, leaf, children }: RenderLeafProps) if (!renderer) continue; children = renderer(children); } - if (leaf.cursor) { + if(leaf.cursor){ const { color, range, styles } = leaf.cursor; - children = Range.isCollapsed(range!) ? ( - - ) : ( - - ); + children = Range.isCollapsed(range!) + ? + : } return {children}; }; diff --git a/code/client/src/domain/editor/slate/utils/selection.ts b/code/client/src/domain/editor/slate/utils/selection.ts index 0f5c4f34..9729be10 100644 --- a/code/client/src/domain/editor/slate/utils/selection.ts +++ b/code/client/src/domain/editor/slate/utils/selection.ts @@ -41,11 +41,13 @@ const pointsToSelection = (editor: Editor, start: Point, end: Point): Selection */ export function pointToCursor(editor: Editor, point: Point): Cursor { const line = point.path[0]; - const children = Node.children(editor, [line]); + const children = Array.from(Node.children(editor, [line])); const cursor: Cursor = { line, column: point.offset }; + for (const entry of children) { // If path has only one element, and it is the same as the first element of the point path - same line if (point.path.length === 1 && point.path[0] === entry[1][0]) break; + // Else verify if the path is the same if (Path.equals(entry[1], point.path)) break; @@ -54,6 +56,9 @@ export function pointToCursor(editor: Editor, point: Point): Cursor { cursor.column += text.text.length; } } + + + return cursor; } diff --git a/code/client/src/ui/pages/document/Document.tsx b/code/client/src/ui/pages/document/Document.tsx index cf902b82..8895fe00 100644 --- a/code/client/src/ui/pages/document/Document.tsx +++ b/code/client/src/ui/pages/document/Document.tsx @@ -2,10 +2,10 @@ import Editor from '@ui/pages/document/components/editor/Editor'; import useFugue from '@domain/editor/crdt/useFugue'; import { useEffect, useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { useCommunication } from '@ui/contexts/communication/useCommunication.ts'; +import { useCommunication } from '@ui/contexts/communication/useCommunication'; import useError from '@ui/contexts/error/useError'; import useDocumentService from '@services/resource/useResourceService'; -import { DocumentResource } from '@notespace/shared/src/workspace/types/resource.ts'; +import { DocumentResource } from '@notespace/shared/src/workspace/types/resource'; import './Document.scss'; function Document() { diff --git a/code/client/src/ui/pages/document/components/editor/Editor.tsx b/code/client/src/ui/pages/document/components/editor/Editor.tsx index 18a25f95..8e8f2cdf 100644 --- a/code/client/src/ui/pages/document/components/editor/Editor.tsx +++ b/code/client/src/ui/pages/document/components/editor/Editor.tsx @@ -54,7 +54,7 @@ function Editor({ title, fugue, communication }: SlateEditorProps) { return (
- + onSelectionChange()}> <Toolbar onApplyMark={onFormat} /> <Editable diff --git a/code/client/src/utils/logging.ts b/code/client/src/utils/logging.ts new file mode 100644 index 00000000..1988aca9 --- /dev/null +++ b/code/client/src/utils/logging.ts @@ -0,0 +1,9 @@ +import {ColorWrap, LogColor} from '@notespace/shared/src/utils/logging'; + + +export const ClientLogCaller = { + React: ColorWrap(LogColor.Blue, 'React'), + Services: ColorWrap(LogColor.Yellow, 'Services'), + Domain: ColorWrap(LogColor.Green, 'Domain'), + PWA: ColorWrap(LogColor.Red, 'PWA'), +} \ No newline at end of file diff --git a/code/server/src/ts/controllers/http/handlers/errorHandler.ts b/code/server/src/ts/controllers/http/handlers/errorHandler.ts index 7c414d36..a702edf1 100644 --- a/code/server/src/ts/controllers/http/handlers/errorHandler.ts +++ b/code/server/src/ts/controllers/http/handlers/errorHandler.ts @@ -20,5 +20,5 @@ export default function errorHandler(error: Error, req: Request, res: Response, } const message = response.statusCode === 500 ? 'Internal server error' : error.message; response.send({ error: message }); - console.error(error.stack); + //console.error(error.stack); } diff --git a/code/server/src/ts/controllers/ws/events/document/onCursorChange.ts b/code/server/src/ts/controllers/ws/events/document/onCursorChange.ts index b3df35fd..05785052 100644 --- a/code/server/src/ts/controllers/ws/events/document/onCursorChange.ts +++ b/code/server/src/ts/controllers/ws/events/document/onCursorChange.ts @@ -11,7 +11,7 @@ const cursorColorsMap = new Map<string, string>(); function onCursorChange() { return (socket: Socket, range: any) => { - const documentId = rooms.document.get(socket)?.id; + const documentId = rooms.document.get(socket.id)?.id; if (!documentId) return; if (!range) { deleteCursor(socket, documentId); @@ -21,7 +21,7 @@ function onCursorChange() { }; } -function deleteCursor(socket: Socket, documentId: string) { +export function deleteCursor(socket: Socket, documentId: string) { cursorColorsMap.delete(socket.id); socket.broadcast.to(documentId).emit('cursorChange', { id: socket.id }); } diff --git a/code/server/src/ts/controllers/ws/events/document/onLeaveDocument.ts b/code/server/src/ts/controllers/ws/events/document/onLeaveDocument.ts index 2fdbd7df..a44b1d4f 100644 --- a/code/server/src/ts/controllers/ws/events/document/onLeaveDocument.ts +++ b/code/server/src/ts/controllers/ws/events/document/onLeaveDocument.ts @@ -1,10 +1,15 @@ import { Socket } from 'socket.io'; import rooms from '@controllers/ws/rooms/rooms'; +import {deleteCursor} from "@controllers/ws/events/document/onCursorChange"; function onLeaveDocument() { return function (socket: Socket) { + + const documentId = rooms.document.get(socket.id)?.id; + if (!documentId) return; + deleteCursor(socket, documentId); // Done so that the cursor is removed when the user leaves the document rooms.document.leave(socket); }; } -export default onLeaveDocument; +export default onLeaveDocument; \ No newline at end of file diff --git a/code/server/src/ts/controllers/ws/events/document/onOperation.ts b/code/server/src/ts/controllers/ws/events/document/onOperation.ts index 4e16af8c..e0ff95df 100644 --- a/code/server/src/ts/controllers/ws/events/document/onOperation.ts +++ b/code/server/src/ts/controllers/ws/events/document/onOperation.ts @@ -9,7 +9,7 @@ function onOperation(service: DocumentsService) { return async (socket: Socket, operations: Operation[]) => { if (!operations) throw new InvalidParameterError('Operations are required'); - const { id, wid } = rooms.document.get(socket); + const { id, wid } = rooms.document.get(socket.id); if (!id) throw new ForbiddenError('Client not in a room'); socket.broadcast.to(id).emit('operations', operations); diff --git a/code/server/src/ts/controllers/ws/events/workspace/onLeaveWorkspace.ts b/code/server/src/ts/controllers/ws/events/workspace/onLeaveWorkspace.ts index e76ce873..1a295025 100644 --- a/code/server/src/ts/controllers/ws/events/workspace/onLeaveWorkspace.ts +++ b/code/server/src/ts/controllers/ws/events/workspace/onLeaveWorkspace.ts @@ -1,8 +1,12 @@ import { Socket } from 'socket.io'; import rooms from '@controllers/ws/rooms/rooms'; +import {deleteCursor} from "@controllers/ws/events/document/onCursorChange"; function onLeaveWorkspace() { return function (socket: Socket) { + const documentId = rooms.document.get(socket.id)?.id; + if (!documentId) return; + deleteCursor(socket, documentId); // Done so that the cursor is removed when the user leaves the document rooms.workspace.leave(socket); }; } diff --git a/code/server/src/ts/controllers/ws/initSocketEvents.ts b/code/server/src/ts/controllers/ws/initSocketEvents.ts index 8ce77f9a..77f406fe 100644 --- a/code/server/src/ts/controllers/ws/initSocketEvents.ts +++ b/code/server/src/ts/controllers/ws/initSocketEvents.ts @@ -1,25 +1,34 @@ import { SocketHandler } from '@controllers/ws/types'; import { Socket } from 'socket.io'; +import {ServiceLogCaller} from "@src/utils/logging"; +import getLogger, {ColorWrap, LogColor} from "@notespace/shared/src/utils/logging" + +const logger = getLogger( + ServiceLogCaller.Controllers + + "-" + ColorWrap(LogColor.Red, "ws") +) + export default function initSocketEvents(events: Record<string, SocketHandler>) { // const onCursorChange = events['cursorChange']; return async (socket: Socket) => { - console.log('a client connected'); + logger.logInfo('Client connected:' + socket.id); Object.entries(events).forEach(([event, handler]) => { socket.on(event, async data => { try { - console.log(event); + logger.logInfo("Event: " + event + "| Data: " + JSON.stringify(data)); await handler(socket, data); - } catch (e) { - console.error(e); + } catch (e : any) { + logger.logError(e) + socket.emit('error', e.message); } }); }); socket.on('disconnect', reason => { // onCursorChange(socket, null); // remove cursor - console.log('a client disconnected', reason); + logger.logInfo('Client disconnected: ' + reason); }); }; } diff --git a/code/server/src/ts/controllers/ws/rooms/roomOperations.ts b/code/server/src/ts/controllers/ws/rooms/roomOperations.ts index d20b00cd..ce3846e5 100644 --- a/code/server/src/ts/controllers/ws/rooms/roomOperations.ts +++ b/code/server/src/ts/controllers/ws/rooms/roomOperations.ts @@ -9,15 +9,15 @@ export function joinRoom(rooms: Map<string, Room>, socket: Socket, id: string) { } export function leaveRoom(rooms: Map<string, Room>, socket: Socket) { - const room = getRoom(rooms, socket); + const room = getRoom(rooms, socket.id); if (!room) return; socket.leave(room.id); room.leave(socket.id); } -export function getRoom(rooms: Map<string, Room>, socket: Socket): Room | null { +export function getRoom(rooms: Map<string, Room>, socketId : string): Room | null { for (const room of rooms.values()) { - if (room.has(socket.id)) return room; + if (room.has(socketId)) return room; } return null; } diff --git a/code/server/src/ts/controllers/ws/rooms/rooms.ts b/code/server/src/ts/controllers/ws/rooms/rooms.ts index c9ada4a7..ebc88dc9 100644 --- a/code/server/src/ts/controllers/ws/rooms/rooms.ts +++ b/code/server/src/ts/controllers/ws/rooms/rooms.ts @@ -15,10 +15,11 @@ function leaveDocument(socket: Socket) { leaveRoom(documentRooms, socket); } -function getDocument(socket: Socket) { - const room = getRoom(documentRooms, socket); +function getDocument(socketId : string) { + const room = getRoom(documentRooms, socketId); + //console.log("Rooms: ", documentRooms, workspaceRooms, room, socketId) if (!room) throw new ForbiddenError('Client not in a document'); - const workspace = getWorkspace(socket); + const workspace = getWorkspace(socketId); if (!workspace) throw new ForbiddenError('Client not in a workspace'); return { id: room.id, wid: workspace.id }; } @@ -35,8 +36,8 @@ function leaveWorkspace(socket: Socket) { leaveRoom(workspaceRooms, socket); } -function getWorkspace(socket: Socket) { - const room = getRoom(workspaceRooms, socket); +function getWorkspace(socketId : string) { + const room = getRoom(workspaceRooms, socketId); if (!room) throw new ForbiddenError('Client not in a workspace'); return { id: room.id }; } diff --git a/code/server/src/ts/databases/postgres/PostgresWorkspacesDB.ts b/code/server/src/ts/databases/postgres/PostgresWorkspacesDB.ts index 53a1c9c3..14d5fdba 100644 --- a/code/server/src/ts/databases/postgres/PostgresWorkspacesDB.ts +++ b/code/server/src/ts/databases/postgres/PostgresWorkspacesDB.ts @@ -52,7 +52,6 @@ export class PostgresWorkspacesDB implements WorkspacesRepository { `; const entries = results.map(entry => [entry.id, entry]); - console.log('Resources:', Object.fromEntries(entries)); return Object.fromEntries(entries); } } diff --git a/code/server/src/ts/server.ts b/code/server/src/ts/server.ts index f86a40c1..e0e5dd26 100644 --- a/code/server/src/ts/server.ts +++ b/code/server/src/ts/server.ts @@ -9,6 +9,12 @@ import config from '@src/config'; import initSocketEvents from '@controllers/ws/initSocketEvents'; import { DocumentsService } from '@services/DocumentsService'; import { TestDatabases } from '@databases/TestDatabases'; +import {ServiceLogCaller} from '@src/utils/logging'; +import getLogger from '@notespace/shared/src/utils/logging'; +import {Runtime} from "firebase-admin/extensions"; + +const logger = getLogger(ServiceLogCaller.Server); +logger.logWarning('Starting server...'); // databases const databases = new TestDatabases(); @@ -23,6 +29,7 @@ const server = http.createServer(app); const io = new Server(server, config.SERVER_OPTIONS); const api = router(services, io); +// setup middleware app.use(cors({ origin: config.ORIGIN })); app.use(express.json()); app.use('/', api); @@ -33,5 +40,5 @@ const socketEvents = initSocketEvents(events); io.on('connection', socketEvents); server.listen(config.SERVER_PORT, config.SERVER_IP, () => { - console.log(`listening on http://${config.SERVER_IP}:${config.SERVER_PORT}`); + logger.logSuccess(`listening on http://${config.SERVER_IP}:${config.SERVER_PORT}`); }); diff --git a/code/server/src/ts/utils/logging.ts b/code/server/src/ts/utils/logging.ts new file mode 100644 index 00000000..233f4166 --- /dev/null +++ b/code/server/src/ts/utils/logging.ts @@ -0,0 +1,12 @@ +import {ColorWrap, LogColor} from "@notespace/shared/src/utils/logging"; + + +export const ServiceLogCaller = { + Server: ColorWrap(LogColor.Blue, 'Server'), + Database: ColorWrap(LogColor.Green, 'Database'), + Services: ColorWrap(LogColor.Yellow, 'Services'), + Controllers: ColorWrap(LogColor.Red, 'Controllers'), +} + + + diff --git a/code/shared/package.json b/code/shared/package.json index d3ee232f..8171bd49 100644 --- a/code/shared/package.json +++ b/code/shared/package.json @@ -1,6 +1,6 @@ { "name": "@notespace/shared", - "version": "0.2.1", + "version": "0.2.2", "description": "shared code", "author": "notespace-team", "scripts": { diff --git a/code/shared/src/utils/logging.ts b/code/shared/src/utils/logging.ts new file mode 100644 index 00000000..4ad7ec5b --- /dev/null +++ b/code/shared/src/utils/logging.ts @@ -0,0 +1,37 @@ +export enum LogColor { + Red = '\x1b[31m', + Green = '\x1b[32m', + Yellow = '\x1b[33m', + Blue = '\x1b[34m', + Reset = '\x1b[0m', +} + +export const ColorWrap = (color : LogColor, message: string) => color + message + LogColor.Reset; + +const colorLog = (caller : string, message : string, color : LogColor) => + log(caller, color + message + LogColor.Reset); + +const log = (caller : string, message : string) => console.log(`[${caller}] ${message}`); + +const logWarning = (caller : string, message : string, ) => + colorLog(caller, "⚠ " + message, LogColor.Yellow); + +const logError = (caller : string, message : string) => + colorLog(caller, "✖ " + message, LogColor.Red); + +const logSuccess = (caller : string, message : string) => + colorLog(caller, "✔ " + message, LogColor.Green); + +const logInfo = (caller : string, message : string) => + colorLog(caller,"🛈" + message, LogColor.Blue); + +const logLine = () => console.log('----------------------------------------'); + +export default (caller : string) => ({ + log: (message : string) => log(caller, message), + logWarning: (message : string) => logWarning(caller, message), + logError: (message : string) => logError(caller, message), + logSuccess: (message : string) => logSuccess(caller, message), + logInfo: (message : string) => logInfo(caller, message), + logLine : () => logLine(), +}); \ No newline at end of file