From 5c275b17b2d40e46110cb323c662ddd43a57e343 Mon Sep 17 00:00:00 2001 From: Guste Gaubaite <219.guste@gmail.com> Date: Mon, 3 Nov 2025 15:44:59 +0100 Subject: [PATCH 01/25] feat(#561 note): add NoteVm interface, provider methods and update schema for note support --- src/core/autosave/autosave.business.spec.ts | 1 + .../canvas-delete-item.business.spec.ts | 8 ++++ .../canvas-schema.business.spec.ts | 28 +++++++++++ .../canvas-updateposition.business.spec.ts | 5 ++ .../canvas.business-toggle-collapse.spec.ts | 2 + .../canvas-schema-vlatest.model.ts | 15 ++++++ .../canvas-schema/canvas-schema.business.ts | 27 ++++++++++- .../canvas-schema/canvas-schema.hook.spec.tsx | 4 ++ .../canvas-schema.mapper.spec.ts | 4 ++ .../canvas-schema/canvas-schema.mapper.ts | 2 +- .../canvas-schema/canvas-schema.provider.tsx | 48 +++++++++++++++++++ .../canvas-schema/canvas.business.spec.ts | 2 + src/pods/canvas/canvas.mock.data.ts | 1 + src/pods/canvas/m-flix.mock.data.ts | 1 + 14 files changed, 146 insertions(+), 2 deletions(-) diff --git a/src/core/autosave/autosave.business.spec.ts b/src/core/autosave/autosave.business.spec.ts index 36e016d5..c4dcfec8 100644 --- a/src/core/autosave/autosave.business.spec.ts +++ b/src/core/autosave/autosave.business.spec.ts @@ -48,6 +48,7 @@ describe('saveToLocal', () => { tables: [{ id: '1', fields: [], tableName: 'tableName', x: 0, y: 0 }], relations: [], selectedElementId: null, + notes: [], }, }; const autosaveError = 0; diff --git a/src/core/providers/canvas-schema/business-specs/canvas-delete-item.business.spec.ts b/src/core/providers/canvas-schema/business-specs/canvas-delete-item.business.spec.ts index e4d96536..92af70e0 100644 --- a/src/core/providers/canvas-schema/business-specs/canvas-delete-item.business.spec.ts +++ b/src/core/providers/canvas-schema/business-specs/canvas-delete-item.business.spec.ts @@ -9,6 +9,7 @@ describe('deleteItemFromCanvasSchema', () => { const dbSchema: DatabaseSchemaVm = { version: '0.1', selectedElementId: '1', + notes: [], relations: [ { id: '20', @@ -78,6 +79,7 @@ describe('deleteItemFromCanvasSchema', () => { const expected = { version: '0.1', selectedElementId: null, + notes: [], relations: [], tables: [ { @@ -110,6 +112,7 @@ describe('deleteItemFromCanvasSchema', () => { const dbSchema: DatabaseSchemaVm = { version: '0.1', selectedElementId: '20', + notes: [], relations: [ { id: '20', @@ -240,6 +243,7 @@ describe('deleteItemFromCanvasSchema', () => { const dbSchema: DatabaseSchemaVm = { version: '0.1', selectedElementId: '30', + notes: [], relations: [ { id: '30', @@ -301,6 +305,7 @@ describe('deleteItemFromCanvasSchema', () => { const expected = { version: '0.1', selectedElementId: null, + notes: [], relations: [], tables: [ { @@ -353,6 +358,7 @@ describe('deleteItemFromCanvasSchema', () => { const dbSchema: DatabaseSchemaVm = { version: '0.1', selectedElementId: '1', + notes: [], relations: [], tables: [ { @@ -396,6 +402,7 @@ describe('deleteItemFromCanvasSchema', () => { const dbSchema: DatabaseSchemaVm = { version: '0.1', selectedElementId: '11', + notes: [], relations: [ { id: '20', @@ -445,6 +452,7 @@ describe('deleteItemFromCanvasSchema', () => { const expected = { version: '0.1', selectedElementId: null, + notes: [], relations: [ { id: '20', diff --git a/src/core/providers/canvas-schema/business-specs/canvas-schema.business.spec.ts b/src/core/providers/canvas-schema/business-specs/canvas-schema.business.spec.ts index 4997a139..893367b9 100644 --- a/src/core/providers/canvas-schema/business-specs/canvas-schema.business.spec.ts +++ b/src/core/providers/canvas-schema/business-specs/canvas-schema.business.spec.ts @@ -23,6 +23,7 @@ describe('canvas-schema.business', () => { const dbSchema: DatabaseSchemaVm = { version: '0.1', selectedElementId: null, + notes: [], relations: [ { id: '20', @@ -54,6 +55,7 @@ describe('canvas-schema.business', () => { const expectedResult: DatabaseSchemaVm = { version: '0.1', selectedElementId: null, + notes: [], relations: [ { id: '20', @@ -114,6 +116,7 @@ describe('canvas-schema.business', () => { const dbSchema: DatabaseSchemaVm = { version: '0.1', selectedElementId: null, + notes: [], relations: [ { id: '20', @@ -145,6 +148,7 @@ describe('canvas-schema.business', () => { const expectedResult: DatabaseSchemaVm = { version: '0.1', selectedElementId: null, + notes: [], relations: [ { id: '20', @@ -198,6 +202,7 @@ describe('canvas-schema.business', () => { const dbSchema: DatabaseSchemaVm = { version: '0.1', selectedElementId: null, + notes: [], relations: [ { id: '20', @@ -229,6 +234,7 @@ describe('canvas-schema.business', () => { const expectedResult: DatabaseSchemaVm = { version: '0.1', selectedElementId: null, + notes: [], relations: [], tables: [ { @@ -273,6 +279,7 @@ describe('canvas-schema.business', () => { const dbSchema: DatabaseSchemaVm = { version: '0.1', selectedElementId: null, + notes: [], relations: [ { id: '20', @@ -316,6 +323,7 @@ describe('canvas-schema.business', () => { const expectedResult: DatabaseSchemaVm = { version: '0.1', selectedElementId: null, + notes: [], relations: [ { id: '20', @@ -382,6 +390,7 @@ describe('canvas-schema.business', () => { const dbSchema: DatabaseSchemaVm = { version: '0.1', selectedElementId: null, + notes: [], relations: [ { id: '20', @@ -451,6 +460,7 @@ describe('canvas-schema.business', () => { const expectedResult: DatabaseSchemaVm = { version: '0.1', selectedElementId: null, + notes: [], relations: [ { id: '20', @@ -537,6 +547,7 @@ describe('canvas-schema.business', () => { const dbSchema: DatabaseSchemaVm = { version: '0.1', selectedElementId: null, + notes: [], relations: [ { id: '20', @@ -568,6 +579,7 @@ describe('canvas-schema.business', () => { const expectedResult: DatabaseSchemaVm = { version: '0.1', selectedElementId: null, + notes: [], relations: [ { id: '20', @@ -678,6 +690,7 @@ describe('canvas-schema.business', () => { }, ], selectedElementId: null, + notes: [], }; const expectedResult: DatabaseSchemaVm = { version: '0.1', @@ -751,6 +764,7 @@ describe('canvas-schema.business', () => { }, ], selectedElementId: null, + notes: [], }; // Act @@ -855,6 +869,7 @@ describe('canvas-schema.business', () => { }, ], selectedElementId: null, + notes: [], }; const expectedResult: DatabaseSchemaVm = { version: '0.1', @@ -950,6 +965,7 @@ describe('canvas-schema.business', () => { }, ], selectedElementId: null, + notes: [], }; // Act @@ -1003,6 +1019,7 @@ describe('canvas-schema.business', () => { }, ], selectedElementId: null, + notes: [], }; const expectedResult: DatabaseSchemaVm = { version: '0.1', @@ -1048,6 +1065,7 @@ describe('canvas-schema.business', () => { }, ], selectedElementId: null, + notes: [], }; // Act @@ -1113,6 +1131,7 @@ describe('canvas-schema.business', () => { }, ], selectedElementId: null, + notes: [], }; const expectedResult: DatabaseSchemaVm = { @@ -1144,6 +1163,7 @@ describe('canvas-schema.business', () => { relations: [], selectedElementId: null, + notes: [], }; // Act @@ -1206,6 +1226,7 @@ describe('canvas-schema.business', () => { }, ], selectedElementId: null, + notes: [], }; const expectedResult: DatabaseSchemaVm = { @@ -1237,6 +1258,7 @@ describe('canvas-schema.business', () => { relations: [], selectedElementId: null, + notes: [], }; // Act @@ -1342,6 +1364,7 @@ describe('canvas-schema.business', () => { }, ], selectedElementId: null, + notes: [], }; const expectedResult: DatabaseSchemaVm = { @@ -1403,6 +1426,7 @@ describe('canvas-schema.business', () => { }, ], selectedElementId: null, + notes: [], }; // Act @@ -1506,6 +1530,7 @@ describe('canvas-schema.business', () => { }, ], selectedElementId: null, + notes: [], }; const expectedResult: DatabaseSchemaVm = { @@ -1565,6 +1590,7 @@ describe('canvas-schema.business', () => { }, ], selectedElementId: null, + notes: [], }; // Act @@ -1619,6 +1645,7 @@ describe('canvas-schema.business', () => { relations: [], selectedElementId: null, + notes: [], }; const expectedResult: DatabaseSchemaVm = { @@ -1648,6 +1675,7 @@ describe('canvas-schema.business', () => { ], relations: [], selectedElementId: null, + notes: [], }; // Act diff --git a/src/core/providers/canvas-schema/business-specs/canvas-updateposition.business.spec.ts b/src/core/providers/canvas-schema/business-specs/canvas-updateposition.business.spec.ts index 25ab4b60..927b3570 100644 --- a/src/core/providers/canvas-schema/business-specs/canvas-updateposition.business.spec.ts +++ b/src/core/providers/canvas-schema/business-specs/canvas-updateposition.business.spec.ts @@ -65,6 +65,7 @@ describe('calculateTablePosition', () => { const schema: DatabaseSchemaVm = { version: '0.1', selectedElementId: null, + notes: [], tables, relations, }; @@ -194,6 +195,7 @@ describe('calculateTablePosition', () => { const schema: DatabaseSchemaVm = { version: '0.1', selectedElementId: null, + notes: [], tables, relations, }; @@ -329,6 +331,7 @@ describe('calculateTablePosition', () => { const schema: DatabaseSchemaVm = { version: '0.1', selectedElementId: null, + notes: [], tables, relations, }; @@ -459,6 +462,7 @@ describe('calculateTablePosition', () => { const schema: DatabaseSchemaVm = { version: '0.1', selectedElementId: null, + notes: [], tables, relations, }; @@ -589,6 +593,7 @@ describe('calculateTablePosition', () => { const schema: DatabaseSchemaVm = { version: '0.1', selectedElementId: null, + notes: [], tables, relations, }; diff --git a/src/core/providers/canvas-schema/business-specs/canvas.business-toggle-collapse.spec.ts b/src/core/providers/canvas-schema/business-specs/canvas.business-toggle-collapse.spec.ts index df6b9a87..4c533112 100644 --- a/src/core/providers/canvas-schema/business-specs/canvas.business-toggle-collapse.spec.ts +++ b/src/core/providers/canvas-schema/business-specs/canvas.business-toggle-collapse.spec.ts @@ -7,6 +7,7 @@ describe('doFieldToggleCollapseLogic', () => { const currentSchema: DatabaseSchemaVm = { version: '0.1', selectedElementId: null, + notes: [], tables: [ { id: 'table1', @@ -50,6 +51,7 @@ describe('doFieldToggleCollapseLogic', () => { const currentSchema: DatabaseSchemaVm = { version: '0.1', selectedElementId: null, + notes: [], tables: [ { id: 'table1', diff --git a/src/core/providers/canvas-schema/canvas-schema-vlatest.model.ts b/src/core/providers/canvas-schema/canvas-schema-vlatest.model.ts index a5f80d9c..b3ee64a7 100644 --- a/src/core/providers/canvas-schema/canvas-schema-vlatest.model.ts +++ b/src/core/providers/canvas-schema/canvas-schema-vlatest.model.ts @@ -31,12 +31,23 @@ export interface RelationVm { type: RelationType; } +export interface NoteVm { + id: GUID; + title?: string; + description: string; + x: number; + y: number; + width: number; + height: number; +} + export type Versions = '0.1'; export interface DatabaseSchemaVm { version: Versions; tables: TableVm[]; relations: RelationVm[]; + notes: NoteVm[]; selectedElementId: GUID | null; isPristine?: boolean; } @@ -45,6 +56,7 @@ export const createDefaultDatabaseSchemaVm = (): DatabaseSchemaVm => ({ version: '0.1', tables: [], relations: [], + notes: [], selectedElementId: null, isPristine: true, }); @@ -71,6 +83,9 @@ export interface CanvasSchemaContextVm { updateFullTable: (table: TableVm) => void; addTable: (table: TableVm) => void; addRelation: (relation: RelationVm) => void; + addNote: (note: NoteVm) => void; + updateFullNote: (note: NoteVm) => void; + updateNotePosition: UpdatePositionFn; doSelectElement: (id: GUID | null) => void; canUndo: () => boolean; canRedo: () => boolean; diff --git a/src/core/providers/canvas-schema/canvas-schema.business.ts b/src/core/providers/canvas-schema/canvas-schema.business.ts index ec255ef2..f5ec0278 100644 --- a/src/core/providers/canvas-schema/canvas-schema.business.ts +++ b/src/core/providers/canvas-schema/canvas-schema.business.ts @@ -1,5 +1,10 @@ import { produce } from 'immer'; -import { FieldVm, RelationVm, TableVm } from './canvas-schema-vlatest.model'; +import { + FieldVm, + NoteVm, + RelationVm, + TableVm, +} from './canvas-schema-vlatest.model'; import { DatabaseSchemaVm } from './canvas-schema-vlatest.model'; import { GUID } from '@/core/model'; @@ -105,3 +110,23 @@ export const updateRelation = ( draft.relations[index] = relation; } }); + +export const addNewNote = ( + note: NoteVm, + dbSchema: DatabaseSchemaVm +): DatabaseSchemaVm => + produce(dbSchema, draft => { + draft.notes.push(note); + }); + +export const updateNote = ( + note: NoteVm, + dbSchema: DatabaseSchemaVm +): DatabaseSchemaVm => + produce(dbSchema, draft => { + const index = draft.notes.findIndex(n => n.id === note.id); + + if (index !== -1) { + draft.notes[index] = note; + } + }); diff --git a/src/core/providers/canvas-schema/canvas-schema.hook.spec.tsx b/src/core/providers/canvas-schema/canvas-schema.hook.spec.tsx index db1dda94..bce7dfad 100644 --- a/src/core/providers/canvas-schema/canvas-schema.hook.spec.tsx +++ b/src/core/providers/canvas-schema/canvas-schema.hook.spec.tsx @@ -11,11 +11,13 @@ describe('useStateWithInterceptor', () => { tables: [], relations: [], selectedElementId: null, + notes: [], }; const newSchema: DatabaseSchemaVm = { version: '0.1', selectedElementId: null, + notes: [], relations: [], tables: [ { @@ -69,11 +71,13 @@ describe('useStateWithInterceptor', () => { tables: [], relations: [], selectedElementId: null, + notes: [], }; const newSchema: DatabaseSchemaVm = { version: '0.1', selectedElementId: null, + notes: [], relations: [], tables: [ { diff --git a/src/core/providers/canvas-schema/canvas-schema.mapper.spec.ts b/src/core/providers/canvas-schema/canvas-schema.mapper.spec.ts index d922e3b3..912e8926 100644 --- a/src/core/providers/canvas-schema/canvas-schema.mapper.spec.ts +++ b/src/core/providers/canvas-schema/canvas-schema.mapper.spec.ts @@ -17,6 +17,7 @@ describe('mapSchemaToLatestVersion', () => { version: '0.1', tables: [], relations: [], + notes: [], selectedElementId: null, }; // Act @@ -37,6 +38,7 @@ describe('mapSchemaToLatestVersion', () => { version: '0.1', tables: [], relations: [], + notes: [], selectedElementId: null, }; // Act @@ -51,12 +53,14 @@ describe('mapSchemaToLatestVersion', () => { version: '1.0', tables: [], relations: [], + notes: [], selectedElementId: null, }; const expectedResult: DatabaseSchemaVm = { version: '1.0' as Versions, tables: [], relations: [], + notes: [], selectedElementId: null, }; // Act diff --git a/src/core/providers/canvas-schema/canvas-schema.mapper.ts b/src/core/providers/canvas-schema/canvas-schema.mapper.ts index fe6dc143..f3459e41 100644 --- a/src/core/providers/canvas-schema/canvas-schema.mapper.ts +++ b/src/core/providers/canvas-schema/canvas-schema.mapper.ts @@ -3,7 +3,7 @@ import * as schemaV0 from './canvas-schema-v0.model'; const mapSchemaFromNoVersionToLatestVersion = ( newSchema: schemaV0.DatabaseSchemaVmV0 -): DatabaseSchemaVm => ({ ...newSchema, version: '0.1' }); +): DatabaseSchemaVm => ({ ...newSchema, version: '0.1', notes: [] }); // TODO: Add unit test to mapSchemaToLatestVersion // #277 diff --git a/src/core/providers/canvas-schema/canvas-schema.provider.tsx b/src/core/providers/canvas-schema/canvas-schema.provider.tsx index d2d61b26..db877a73 100644 --- a/src/core/providers/canvas-schema/canvas-schema.provider.tsx +++ b/src/core/providers/canvas-schema/canvas-schema.provider.tsx @@ -3,6 +3,7 @@ import { produce } from 'immer'; import { CanvasSchemaContext } from './canvas-schema.context'; import { DatabaseSchemaVm, + NoteVm, RelationVm, TableVm, UpdatePositionItemInfo, @@ -16,7 +17,9 @@ import { deleteItemFromCanvasSchema, } from './canvas.business'; import { + addNewNote, addNewTable, + updateNote, updateRelation, updateTable, } from './canvas-schema.business'; @@ -135,6 +138,48 @@ export const CanvasSchemaProvider: React.FC = props => { ); }; + const addNote = (note: NoteVm) => { + setSchema(prevSchema => + addNewNote(note, { ...prevSchema, isPristine: false }) + ); + }; + + const updateFullNote = (note: NoteVm) => { + setSchema(prevSchema => + updateNote(note, { ...prevSchema, isPristine: false }) + ); + }; + + const updateNotePosition = ( + noteItemInfo: UpdatePositionItemInfo, + isDragFinished: boolean + ) => { + const { id, position } = noteItemInfo; + if (isDragFinished) { + setSchema(prevSchema => + produce(prevSchema, (draft: DatabaseSchemaVm) => { + const noteIndex = draft.notes.findIndex((n: NoteVm) => n.id === id); + if (noteIndex !== -1) { + draft.notes[noteIndex].x = position.x; + draft.notes[noteIndex].y = position.y; + draft.isPristine = false; + } + }) + ); + } else { + // Dragging in progress - skip history for performance + setSchemaSkipHistory(prevSchema => + produce(prevSchema, (draft: DatabaseSchemaVm) => { + const noteIndex = draft.notes.findIndex((n: NoteVm) => n.id === id); + if (noteIndex !== -1) { + draft.notes[noteIndex].x = position.x; + draft.notes[noteIndex].y = position.y; + } + }) + ); + } + }; + // TODO: #57 created to track this // https://github.com/Lemoncode/mongo-modeler/issues/57 const doFieldToggleCollapse = (tableId: GUID, fieldId: GUID): void => { @@ -269,6 +314,9 @@ export const CanvasSchemaProvider: React.FC = props => { updateFullTable, addTable, addRelation, + addNote, + updateFullNote, + updateNotePosition, doSelectElement, canUndo, canRedo, diff --git a/src/core/providers/canvas-schema/canvas.business.spec.ts b/src/core/providers/canvas-schema/canvas.business.spec.ts index c1336137..4f2b2771 100644 --- a/src/core/providers/canvas-schema/canvas.business.spec.ts +++ b/src/core/providers/canvas-schema/canvas.business.spec.ts @@ -6,6 +6,7 @@ describe('doesRelationAlreadyExists', () => { const alreadyExists: DatabaseSchemaVm = { version: '0.1', selectedElementId: null, + notes: [], tables: [ { id: '1', @@ -77,6 +78,7 @@ describe('doesRelationAlreadyExists', () => { it('should return false if relation does not exist', () => { const alreadyExists: DatabaseSchemaVm = { version: '0.1', + notes: [], selectedElementId: null, tables: [ { diff --git a/src/pods/canvas/canvas.mock.data.ts b/src/pods/canvas/canvas.mock.data.ts index 3db237b2..89ce7ca7 100644 --- a/src/pods/canvas/canvas.mock.data.ts +++ b/src/pods/canvas/canvas.mock.data.ts @@ -160,4 +160,5 @@ export const mockSchema: DatabaseSchemaVm = { selectedElementId: null, tables: mockTables, relations: mockRelations, + notes: [], }; diff --git a/src/pods/canvas/m-flix.mock.data.ts b/src/pods/canvas/m-flix.mock.data.ts index f10b5e3d..cfef471e 100644 --- a/src/pods/canvas/m-flix.mock.data.ts +++ b/src/pods/canvas/m-flix.mock.data.ts @@ -674,5 +674,6 @@ export const mFlix: DatabaseSchemaVm = { toTableId: '4d88b10a-d79f-4889-af2b-103946262e39', }, ], + notes: [], selectedElementId: '4c6b9f3e-7cc6-463b-ad68-aafc89f38175', }; From a3166f4d9a6962d56987e5dc1347a527ac367862 Mon Sep 17 00:00:00 2001 From: Guste Gaubaite <219.guste@gmail.com> Date: Mon, 3 Nov 2025 20:29:26 +0100 Subject: [PATCH 02/25] feat(#561 note): implement draggable note component with title, body and edit icon --- .../components/note/components/index.ts | 3 + .../note/components/note-body.component.tsx | 32 ++++++ .../note/components/note-border.component.tsx | 22 ++++ .../note/components/note-title.component.tsx | 104 ++++++++++++++++++ src/pods/canvas/components/note/index.ts | 1 + .../canvas/components/note/note.component.tsx | 93 ++++++++++++++++ src/pods/canvas/components/note/note.const.ts | 25 +++++ .../canvas/components/note/note.module.css | 46 ++++++++ 8 files changed, 326 insertions(+) create mode 100644 src/pods/canvas/components/note/components/index.ts create mode 100644 src/pods/canvas/components/note/components/note-body.component.tsx create mode 100644 src/pods/canvas/components/note/components/note-border.component.tsx create mode 100644 src/pods/canvas/components/note/components/note-title.component.tsx create mode 100644 src/pods/canvas/components/note/index.ts create mode 100644 src/pods/canvas/components/note/note.component.tsx create mode 100644 src/pods/canvas/components/note/note.const.ts create mode 100644 src/pods/canvas/components/note/note.module.css diff --git a/src/pods/canvas/components/note/components/index.ts b/src/pods/canvas/components/note/components/index.ts new file mode 100644 index 00000000..d2947a8a --- /dev/null +++ b/src/pods/canvas/components/note/components/index.ts @@ -0,0 +1,3 @@ +export * from './note-body.component'; +export * from './note-title.component'; +export * from './note-border.component'; diff --git a/src/pods/canvas/components/note/components/note-body.component.tsx b/src/pods/canvas/components/note/components/note-body.component.tsx new file mode 100644 index 00000000..4d74f082 --- /dev/null +++ b/src/pods/canvas/components/note/components/note-body.component.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { NOTE_CONST } from '../note.const'; +import classes from '../note.module.css'; + +interface Props { + description: string; + width: number; + height: number; +} + +export const NoteBody: React.FC = props => { + const { description, width, height } = props; + return ( + + + +
{description}
+
+
+ ); +}; diff --git a/src/pods/canvas/components/note/components/note-border.component.tsx b/src/pods/canvas/components/note/components/note-border.component.tsx new file mode 100644 index 00000000..87010908 --- /dev/null +++ b/src/pods/canvas/components/note/components/note-border.component.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import classes from '../note.module.css'; + +interface Props { + width: number; + height: number; + isSelected: boolean; +} + +export const NoteBorder: React.FC = props => { + const { width, height, isSelected } = props; + + return ( + + ); +}; diff --git a/src/pods/canvas/components/note/components/note-title.component.tsx b/src/pods/canvas/components/note/components/note-title.component.tsx new file mode 100644 index 00000000..16c69b88 --- /dev/null +++ b/src/pods/canvas/components/note/components/note-title.component.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { NOTE_CONST } from '../note.const'; +import classes from '../note.module.css'; +import { TruncatedText } from '../../table/components'; +import { Edit } from '@/common/components'; + +interface Props { + title?: string; + onEditNote: () => void; + onSelectNote: () => void; + isSelected: boolean; + width: number; + isTabletOrMobileDevice: boolean; +} + +export const NoteTitle: React.FC = props => { + const { + title, + onEditNote, + onSelectNote, + isSelected, + width, + isTabletOrMobileDevice, + } = props; + + const handlePencilIconClick = ( + e: React.MouseEvent + ) => { + onEditNote(); + e.stopPropagation(); + }; + + const handleClick = (e: React.MouseEvent) => { + onSelectNote(); + e.stopPropagation(); + }; + + const handleDoubleClick = (e: React.MouseEvent) => { + onEditNote(); + e.stopPropagation(); + }; + + return ( + <> + + + + + + {isSelected && !isTabletOrMobileDevice && ( + + + + + + + )} + {/* Clikable area to select the note or edit it*/} + + + ); +}; diff --git a/src/pods/canvas/components/note/index.ts b/src/pods/canvas/components/note/index.ts new file mode 100644 index 00000000..ce29ec83 --- /dev/null +++ b/src/pods/canvas/components/note/index.ts @@ -0,0 +1 @@ +export * from './note.component'; diff --git a/src/pods/canvas/components/note/note.component.tsx b/src/pods/canvas/components/note/note.component.tsx new file mode 100644 index 00000000..7a202cf1 --- /dev/null +++ b/src/pods/canvas/components/note/note.component.tsx @@ -0,0 +1,93 @@ +import { GUID, Size } from '@/core/model'; +import { NoteVm, UpdatePositionFn } from '@/core/providers'; +import React from 'react'; +import { NOTE_CONST } from './note.const'; +import { useDraggable } from '@/common/canvas-draggable'; +import { NoteBody, NoteBorder, NoteTitle } from './components'; +import classes from './note.module.css'; + +interface Props { + noteInfo: NoteVm; + updatePosition: UpdatePositionFn; + onEditNote: (note: NoteVm) => void; + canvasSize: Size; + isSelected: boolean; + selectNote: (noteId: GUID) => void; + isTabletOrMobileDevice: boolean; + viewBoxSize: Size; + zoomFactor: number; +} + +export const Note: React.FC = props => { + const { + noteInfo, + updatePosition, + onEditNote, + canvasSize, + isSelected, + selectNote, + isTabletOrMobileDevice, + viewBoxSize, + zoomFactor, + } = props; + + const noteWidth = noteInfo.width ?? NOTE_CONST.DEFAULT_NOTE_WIDTH; + const noteHeight = noteInfo.height ?? NOTE_CONST.DEFAULT_NOTE_HEIGHT; + + const bodyHeight = noteHeight - NOTE_CONST.TITLE_HEIGHT; + + const { onMouseDown, onTouchStart, ref } = useDraggable( + noteInfo.id, + noteInfo.x, + noteInfo.y, + updatePosition, + noteHeight, + canvasSize, + viewBoxSize, + zoomFactor + ); + + const handleSelectNote = () => { + if (!isSelected) { + selectNote(noteInfo.id); + } + }; + + const handleDoubleClick = () => { + onEditNote(noteInfo); + }; + + return ( + | undefined} + > + {/* Border */} + + + {/* Title */} + + + {/* Body */} + + + ); +}; diff --git a/src/pods/canvas/components/note/note.const.ts b/src/pods/canvas/components/note/note.const.ts new file mode 100644 index 00000000..dd7cf0fc --- /dev/null +++ b/src/pods/canvas/components/note/note.const.ts @@ -0,0 +1,25 @@ +const DEFAULT_NOTE_WIDTH = 240; +const DEFAULT_NOTE_HEIGHT = 270; +const TITLE_HEIGHT = 40; +const PADDING_Y = 12; +const PADDING_X = 16; +const MIN_NOTE_WIDTH = 200; +const MIN_NOTE_HEIGHT = 150; +const PENCIL_ICON_WIDTH = 30; +const PENCIL_ICON_HEIGHT = 25; +const PENCIL_MARGIN_RIGHT = 5; +const GAP = 8; + +export const NOTE_CONST = { + DEFAULT_NOTE_WIDTH, + DEFAULT_NOTE_HEIGHT, + TITLE_HEIGHT, + PADDING_X, + PADDING_Y, + MIN_NOTE_WIDTH, + MIN_NOTE_HEIGHT, + PENCIL_ICON_WIDTH, + PENCIL_ICON_HEIGHT, + PENCIL_MARGIN_RIGHT, + GAP, +}; diff --git a/src/pods/canvas/components/note/note.module.css b/src/pods/canvas/components/note/note.module.css new file mode 100644 index 00000000..d198c3ec --- /dev/null +++ b/src/pods/canvas/components/note/note.module.css @@ -0,0 +1,46 @@ +.noteTitle { + fill: #f8f4d3; +} + +.noteText { + font-size: 14px; + fill: var(--text-dark); +} + +.noteBody { + fill: #f8f4d3; +} + +.noteBodyText { + width: 100%; + height: 100%; + text-align: left; + overflow: hidden; + word-wrap: break-word; + white-space: pre-wrap; + overflow-wrap: break-word; + font-size: 14px; + line-height: 1.4; + color: var(--text-dark); +} + +.noteBorder { + fill: none; + stroke: #d4c896; + stroke-width: 1; +} + +.noteBorderSelected { + fill: none; + stroke: #c9b155; + stroke-width: 3; +} + +.noteContainer { + cursor: grabbing; +} + +.editIcon { + color: #000000; + fill: #000000; +} From 53da01817c3b102fb6d21d2e7e0eac9b6e59116a Mon Sep 17 00:00:00 2001 From: Guste Gaubaite <219.guste@gmail.com> Date: Mon, 3 Nov 2025 20:31:07 +0100 Subject: [PATCH 03/25] refactor(#561 use draggable hook): extract draggable logic into reusable useCanvasDraggable hook --- .../canvas-draggable.hook.tsx | 126 ++++++++++++++++++ src/common/canvas-draggable/index.ts | 1 + 2 files changed, 127 insertions(+) create mode 100644 src/common/canvas-draggable/canvas-draggable.hook.tsx create mode 100644 src/common/canvas-draggable/index.ts diff --git a/src/common/canvas-draggable/canvas-draggable.hook.tsx b/src/common/canvas-draggable/canvas-draggable.hook.tsx new file mode 100644 index 00000000..56b0c76f --- /dev/null +++ b/src/common/canvas-draggable/canvas-draggable.hook.tsx @@ -0,0 +1,126 @@ +import React, { useState, useCallback } from 'react'; +import { Size } from '@/core/model'; +import { UpdatePositionFn, UpdatePositionItemInfo } from '@/core/providers'; +import { setOffSetZoomToCoords } from '@/common/helpers/set-off-set-zoom-to-coords.helper'; + +export const useDraggable = ( + id: string, + initialX: number, + initialY: number, + updatePosition: UpdatePositionFn, + totalHeight: number, + canvasSize: Size, + viewBoxSize: Size, + zoomFactor: number +) => { + const [isDragging, setIsDragging] = useState(false); + const [startDragPosition, setStartDragPosition] = useState({ x: 0, y: 0 }); + const [finalInfoAfterDrag, setFinalInfoAfterDrag] = + useState(null); + const [node, setNode] = React.useState(null); + const ref = React.useCallback((nodeEle: SVGElement): void => { + setNode(nodeEle); + }, []); + + const startDrag = (x: number, y: number) => { + const { x: offsetX, y: offsetY } = setOffSetZoomToCoords( + x, + y, + viewBoxSize, + canvasSize, + zoomFactor + ); + setStartDragPosition({ + x: offsetX - initialX, + y: offsetY - initialY, + }); + setIsDragging(true); + }; + + const onMouseDown = useCallback( + (event: React.MouseEvent) => { + startDrag(event.clientX, event.clientY); + }, + [initialX, initialY, viewBoxSize] + ); + + const onTouchStart = useCallback( + (event: React.TouchEvent) => { + event.preventDefault(); + const touch = event.touches[0]; + startDrag(touch.clientX, touch.clientY); + }, + [initialX, initialY, viewBoxSize] + ); + + const updateDrag = (x: number, y: number) => { + if (isDragging) { + const { x: offsetX, y: offsetY } = setOffSetZoomToCoords( + x, + y, + viewBoxSize, + canvasSize, + zoomFactor + ); + const newPosition = { + id, + position: { + x: offsetX - startDragPosition.x, + y: offsetY - startDragPosition.y, + }, + totalHeight, + canvasSize, + }; + + updatePosition(newPosition, false); + setFinalInfoAfterDrag(newPosition); + } + }; + + const onMouseMove = useCallback( + (event: MouseEvent) => { + updateDrag(event.clientX, event.clientY); + }, + [id, isDragging, startDragPosition, updatePosition, totalHeight, canvasSize] + ); + + const onTouchMove = useCallback( + (event: TouchEvent) => { + event.preventDefault(); + const touch = event.touches[0]; + updateDrag(touch.clientX, touch.clientY); + }, + [id, isDragging, startDragPosition, updatePosition, totalHeight, canvasSize] + ); + + const endDrag = useCallback(() => { + setIsDragging(false); + if (finalInfoAfterDrag) { + updatePosition(finalInfoAfterDrag, true); + } + }, [finalInfoAfterDrag, updatePosition]); + + React.useEffect(() => { + if (!node) return; + if (isDragging) { + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('mouseup', endDrag); + node.addEventListener('touchmove', onTouchMove); + node.addEventListener('touchend', endDrag); + } else { + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('mouseup', endDrag); + node.removeEventListener('touchmove', onTouchMove); + node.removeEventListener('touchend', endDrag); + } + + return () => { + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('mouseup', endDrag); + node.removeEventListener('touchmove', onTouchMove); + node.removeEventListener('touchend', endDrag); + }; + }, [isDragging, onMouseMove, onTouchMove, endDrag]); + + return { onMouseDown, onTouchStart, ref }; +}; diff --git a/src/common/canvas-draggable/index.ts b/src/common/canvas-draggable/index.ts new file mode 100644 index 00000000..ee1bc116 --- /dev/null +++ b/src/common/canvas-draggable/index.ts @@ -0,0 +1 @@ +export * from './canvas-draggable.hook'; From 16b1e5f36cf157f692d25ee6b680f59fa18adfcc Mon Sep 17 00:00:00 2001 From: Guste Gaubaite <219.guste@gmail.com> Date: Tue, 4 Nov 2025 20:56:49 +0100 Subject: [PATCH 04/25] feat(#561 floating-bar): add AddNote component with NoteIcon and integrate into FloatingBar --- .../components/icons/note-icon.component.tsx | 15 ++++++++++++++ .../add-note/add-note.component.tsx | 20 +++++++++++++++++++ .../floating-bar/components/add-note/index.ts | 1 + src/pods/floating-bar/components/index.ts | 1 + .../floating-bar/floating-bar.component.tsx | 3 ++- 5 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 src/common/components/icons/note-icon.component.tsx create mode 100644 src/pods/floating-bar/components/add-note/add-note.component.tsx create mode 100644 src/pods/floating-bar/components/add-note/index.ts diff --git a/src/common/components/icons/note-icon.component.tsx b/src/common/components/icons/note-icon.component.tsx new file mode 100644 index 00000000..129f56ae --- /dev/null +++ b/src/common/components/icons/note-icon.component.tsx @@ -0,0 +1,15 @@ +export const NoteIcon = () => { + return ( + + + + ); +}; diff --git a/src/pods/floating-bar/components/add-note/add-note.component.tsx b/src/pods/floating-bar/components/add-note/add-note.component.tsx new file mode 100644 index 00000000..d5a81f3a --- /dev/null +++ b/src/pods/floating-bar/components/add-note/add-note.component.tsx @@ -0,0 +1,20 @@ +import { ActionButton } from '@/common/components/action-button'; +import classes from '../floating-bar-components.module.css'; +import { NoteIcon } from '@/common/components/icons/note-icon.component'; + +export const AddNote = () => { + const handleAddNoteClick = () => { + console.log('Modal open'); + }; + + return ( + } + label="Add Note" + onClick={handleAddNoteClick} + className={`${classes.button} hide-mobile add-note-button`} + showLabel={false} + tooltipPosition="top" + /> + ); +}; diff --git a/src/pods/floating-bar/components/add-note/index.ts b/src/pods/floating-bar/components/add-note/index.ts new file mode 100644 index 00000000..ac25d227 --- /dev/null +++ b/src/pods/floating-bar/components/add-note/index.ts @@ -0,0 +1 @@ +export * from './add-note.component'; diff --git a/src/pods/floating-bar/components/index.ts b/src/pods/floating-bar/components/index.ts index 8e63fe90..ba481bde 100644 --- a/src/pods/floating-bar/components/index.ts +++ b/src/pods/floating-bar/components/index.ts @@ -1,2 +1,3 @@ export * from './add-collection'; export * from './relation-button'; +export * from './add-note'; diff --git a/src/pods/floating-bar/floating-bar.component.tsx b/src/pods/floating-bar/floating-bar.component.tsx index 84fc7461..098cbc4e 100644 --- a/src/pods/floating-bar/floating-bar.component.tsx +++ b/src/pods/floating-bar/floating-bar.component.tsx @@ -1,4 +1,4 @@ -import { AddCollection } from './components'; +import { AddCollection, AddNote } from './components'; import { RelationButton } from './components'; import classes from './floating-bar.pod.module.css'; @@ -9,6 +9,7 @@ export const FloatingBarComponent: React.FC = () => {
+
From df9b4d86cb4c92f7259d320e7b14f19847ea0a7f Mon Sep 17 00:00:00 2001 From: Guste Gaubaite <219.guste@gmail.com> Date: Sat, 8 Nov 2025 10:36:58 +0100 Subject: [PATCH 05/25] refactor(#561 note): Move DEFAULT_NOTE_WIDTH and DEFAULT_NOTE_HEIGHT to canvas.const.ts, update all imports and references across note components --- .../providers/canvas-schema/canvas.const.ts | 8 +++++ .../note/components/note-body.component.tsx | 12 +++---- .../note/components/note-title.component.tsx | 31 ++++++++++++------- .../canvas/components/note/note.component.tsx | 6 ++-- src/pods/canvas/components/note/note.const.ts | 6 +--- 5 files changed, 37 insertions(+), 26 deletions(-) diff --git a/src/core/providers/canvas-schema/canvas.const.ts b/src/core/providers/canvas-schema/canvas.const.ts index 634a94b0..3b878a3f 100644 --- a/src/core/providers/canvas-schema/canvas.const.ts +++ b/src/core/providers/canvas-schema/canvas.const.ts @@ -29,3 +29,11 @@ export const TABLE_CONST = { MAX_PLACEMENT_ATTEMPTS, TABLE_SHIFT_DISTANCE, }; + +const DEFAULT_NOTE_WIDTH = 240; +const DEFAULT_NOTE_HEIGHT = 270; + +export const NOTE_CONST = { + DEFAULT_NOTE_WIDTH, + DEFAULT_NOTE_HEIGHT, +}; diff --git a/src/pods/canvas/components/note/components/note-body.component.tsx b/src/pods/canvas/components/note/components/note-body.component.tsx index 4d74f082..aea43dc7 100644 --- a/src/pods/canvas/components/note/components/note-body.component.tsx +++ b/src/pods/canvas/components/note/components/note-body.component.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { NOTE_CONST } from '../note.const'; +import { NOTE_COMPONENT_CONST } from '../note.const'; import classes from '../note.module.css'; interface Props { @@ -14,16 +14,16 @@ export const NoteBody: React.FC = props => {
{description}
diff --git a/src/pods/canvas/components/note/components/note-title.component.tsx b/src/pods/canvas/components/note/components/note-title.component.tsx index 16c69b88..dc7f2a6d 100644 --- a/src/pods/canvas/components/note/components/note-title.component.tsx +++ b/src/pods/canvas/components/note/components/note-title.component.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { NOTE_CONST } from '../note.const'; +import { NOTE_COMPONENT_CONST } from '../note.const'; import classes from '../note.module.css'; import { TruncatedText } from '../../table/components'; import { Edit } from '@/common/components'; @@ -46,40 +46,45 @@ export const NoteTitle: React.FC = props => { x="0" y="0" width={width} - height={NOTE_CONST.TITLE_HEIGHT} + height={NOTE_COMPONENT_CONST.TITLE_HEIGHT} className={classes.noteTitle} /> {isSelected && !isTabletOrMobileDevice && ( @@ -92,8 +97,10 @@ export const NoteTitle: React.FC = props => { = props => { const noteWidth = noteInfo.width ?? NOTE_CONST.DEFAULT_NOTE_WIDTH; const noteHeight = noteInfo.height ?? NOTE_CONST.DEFAULT_NOTE_HEIGHT; - const bodyHeight = noteHeight - NOTE_CONST.TITLE_HEIGHT; + const bodyHeight = noteHeight - NOTE_COMPONENT_CONST.TITLE_HEIGHT; const { onMouseDown, onTouchStart, ref } = useDraggable( noteInfo.id, diff --git a/src/pods/canvas/components/note/note.const.ts b/src/pods/canvas/components/note/note.const.ts index dd7cf0fc..c07282d7 100644 --- a/src/pods/canvas/components/note/note.const.ts +++ b/src/pods/canvas/components/note/note.const.ts @@ -1,5 +1,3 @@ -const DEFAULT_NOTE_WIDTH = 240; -const DEFAULT_NOTE_HEIGHT = 270; const TITLE_HEIGHT = 40; const PADDING_Y = 12; const PADDING_X = 16; @@ -10,9 +8,7 @@ const PENCIL_ICON_HEIGHT = 25; const PENCIL_MARGIN_RIGHT = 5; const GAP = 8; -export const NOTE_CONST = { - DEFAULT_NOTE_WIDTH, - DEFAULT_NOTE_HEIGHT, +export const NOTE_COMPONENT_CONST = { TITLE_HEIGHT, PADDING_X, PADDING_Y, From 720ba9edaf89b82af19ae0e87a9bf1c710a4f7c1 Mon Sep 17 00:00:00 2001 From: Guste Gaubaite <219.guste@gmail.com> Date: Sat, 8 Nov 2025 10:37:48 +0100 Subject: [PATCH 06/25] feat(#561 edit-note): add EditNote component, styles and pod for note creation functionality --- src/pods/edit-note/edit-note.component.tsx | 53 ++++++++++++++++++++++ src/pods/edit-note/edit-note.module.css | 24 ++++++++++ src/pods/edit-note/edit-note.pod.tsx | 50 ++++++++++++++++++++ src/pods/edit-note/edit-note.vm.ts | 22 +++++++++ src/pods/edit-note/index.ts | 1 + 5 files changed, 150 insertions(+) create mode 100644 src/pods/edit-note/edit-note.component.tsx create mode 100644 src/pods/edit-note/edit-note.module.css create mode 100644 src/pods/edit-note/edit-note.pod.tsx create mode 100644 src/pods/edit-note/edit-note.vm.ts create mode 100644 src/pods/edit-note/index.ts diff --git a/src/pods/edit-note/edit-note.component.tsx b/src/pods/edit-note/edit-note.component.tsx new file mode 100644 index 00000000..dd6faf2a --- /dev/null +++ b/src/pods/edit-note/edit-note.component.tsx @@ -0,0 +1,53 @@ +import { NoteVm } from './edit-note.vm'; +import classes from './edit-note.module.css'; + +interface Props { + note: NoteVm; + updateTitle: (value: string) => void; + updateDescription: (value: string) => void; +} + +export const EditNoteComponent: React.FC = props => { + const { note, updateTitle, updateDescription } = props; + + const handleChangeTitle = (e: React.ChangeEvent) => { + updateTitle(e.currentTarget.value); + }; + + const handleChangeDescription = ( + e: React.ChangeEvent + ) => { + updateDescription(e.currentTarget.value); + }; + + return ( +
+
+ +
+ +
+