From 7a50cdd00727d5565b38c2203242415751cc9f52 Mon Sep 17 00:00:00 2001 From: 7eliassen Date: Thu, 20 Aug 2026 16:36:02 +0300 Subject: [PATCH] feat: add configuration for imageTool --- src/application/services/useNoteEditor.ts | 74 +++++++++++++++++-- src/domain/entities/EditorTool.ts | 2 +- ...AttachmentUploader.repository.interface.ts | 8 ++ src/domain/noteSettings.service.ts | 13 ++++ .../noteAttachmentUploader.repository.ts | 10 +++ .../transport/fetch.transport.ts | 7 ++ src/presentation/pages/HistoryVersion.vue | 1 + src/presentation/pages/Note.vue | 1 + 8 files changed, 108 insertions(+), 8 deletions(-) diff --git a/src/application/services/useNoteEditor.ts b/src/application/services/useNoteEditor.ts index a5fb4abc..d78a66dd 100644 --- a/src/application/services/useNoteEditor.ts +++ b/src/application/services/useNoteEditor.ts @@ -2,8 +2,8 @@ import { type Ref, computed, ref, toValue, watch } from 'vue'; import { useAppState } from './useAppState'; import type EditorTool from '@/domain/entities/EditorTool'; import { type NoteContent } from '@/domain/entities/Note'; -import { editorToolsService } from '@/domain'; -import type { EditorjsToolsConfig } from '@/domain/entities/EditorTool'; +import { editorToolsService, noteSettingsService } from '@/domain'; +import type { EditorjsConfigTool, EditorjsToolsConfig } from '@/domain/entities/EditorTool'; import { useI18n } from 'vue-i18n'; interface UseNoteEditorOptions { @@ -21,6 +21,12 @@ interface UseNoteEditorOptions { * Flag indicating that user can edit the note */ canEdit: Ref; + + /** + * Note id to build image upload endpoint + * Null for new notes + */ + noteId: Ref; } interface UseNoteEditorComposableState { @@ -131,13 +137,67 @@ export const useNoteEditor = function useNoteEditor(options: UseNoteEditorOption return Object.fromEntries( loadedToolsWithoutParagraph - .map(toolClassAndInfo => [ - toolClassAndInfo.tool.name, - { + .map((toolClassAndInfo) => { + const toolConfig: { class: EditorjsConfigTool; inlineToolbar: boolean; config?: Record } = { class: toolClassAndInfo.class, inlineToolbar: true, - }, - ]) + }; + + /** + * Add image tool config with a custom uploader + * that uses the authorized note attachment upload method + */ + if (toolClassAndInfo.tool.name === 'image') { + const noteId = toValue(options.noteId); + + if (noteId !== null) { + toolConfig.config = { + /** + * The image tool internally accesses `this.config.endpoints.byFile` + * when a file is selected, even when a custom uploader is provided + * Provide an empty endpoints object to prevent a TypeError + */ + endpoints: { + byFile: '', + }, + features: { + caption: 'optional', + }, + uploader: { + /** + * Uploads file using the existing authorized repository method + * @param file - file selected in the editor + */ + uploadByFile: async (file: File): Promise<{ success: 1; file: { url: string } }> => { + const url = await noteSettingsService.uploadImage(noteId, file); + + return { + success: 1, + file: { + url, + }, + }; + }, + /** + * When a user pastes an image URL, use it as-is + * without uploading the image to the server + * @param url - image URL pasted by the user + */ + uploadByUrl: (url: string): Promise<{ success: 1; file: { url: string } }> => { + return Promise.resolve({ + success: 1, + file: { + url, + }, + }); + }, + }, + }; + } + } + + return [toolClassAndInfo.tool.name, toolConfig]; + }) ); } diff --git a/src/domain/entities/EditorTool.ts b/src/domain/entities/EditorTool.ts index 16bd55c9..cde8d9f2 100644 --- a/src/domain/entities/EditorTool.ts +++ b/src/domain/entities/EditorTool.ts @@ -81,7 +81,7 @@ export type EditorjsConfigTool = ToolSettings | ToolConstructable; /** * Editor.js tools config — map of tool name to its class and inline toolbar flag */ -export type EditorjsToolsConfig = Record; +export type EditorjsToolsConfig = Record }>; /** * Editor tool info alogn with its plugin's class ready to use diff --git a/src/domain/noteAttachmentUploader.repository.interface.ts b/src/domain/noteAttachmentUploader.repository.interface.ts index dd510ba2..93abe219 100644 --- a/src/domain/noteAttachmentUploader.repository.interface.ts +++ b/src/domain/noteAttachmentUploader.repository.interface.ts @@ -19,4 +19,12 @@ export default interface NoteAttachmentUploaderRepositoryInterface { * @returns file binary data */ load: (noteId: Note['id'], key: string) => Promise; + + /** + * Build a URL to load the note attachment by key + * @param noteId - identifier for note to get attachment + * @param key - file key on server side + * @returns URL of the attachment + */ + getFileUrl: (noteId: Note['id'], key: string) => string; } diff --git a/src/domain/noteSettings.service.ts b/src/domain/noteSettings.service.ts index bede251f..5a1520d8 100644 --- a/src/domain/noteSettings.service.ts +++ b/src/domain/noteSettings.service.ts @@ -80,6 +80,19 @@ export default class NoteSettingsService { return await this.patchNoteSettingsByNoteId(id, { cover: key }); } + /** + * Upload an image to be used in the note editor + * Uses the authorized transport via noteAttachmentRepository + * @param id - note id + * @param data - image binary data + * @returns URL of the uploaded image that can be rendered in the editor + */ + public async uploadImage(id: NoteId, data: Blob): Promise { + const key = await this.noteAttachmentRepository.upload(id, data); + + return this.noteAttachmentRepository.getFileUrl(id, key); + } + /** * Revoke invitation hash * @param id - Note id diff --git a/src/infrastructure/noteAttachmentUploader.repository.ts b/src/infrastructure/noteAttachmentUploader.repository.ts index 241ad4b3..086dc862 100644 --- a/src/infrastructure/noteAttachmentUploader.repository.ts +++ b/src/infrastructure/noteAttachmentUploader.repository.ts @@ -53,4 +53,14 @@ export default class NoteAttachmentUploaderRepository implements NoteAttachmentU public async load(noteId: Note['id'], key: string): Promise { return await this.transport.getBlob(`/upload/${noteId}/${key}`); } + + /** + * Build a URL to load the note attachment by key + * @param noteId - identifier for note to get attachment + * @param key - file key on server side + * @returns URL of the attachment + */ + public getFileUrl(noteId: Note['id'], key: string): string { + return this.transport.getBaseUrl() + `/upload/${noteId}/${key}`; + } } diff --git a/src/infrastructure/transport/fetch.transport.ts b/src/infrastructure/transport/fetch.transport.ts index 1dbe00a9..c24ede3a 100644 --- a/src/infrastructure/transport/fetch.transport.ts +++ b/src/infrastructure/transport/fetch.transport.ts @@ -32,6 +32,13 @@ export default class FetchTransport { private readonly options?: FetchTransportOptions ) {} + /** + * Returns the base URL of the transport + */ + public getBaseUrl(): string { + return this.baseUrl; + } + /** * Gets specific resource * @template Response - Response data type diff --git a/src/presentation/pages/HistoryVersion.vue b/src/presentation/pages/HistoryVersion.vue index c0a32343..83a754c3 100644 --- a/src/presentation/pages/HistoryVersion.vue +++ b/src/presentation/pages/HistoryVersion.vue @@ -86,6 +86,7 @@ const { isEditorReady, editorConfig } = useNoteEditor({ noteTools: historyTools, noteContentResolver: () => historyContent.value, canEdit, + noteId, }); /** diff --git a/src/presentation/pages/Note.vue b/src/presentation/pages/Note.vue index d04c68a5..b97d54cc 100644 --- a/src/presentation/pages/Note.vue +++ b/src/presentation/pages/Note.vue @@ -129,6 +129,7 @@ const { isEditorReady, editorConfig } = useNoteEditor({ noteTools, noteContentResolver: () => note.value?.content, canEdit, + noteId, }); /**