From 4e23826c5ef1cb5a55f03623afac05cce392a9ba Mon Sep 17 00:00:00 2001 From: Raulms29 Date: Sun, 13 Jul 2025 19:04:57 +0200 Subject: [PATCH 01/51] feat: :sparkles: Subnotes now appear at the top when editing. --- components.d.ts | 2 + package.json | 2 +- src/assets/electron.svg | 16 ++++- src/assets/icons/note-children.svg | 8 +++ src/assets/icons/note-no-children.svg | 8 +++ src/components/editor/EditorView.vue | 44 ++++++++++++- .../editor/note-children/NoteChildrenBig.vue | 49 +++++++++++++++ .../note-children/NoteChildrenSmall.vue | 62 +++++++++++++++++++ src/components/sidebar/Sidebar.vue | 11 +++- src/services/domain/Note.ts | 4 ++ src/stores/useNotesStore.ts | 2 - test/unit/note/note.test.ts | 7 +++ 12 files changed, 209 insertions(+), 6 deletions(-) create mode 100644 src/assets/icons/note-children.svg create mode 100644 src/assets/icons/note-no-children.svg create mode 100644 src/components/editor/note-children/NoteChildrenBig.vue create mode 100644 src/components/editor/note-children/NoteChildrenSmall.vue diff --git a/components.d.ts b/components.d.ts index 58f6a08..e7fb7b9 100644 --- a/components.d.ts +++ b/components.d.ts @@ -19,6 +19,8 @@ declare module 'vue' { GenericErrorMessage: typeof import('./src/components/generic/GenericErrorMessage.vue')['default'] LoadingOverlay: typeof import('./src/components/loading/LoadingOverlay.vue')['default'] NewWorkspaceCard: typeof import('./src/components/workspace/NewWorkspaceCard.vue')['default'] + NoteChildrenBig: typeof import('./src/components/editor/note-children/NoteChildrenBig.vue')['default'] + NoteChildrenSmall: typeof import('./src/components/editor/note-children/NoteChildrenSmall.vue')['default'] RemoveDialog: typeof import('./src/components/workspace/dialog/RemoveDialog.vue')['default'] RenameDialog: typeof import('./src/components/workspace/dialog/RenameDialog.vue')['default'] RenameErrorMessage: typeof import('./src/components/editor/error/RenameErrorMessage.vue')['default'] diff --git a/package.json b/package.json index bdd7ebe..12a6429 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "package": "electron-forge package", "make": "echo Creating Installer... 💻 && electron-forge make", "publish": "electron-forge publish", - "lint": "eslint --ext .ts,.tsx .", + "lint": "eslint --ext .ts .", "clean": "echo Removing files... 🫧 && rimraf .vite node_modules out coverage && echo All clean now!", "clean-linux": "echo Removing files... 🫧 && rm -rf .vite && rm -rf node_modules && rm -rf out && rm -rf coverage && echo All clean now!", "setup": "echo Installing dependencies... 🛠️ && npm ci", diff --git a/src/assets/electron.svg b/src/assets/electron.svg index dc81c99..4396825 100644 --- a/src/assets/electron.svg +++ b/src/assets/electron.svg @@ -1 +1,15 @@ - \ No newline at end of file + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/icons/note-children.svg b/src/assets/icons/note-children.svg new file mode 100644 index 0000000..1ac7510 --- /dev/null +++ b/src/assets/icons/note-children.svg @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/src/assets/icons/note-no-children.svg b/src/assets/icons/note-no-children.svg new file mode 100644 index 0000000..a28419e --- /dev/null +++ b/src/assets/icons/note-no-children.svg @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/src/components/editor/EditorView.vue b/src/components/editor/EditorView.vue index 2fc13d2..112ef9e 100644 --- a/src/components/editor/EditorView.vue +++ b/src/components/editor/EditorView.vue @@ -19,7 +19,19 @@ - +
+ + + + + +
@@ -83,6 +95,7 @@ function toggleFocusMode() { } import '../../styles/editor.css'; +import NoteChildrenBig from './note-children/NoteChildrenBig.vue'; \ No newline at end of file diff --git a/src/components/editor/note-children/NoteChildrenBig.vue b/src/components/editor/note-children/NoteChildrenBig.vue new file mode 100644 index 0000000..55f63f8 --- /dev/null +++ b/src/components/editor/note-children/NoteChildrenBig.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/src/components/editor/note-children/NoteChildrenSmall.vue b/src/components/editor/note-children/NoteChildrenSmall.vue new file mode 100644 index 0000000..ad0afcd --- /dev/null +++ b/src/components/editor/note-children/NoteChildrenSmall.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/src/components/sidebar/Sidebar.vue b/src/components/sidebar/Sidebar.vue index e746a5b..18493bd 100644 --- a/src/components/sidebar/Sidebar.vue +++ b/src/components/sidebar/Sidebar.vue @@ -27,6 +27,7 @@ import { useNotesStore } from '../../stores/useNotesStore' import { Note } from '../../services/domain/Note' import ChevronRight from 'icons/ChevronRight.vue' import Delete from 'icons/Delete.vue' +import FilePlusOutline from 'icons/FilePlusOutline.vue'; import DotsHorizontal from 'icons/DotsHorizontal.vue' const store = useNotesStore() @@ -80,6 +81,11 @@ function noteToTreeOption(note: Note): TreeOption { function getMenuOptions(option: TreeOption) { return [ + { + label: 'New Note', + key: 'new', + icon: getNIcon(FilePlusOutline), + }, { label: 'Delete', key: 'delete', @@ -147,6 +153,9 @@ function handleMenuSelect(option: TreeOption) { if (key === 'delete' && option.rawNote) { store.deleteNote(option.rawNote as Note) } + if (key === 'new' && option.rawNote) { + store.createNote(undefined, option.rawNote as Note); + } } } @@ -174,7 +183,7 @@ function renderLabel({ option }: { option: TreeOption }) { h('span', { class: 'truncate' }, typeof option.label === 'string' && option.label.length > 0 ? option.label : 'Untitled'), h(NDropdown, { options: getMenuOptions(option), - trigger: 'click', + trigger: 'hover', onSelect: handleMenuSelect(option), placement: 'bottom-end', onClick: (e: MouseEvent) => e.stopPropagation() diff --git a/src/services/domain/Note.ts b/src/services/domain/Note.ts index d6ad962..891acdf 100644 --- a/src/services/domain/Note.ts +++ b/src/services/domain/Note.ts @@ -195,4 +195,8 @@ export class Note { } return descendants; } + + hasChildren(): boolean { + return this._children.length > 0; + } } \ No newline at end of file diff --git a/src/stores/useNotesStore.ts b/src/stores/useNotesStore.ts index daac69e..f49d299 100644 --- a/src/stores/useNotesStore.ts +++ b/src/stores/useNotesStore.ts @@ -133,10 +133,8 @@ export const useNotesStore = defineStore('notes', () => { if (parentTree != null) { parentTree.addChild(newNote); - console.log(`Creating note "${newNote.name}" under parent "${parent.name}".`); } else { - console.log(`Creating note "${newNote.name}" at root level.`); notes.value.push(newNote); } diff --git a/test/unit/note/note.test.ts b/test/unit/note/note.test.ts index b717435..8f76181 100644 --- a/test/unit/note/note.test.ts +++ b/test/unit/note/note.test.ts @@ -2,6 +2,10 @@ import { Note } from '../../../src/services/domain/Note'; describe('GIVEN a Note', () => { describe('WHEN it has no children', () => { + it('THEN hasChildren returns false', () => { + const note = new Note('Test Note', [], '00000001'); + expect(note.hasChildren()).toBe(false); + }); it('THEN initializes with an 8-char ID', () => { const note = new Note('Test Note', [], '00000001'); @@ -89,6 +93,9 @@ describe('GIVEN a Note', () => { note = new Note('Parent Note', [childNote], '00000001'); }); + it('THEN hasChildren returns true', () => { + expect(note.hasChildren()).toBe(true); + }); it('THEN initializes with 1 child', () => { expect(note.id).toBe('00000001'); expect(note.name).toBe('Parent Note'); From 3a6f15ad448538e58a9267443d79a12d5f607c39 Mon Sep 17 00:00:00 2001 From: Raulms29 Date: Thu, 17 Jul 2025 20:40:01 +0200 Subject: [PATCH 02/51] feat: :sparkles: Sub notes now appear with a small dropdown menu that can be used to delete and add a new note inside them. Markdown export now includes note links. As minor changes: When a note is selected a loading overlay is shown when it takes some time (happens to very big notes). The editor now recognises tabs and they can be added to the text. --- components.d.ts | 2 + package-lock.json | 1 + package.json | 3 +- src/components/editor/Editor.vue | 37 ++++++++--- src/components/editor/EditorView.vue | 16 ++--- .../editor/bubble-menu/BubbleMenu.vue | 16 ++--- src/components/editor/createEditor.ts | 8 ++- src/components/editor/extensions/NoteLink.ts | 27 +++++++- .../editor/note-children/ChildrenOptions.vue | 63 +++++++++++++++++++ .../editor/note-children/NoteChildren.vue | 14 +++++ .../editor/note-children/NoteChildrenBig.vue | 39 ++++++++++-- .../note-children/NoteChildrenSmall.vue | 19 +++++- src/components/sidebar/Sidebar.vue | 12 ++-- src/styles/editor/note-link.css | 24 +++---- src/utils/icons.ts | 6 ++ src/views/NoteSpace.vue | 2 +- 16 files changed, 231 insertions(+), 58 deletions(-) create mode 100644 src/components/editor/note-children/ChildrenOptions.vue create mode 100644 src/components/editor/note-children/NoteChildren.vue create mode 100644 src/utils/icons.ts diff --git a/components.d.ts b/components.d.ts index e7fb7b9..288b9df 100644 --- a/components.d.ts +++ b/components.d.ts @@ -10,6 +10,7 @@ declare module 'vue' { export interface GlobalComponents { AddLinkDialog: typeof import('./src/components/editor/bubble-menu/AddLinkDialog.vue')['default'] BubbleMenu: typeof import('./src/components/editor/bubble-menu/BubbleMenu.vue')['default'] + ChildrenOptions: typeof import('./src/components/editor/note-children/ChildrenOptions.vue')['default'] Editor: typeof import('./src/components/editor/Editor.vue')['default'] EditorOptions: typeof import('./src/components/editor/EditorOptions.vue')['default'] EditorView: typeof import('./src/components/editor/EditorView.vue')['default'] @@ -19,6 +20,7 @@ declare module 'vue' { GenericErrorMessage: typeof import('./src/components/generic/GenericErrorMessage.vue')['default'] LoadingOverlay: typeof import('./src/components/loading/LoadingOverlay.vue')['default'] NewWorkspaceCard: typeof import('./src/components/workspace/NewWorkspaceCard.vue')['default'] + NoteChildren: typeof import('./src/components/editor/note-children/NoteChildren.vue')['default'] NoteChildrenBig: typeof import('./src/components/editor/note-children/NoteChildrenBig.vue')['default'] NoteChildrenSmall: typeof import('./src/components/editor/note-children/NoteChildrenSmall.vue')['default'] RemoveDialog: typeof import('./src/components/workspace/dialog/RemoveDialog.vue')['default'] diff --git a/package-lock.json b/package-lock.json index dab1719..0958525 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "lowlight": "^3.3.0", "naive-ui": "^2.41.1", "pinia": "^3.0.2", + "prosemirror-markdown": "^1.11.1", "splitpanes": "^4.0.4", "tiptap-markdown": "^0.8.10", "vue": "^3.4.19", diff --git a/package.json b/package.json index 12a6429..f1783dc 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "tiptap-markdown": "^0.8.10", "vue": "^3.4.19", "vue-material-design-icons": "^5.3.1", - "vue-router": "^4.5.1" + "vue-router": "^4.5.1", + "prosemirror-markdown": "^1.11.1" } } \ No newline at end of file diff --git a/src/components/editor/Editor.vue b/src/components/editor/Editor.vue index fad9ea6..5fa9788 100644 --- a/src/components/editor/Editor.vue +++ b/src/components/editor/Editor.vue @@ -1,9 +1,12 @@ @@ -12,13 +15,15 @@ import { ref, watch, onBeforeMount, onBeforeUnmount } from 'vue'; import { useNotesStore } from '../../stores/useNotesStore'; import { Editor, EditorContent } from '@tiptap/vue-3'; import { Note } from '../../services/domain/Note'; -import BubbleMenu from './bubble-menu/BubbleMenu.vue'; import { createEditor } from './createEditor'; +import { EditorView } from '@tiptap/pm/view'; const emit = defineEmits(['note-change', 'note-content-update']); const notesStore = useNotesStore(); const editor = ref(null); const currentNote = ref(null); +const isLoading = ref(false); +const isBubbleMenuVisible = ref(false); function emitNoteChange(previousNote: Note, previousNoteContent: string) { emit('note-change', previousNote, previousNoteContent); @@ -44,16 +49,31 @@ async function handlePdfUpload(filePath: string) { } onBeforeMount(() => { - editor.value = createEditor(notesStore, emitNoteContentUpdate); + editor.value = createEditor(notesStore, emitNoteContentUpdate, { + handleKeyDown(view: EditorView, event: KeyboardEvent) { + if (event.key === 'Tab') { + event.preventDefault(); + view.dispatch( + view.state.tr.insertText('\t', view.state.selection.from, view.state.selection.to) + ); + return true; + } + } + }); + isLoading.value = false; + // Whatch when the current note changes and update the editor content accordingly watch( () => notesStore.currentNote, async (newNote: Note) => { if (newNote && editor.value) { + isLoading.value = true; const content = editor.value.getHTML(); - editor.value.commands.setContent(await notesStore.loadCurrentNoteContent()); + const noteContent = await notesStore.loadCurrentNoteContent(); + editor.value.commands.setContent(noteContent); emitNoteChange(currentNote.value as Note, content); currentNote.value = newNote; + isLoading.value = false; } }, { immediate: true } @@ -66,5 +86,6 @@ onBeforeUnmount(() => { defineExpose({ editor, + isLoading, }) diff --git a/src/components/editor/EditorView.vue b/src/components/editor/EditorView.vue index 112ef9e..7b75dcc 100644 --- a/src/components/editor/EditorView.vue +++ b/src/components/editor/EditorView.vue @@ -1,4 +1,5 @@ diff --git a/src/components/sidebar/Sidebar.vue b/src/components/sidebar/Sidebar.vue index 18493bd..8ce5442 100644 --- a/src/components/sidebar/Sidebar.vue +++ b/src/components/sidebar/Sidebar.vue @@ -29,6 +29,7 @@ import ChevronRight from 'icons/ChevronRight.vue' import Delete from 'icons/Delete.vue' import FilePlusOutline from 'icons/FilePlusOutline.vue'; import DotsHorizontal from 'icons/DotsHorizontal.vue' +import { getNIcon } from '../../utils/icons'; const store = useNotesStore() @@ -60,11 +61,6 @@ watch( { immediate: true } ) -// --- Icon helpers --- -function getNIcon(icon: any) { - return () => h(NIcon, null, { default: () => h(icon) }) -} - // --- Tree option helpers --- function noteToTreeOption(note: Note): TreeOption { let noteChildren = note.children?.map(noteToTreeOption) @@ -192,10 +188,12 @@ function renderLabel({ option }: { option: TreeOption }) { h( NButton, { - class: 'sidebar-action-btn', + class: 'note-options-trigger-btn sidebar-action-btn', size: 'tiny', quaternary: true, - style: 'margin-left: 8px;', + style: `margin-left: 8px; + --n-color-hover: var(--primary-color, #007bff);; + --n-text-color-hover: white;`, onClick: (e: MouseEvent) => e.stopPropagation() }, { default: getNIcon(DotsHorizontal) } diff --git a/src/styles/editor/note-link.css b/src/styles/editor/note-link.css index fb5c300..5939921 100644 --- a/src/styles/editor/note-link.css +++ b/src/styles/editor/note-link.css @@ -1,13 +1,15 @@ -.note-link { - color: #2563eb; - border-radius: 0.35em; - padding: 0.1em 0.4em; - font-weight: 500; - cursor: pointer; - text-decoration: underline double #2563eb; -} +.tiptap { + .note-link { + color: #2563eb; + border-radius: 0.35em; + padding: 0.1em 0.4em; + font-weight: 500; + cursor: pointer; + text-decoration: underline double #2563eb; + } -.note-link:hover { - color: #1e40af; - text-decoration: none; + .note-link:hover { + color: #1e40af; + text-decoration: none; + } } \ No newline at end of file diff --git a/src/utils/icons.ts b/src/utils/icons.ts new file mode 100644 index 0000000..8ec639f --- /dev/null +++ b/src/utils/icons.ts @@ -0,0 +1,6 @@ +import { NIcon } from "naive-ui"; +import { h } from "vue"; + +export const getNIcon = (icon: unknown) => { + return () => h(NIcon, null, { default: () => h(icon) }); +}; \ No newline at end of file diff --git a/src/views/NoteSpace.vue b/src/views/NoteSpace.vue index 1ee35e2..fa4a7c9 100644 --- a/src/views/NoteSpace.vue +++ b/src/views/NoteSpace.vue @@ -18,7 +18,7 @@ -
+
From fbf7e2fa819ee6fd6c45f550064a109865f2a87d Mon Sep 17 00:00:00 2001 From: Raulms29 Date: Mon, 28 Jul 2025 19:51:33 +0200 Subject: [PATCH 03/51] feat: :lipstick: Breadcrumbs added for notes and some UI improvements. --- components.d.ts | 1 + src/components/editor/EditorView.vue | 22 +++++- src/components/sidebar/Sidebar.vue | 70 ++++++++++++++++++- .../sidebar/settings/SidebarSettings.vue | 26 +++++++ src/main.ts | 3 +- src/stores/useNotesStore.ts | 29 ++++++++ src/utils/ipc/exportHandler.ts | 2 +- src/views/NoteSpace.vue | 28 +++++++- 8 files changed, 173 insertions(+), 8 deletions(-) create mode 100644 src/components/sidebar/settings/SidebarSettings.vue diff --git a/components.d.ts b/components.d.ts index 288b9df..d696ecb 100644 --- a/components.d.ts +++ b/components.d.ts @@ -30,6 +30,7 @@ declare module 'vue' { RouterView: typeof import('vue-router')['RouterView'] Sidebar: typeof import('./src/components/sidebar/Sidebar.vue')['default'] SidebarButtons: typeof import('./src/components/sidebar/SidebarButtons.vue')['default'] + SidebarSettings: typeof import('./src/components/sidebar/settings/SidebarSettings.vue')['default'] Spinner: typeof import('./src/components/loading/Spinner.vue')['default'] WorkspaceCard: typeof import('./src/components/workspace/WorkspaceCard.vue')['default'] WorkspaceMenuItem: typeof import('./src/components/workspace/WorkspaceMenuItem.vue')['default'] diff --git a/src/components/editor/EditorView.vue b/src/components/editor/EditorView.vue index 7b75dcc..e1fce65 100644 --- a/src/components/editor/EditorView.vue +++ b/src/components/editor/EditorView.vue @@ -12,6 +12,15 @@
+ +
@@ -23,8 +32,7 @@
+ @delete="notesStore.deleteNote($event as Note)" @create="handleCreateNote($event as Note)" />
@@ -90,6 +98,11 @@ function toggleFocusMode() { settingsStore.updateSetting('focusMode', !settingsStore.settings.focusMode); } +async function handleCreateNote(note: Note) { + const createdNote = await notesStore.createNote(undefined, note); + notesStore.selectNote(createdNote); +} + import '../../styles/editor.css'; import NoteChildren from './note-children/NoteChildren.vue'; @@ -216,4 +229,9 @@ import NoteChildren from './note-children/NoteChildren.vue'; height: auto; margin-bottom: 0.5rem; } + +.breadcrumb { + display: flex; + justify-content: center; +} \ No newline at end of file diff --git a/src/components/sidebar/Sidebar.vue b/src/components/sidebar/Sidebar.vue index 8ce5442..25909c0 100644 --- a/src/components/sidebar/Sidebar.vue +++ b/src/components/sidebar/Sidebar.vue @@ -16,7 +16,10 @@ :pattern="pattern" :selected-keys="selectedKeys" :show-irrelevant-nodes="false" style="--n-drop-mark-color: #1976d2;" />
+
+ + + + diff --git a/src/main.ts b/src/main.ts index 5ac993a..f9be918 100644 --- a/src/main.ts +++ b/src/main.ts @@ -104,7 +104,8 @@ if (!gotTheLock) { registerSettingsHandlers(); registerExportHandlers(); // Register window handlers and create the browser window - registerWindowHandlers(createWindow()); + const mainWindow = createWindow(); + registerWindowHandlers(mainWindow); let workspaceRoot: string | null = null; ipcMain.handle('set-workspace-root', (_, rootPath: string) => { diff --git a/src/stores/useNotesStore.ts b/src/stores/useNotesStore.ts index f49d299..fff8f7f 100644 --- a/src/stores/useNotesStore.ts +++ b/src/stores/useNotesStore.ts @@ -179,6 +179,18 @@ export const useNotesStore = defineStore('notes', () => { return [filePathC, fileName]; } + function getNoteBreadcrumb(note: Note): Note[] { + if (!note) { + throw new Error('No note provided to get breadcrumb for.'); + } + const path: Note[] = []; + if (!notes.value || !findBreadCrumb(notes.value, note.id, path)) { + throw new Error('Note not found in the note tree.'); + } + + return path; + } + // --- PRIVATE/HELPER FUNCTIONS --- function removeNoteFromTree(noteToDelete: Note): boolean { let removed = false; @@ -238,6 +250,22 @@ export const useNotesStore = defineStore('notes', () => { return cleanedPath; } + function findBreadCrumb(currentNotes: Note[], targetId: string, path: Note[]): boolean { + for (const n of currentNotes) { + path.push(n); + if (n.id === targetId) { + return true; + } + if (n.children && n.children.length > 0) { + if (findBreadCrumb(n.children, targetId, path)) { + return true; + } + } + path.pop(); + } + return false; + } + return { noteTree: notes, currentNote, @@ -257,5 +285,6 @@ export const useNotesStore = defineStore('notes', () => { getNoteById, saveImage, savePDF, + getNoteBreadcrumb }; }); diff --git a/src/utils/ipc/exportHandler.ts b/src/utils/ipc/exportHandler.ts index 5e1708d..ab6e7e5 100644 --- a/src/utils/ipc/exportHandler.ts +++ b/src/utils/ipc/exportHandler.ts @@ -31,4 +31,4 @@ export function registerExportHandlers() { win.destroy(); } }); -} +} \ No newline at end of file diff --git a/src/views/NoteSpace.vue b/src/views/NoteSpace.vue index fa4a7c9..75b1e62 100644 --- a/src/views/NoteSpace.vue +++ b/src/views/NoteSpace.vue @@ -1,7 +1,7 @@ \ No newline at end of file From fd89839585aad21d5df4c2175ef0ef8a11cbfef5 Mon Sep 17 00:00:00 2001 From: Raulms29 Date: Wed, 5 Nov 2025 13:42:19 +0100 Subject: [PATCH 43/51] ci: :stethoscope: Changed sonar config --- index.html | 23 ++++++++++++---------- src/components/sidebar/Sidebar.vue | 23 ++++++++++------------ src/components/sidebar/SidebarSettings.vue | 3 +-- src/main.ts | 11 +++++++---- src/preload.ts | 4 ++-- src/types/preload.d.ts | 2 +- src/utils/ipc/windowHandler.ts | 4 ++-- src/views/NoteSpace.vue | 16 ++++----------- src/views/Workspace.vue | 12 +++++------ test/e2e/workspace.e2e.ts | 2 +- 10 files changed, 47 insertions(+), 53 deletions(-) diff --git a/index.html b/index.html index d0ced27..7501a04 100644 --- a/index.html +++ b/index.html @@ -1,12 +1,15 @@ - - - - Note App - - -
- - - + + + + + Slate + + + +
+ + + + \ No newline at end of file diff --git a/src/components/sidebar/Sidebar.vue b/src/components/sidebar/Sidebar.vue index 189c1a7..8928a1e 100644 --- a/src/components/sidebar/Sidebar.vue +++ b/src/components/sidebar/Sidebar.vue @@ -2,14 +2,13 @@