diff --git a/src/application/i18n/messages/en.json b/src/application/i18n/messages/en.json index 3ff0468b..e72e0db1 100644 --- a/src/application/i18n/messages/en.json +++ b/src/application/i18n/messages/en.json @@ -4,7 +4,8 @@ }, "settings": { "title": "Settings", - "changeTheme": "Change theme" + "changeTheme": "Change theme", + "userEditorTools": "User editor tools" }, "header": { "buttons": { diff --git a/src/application/services/useAppState.ts b/src/application/services/useAppState.ts index fb37d479..ff14989e 100644 --- a/src/application/services/useAppState.ts +++ b/src/application/services/useAppState.ts @@ -1,4 +1,5 @@ import { AppStateController } from '@/domain'; +import type EditorTool from '@/domain/entities/EditorTool'; import type { User } from '@/domain/entities/User'; import { createSharedComposable } from '@vueuse/core'; import { type Ref, ref } from 'vue'; @@ -11,6 +12,11 @@ interface UseAppStateComposable { * Current authenticated user */ user: Ref; + + /** + * User editor tools that are used in notes creation + */ + userEditorTools: Ref } /** @@ -22,16 +28,25 @@ export const useAppState = createSharedComposable((): UseAppStateComposable => { */ const user = ref(null); + /** + * User editor tools that are used in notes creation + */ + const userEditorTools = ref([]); + /** * Subscribe to user changes in the App State */ - AppStateController.user((prop: 'user', value: User | null) => { + AppStateController.user((prop: 'user' | 'editorTools', value: User | EditorTool[] | null) => { if (prop === 'user') { - user.value = value; + user.value = value as User; + } + if (prop === 'editorTools') { + userEditorTools.value = value as EditorTool[]; } }); return { user, + userEditorTools, }; }); diff --git a/src/application/services/useUserSettings.ts b/src/application/services/useUserSettings.ts new file mode 100644 index 00000000..85a99c3b --- /dev/null +++ b/src/application/services/useUserSettings.ts @@ -0,0 +1,30 @@ +import { userService } from '@/domain'; + +/** + * User settings hook state + */ +interface UseUserSettingsComposableState { + /** + * Add tool to the user settings + */ + addTool(id: string): void +} + + +/** + * Methods for working with user settings + */ +export function useUserSettings(): UseUserSettingsComposableState { + /** + * Add tool to the user settings + * + * @param id - Tool identifier + */ + function addTool(id: string): void { + userService.addTool(id); + } + + return { + addTool, + }; +} diff --git a/src/domain/entities/EditorTool.ts b/src/domain/entities/EditorTool.ts new file mode 100644 index 00000000..e272ec9f --- /dev/null +++ b/src/domain/entities/EditorTool.ts @@ -0,0 +1,40 @@ +/** + * Plugin that connects to the editor based on user settings + */ +export default interface EditorTool { + /** + * Unique identifier of the tool. Nano-ID + */ + id: string; + + /** + * Technical name of the tool, like 'header', 'list', 'linkTool' + */ + name: string; + + /** + * User-friendly plugin title + */ + title: string; + + /** + * Name of the tool class. Since it's imported globally, + * we need the class name to properly connect the tool to the editor + */ + exportName: string; + + /** + * Is plugin included by default in the editor + */ + isDefault?: boolean; + + /** + * Source of the tool to get it's code + */ + source: { + /** + * Tool URL in content delivery network + */ + cdn?: string; + } +} diff --git a/src/domain/user.repository.interface.ts b/src/domain/user.repository.interface.ts index 89baf1d6..61e45091 100644 --- a/src/domain/user.repository.interface.ts +++ b/src/domain/user.repository.interface.ts @@ -1,3 +1,4 @@ +import type EditorTool from './entities/EditorTool'; import type { User } from './entities/User'; /** @@ -13,4 +14,21 @@ export default interface UserRepositoryInterface { * Return stored user data */ getUser: () => User | null; + + /** + * Loads and store editor tools from user extensions + */ + loadUserEditorTools: () => Promise; + + /** + * Returns array of editor tools + */ + getUserEditorTools: () => EditorTool[]; + + /** + * Adds a tool to the user (marketplace mock) + * + * @param id - tool id + */ + addTool: (id: string) => void; } diff --git a/src/domain/user.service.ts b/src/domain/user.service.ts index 9818875d..1ae88013 100644 --- a/src/domain/user.service.ts +++ b/src/domain/user.service.ts @@ -26,6 +26,7 @@ export default class UserService { */ eventBus.addEventListener(AUTH_COMPLETED_EVENT_NAME, () => { void this.repository.loadUser(); + void this.repository.loadUserEditorTools(); }); } @@ -35,4 +36,13 @@ export default class UserService { public getUser(): User | null { return this.repository.getUser(); } + + /** + * Adds a tool to the user (marketplace mock) + * + * @param id - tool id + */ + public addTool(id: string): void { + this.repository.addTool(id); + } } diff --git a/src/infrastructure/storage/user.ts b/src/infrastructure/storage/user.ts index 9f90a3c9..2496d66b 100644 --- a/src/infrastructure/storage/user.ts +++ b/src/infrastructure/storage/user.ts @@ -1,5 +1,6 @@ import type { User } from '@/domain/entities/User'; import { SubscribableStore } from './abstract/subscribable'; +import type EditorTool from '@/domain/entities/EditorTool'; /** * Data stored in the user store @@ -9,6 +10,11 @@ export type UserStoreData = { * User data */ user: User | null; + + /** + * User editor tools that are used in notes creation + */ + editorTools: EditorTool[]; }; /** @@ -30,4 +36,20 @@ export class UserStore extends SubscribableStore { public setUser(user: User): void { this.data.user = user; } + + /** + * Array of tools + */ + public getUserEditorTools(): EditorTool[] { + return this.data.editorTools; + } + + /** + * Set editor tools that are used in notes creation + * + * @param editorTools - editor plugins + */ + public setUserEditorTools(editorTools: EditorTool[]): void { + this.data.editorTools = editorTools; + } } diff --git a/src/infrastructure/user.repository.ts b/src/infrastructure/user.repository.ts index 68fc057d..1f0374b3 100644 --- a/src/infrastructure/user.repository.ts +++ b/src/infrastructure/user.repository.ts @@ -3,11 +3,12 @@ import type NotesApiTransport from './transport/notes-api'; import type { UserStore, UserStoreData } from './storage/user'; import type { User } from '@/domain/entities/User'; import Repository from './repository'; +import type EditorTool from '@/domain/entities/EditorTool'; /** * Facade for the user data */ -export default class UserRepository extends Repository implements UserRepositoryInterface { +export default class UserRepository extends Repository implements UserRepositoryInterface { /** * Transport instance */ @@ -37,7 +38,36 @@ export default class UserRepository extends Repository /** * Load user data and put it to the storage */ - public getUser(): User | null { + public getUser(): User | null { return this.store.getUser(); } + + /** + * Load tools and set it + */ + public async loadUserEditorTools(): Promise { + const response = await this.transport.get<{ data: EditorTool[] }>('/user/editor-tools'); + + this.store.setUserEditorTools(response.data); + } + + /** + * Returns current user editor tools + */ + public getUserEditorTools(): EditorTool[] { + return this.store.getUserEditorTools(); + } + + /** + * Adds a tool to the user (marketplace mock) + * + * @param id - tool id + */ + public async addTool(id: string): Promise { + const response = await this.transport.post<{toolId: string}>('/user/editor-tools', { + toolId: id, + }); + + console.log('Add tool response', response); + } } diff --git a/src/presentation/components/editor/Editor.vue b/src/presentation/components/editor/Editor.vue index 3e2d7418..f98f0594 100644 --- a/src/presentation/components/editor/Editor.vue +++ b/src/presentation/components/editor/Editor.vue @@ -8,32 +8,54 @@ import Editor, { type OutputData, type API } from '@editorjs/editorjs'; // @ts-expect-error: we need to rewrite plugins to TS to get their types import Header from '@editorjs/header'; -// @ts-expect-error: we need to rewrite plugins to TS to get their types -import Image from '@editorjs/image'; -// @ts-expect-error: we need to rewrite plugins to TS to get their types -import CodeTool from '@editorjs/code'; -// @ts-expect-error: we need to rewrite plugins to TS to get their types -import List from '@editorjs/list'; -// @ts-expect-error: we need to rewrite plugins to TS to get their types -import Delimiter from '@editorjs/delimiter'; -// @ts-expect-error: we need to rewrite plugins to TS to get their types -import Table from '@editorjs/table'; -// @ts-expect-error: we need to rewrite plugins to TS to get their types -import Warning from '@editorjs/warning'; -// @ts-expect-error: we need to rewrite plugins to TS to get their types -import Checklist from '@editorjs/checklist'; -// @ts-expect-error: we need to rewrite plugins to TS to get their types -import LinkTool from '@editorjs/link'; -// @ts-expect-error: we need to rewrite plugins to TS to get their types -import RawTool from '@editorjs/raw'; -// @ts-expect-error: we need to rewrite plugins to TS to get their types -import Embed from '@editorjs/embed'; -// @ts-expect-error: we need to rewrite plugins to TS to get their types -import InlineCode from '@editorjs/inline-code'; -// @ts-expect-error: we need to rewrite plugins to TS to get their types -import Marker from '@editorjs/marker'; +// // @ts-expect-error: we need to rewrite plugins to TS to get their types +// import Image from '@editorjs/image'; +// // @ts-expect-error: we need to rewrite plugins to TS to get their types +// import CodeTool from '@editorjs/code'; +// // @ts-expect-error: we need to rewrite plugins to TS to get their types +// import List from '@editorjs/list'; +// // @ts-expect-error: we need to rewrite plugins to TS to get their types +// import Delimiter from '@editorjs/delimiter'; +// // @ts-expect-error: we need to rewrite plugins to TS to get their types +// import Table from '@editorjs/table'; +// // @ts-expect-error: we need to rewrite plugins to TS to get their types +// import Warning from '@editorjs/warning'; +// // @ts-expect-error: we need to rewrite plugins to TS to get their types +// import Checklist from '@editorjs/checklist'; +// // @ts-expect-error: we need to rewrite plugins to TS to get their types +// import LinkTool from '@editorjs/link'; +// // @ts-expect-error: we need to rewrite plugins to TS to get their types +// import RawTool from '@editorjs/raw'; +// // @ts-expect-error: we need to rewrite plugins to TS to get their types +// import Embed from '@editorjs/embed'; +// // @ts-expect-error: we need to rewrite plugins to TS to get their types +// import InlineCode from '@editorjs/inline-code'; +// // @ts-expect-error: we need to rewrite plugins to TS to get their types +// import Marker from '@editorjs/marker'; + +import EditorTool from '@/domain/entities/EditorTool'; +import { useAppState } from '@/application/services/useAppState'; + + +const { userEditorTools } = useAppState(); +/** + * Load one tool at a time + * + * @param src - source path to tool + */ +function loadScript(src: string) { + return new Promise(function (resolve, reject) { + const editorToolScript = document.createElement('script'); + + editorToolScript.src = src; + editorToolScript.onload = resolve; + editorToolScript.onerror = reject; + document.head.appendChild(editorToolScript); + }); +} + /** * Define the props for the component */ @@ -104,42 +126,79 @@ async function onChange(api: API): Promise { emit('change', data); } -onMounted(async () => { - const editorInstance = new Editor({ +const isEditorMounted = ref(false); + +const mountEditorOnce = async () => { + console.log('mount'); + isEditorMounted.value = true; + + Promise.allSettled(userEditorTools.value.map((spec: EditorTool) => { + if (!spec.source.cdn) { + return; + } + + return loadScript(spec.source.cdn); + })).then(async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const loadedTools: {[key: string]: any } = userEditorTools.value.reduce( + (acc, spec: EditorTool) => { + // @ts-expect-error: we need to rewrite plugins to TS to get their types + const windowPlugin = window[spec.exportName]; + + return { + ...acc, + [spec.title]: windowPlugin, + }; + }, + {} + ); + + + const editorInstance = new Editor({ /** * Block Tools */ - tools: { - header: { - class: Header, - config: { - placeholder: 'Title...', + tools: { + header: { + class: Header, + config: { + placeholder: 'Title...', + }, }, + // image: Image, + // code: CodeTool, + // list: List, + // delimiter: Delimiter, + // table: Table, + // warning: Warning, + // checklist: Checklist, + // linkTool: LinkTool, + // raw: RawTool, + // embed: Embed, + + // /** + // * Inline Tools + // */ + // inlineCode: InlineCode, + // marker: Marker, + ...loadedTools, }, - image: Image, - code: CodeTool, - list: List, - delimiter: Delimiter, - table: Table, - warning: Warning, - checklist: Checklist, - linkTool: LinkTool, - raw: RawTool, - embed: Embed, - - /** - * Inline Tools - */ - inlineCode: InlineCode, - marker: Marker, - }, - data: props.content, - onChange, - }); + data: props.content, + onChange, + }); - await editorInstance.isReady; + await editorInstance.isReady; - editor.value = editorInstance; + editor.value = editorInstance; + }); +}; + +watch(userEditorTools, mountEditorOnce); +onMounted(() => { + console.log('mount', userEditorTools.value); + if (userEditorTools.value.length > 0) { + mountEditorOnce(); + } }); watch(() => props.content, (content) => { @@ -169,5 +228,4 @@ defineExpose({ }); - + diff --git a/src/presentation/pages/Settings.vue b/src/presentation/pages/Settings.vue index cadbd555..0b95644f 100644 --- a/src/presentation/pages/Settings.vue +++ b/src/presentation/pages/Settings.vue @@ -1,14 +1,76 @@