From 811a043ed4c2a547aafc234e465f84dc12cece32 Mon Sep 17 00:00:00 2001 From: GeekaN2 Date: Sun, 22 Oct 2023 00:36:35 +0300 Subject: [PATCH 1/7] feat: show editor tools in user settings --- src/application/i18n/messages/en.json | 3 +- src/application/services/useAppState.ts | 12 ++++++-- src/domain/entities/EditorTool.ts | 40 +++++++++++++++++++++++++ src/domain/user.repository.interface.ts | 5 ++++ src/domain/user.service.ts | 1 + src/infrastructure/storage/user.ts | 19 ++++++++++++ src/infrastructure/user.repository.ts | 21 +++++++++++-- src/presentation/pages/Settings.vue | 17 +++++++++-- 8 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 src/domain/entities/EditorTool.ts diff --git a/src/application/i18n/messages/en.json b/src/application/i18n/messages/en.json index a72d9e55..2f4d2ebb 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" }, "home": { "title": "Home", diff --git a/src/application/services/useAppState.ts b/src/application/services/useAppState.ts index fb37d479..91f86b28 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,8 @@ interface UseAppStateComposable { * Current authenticated user */ user: Ref; + + userEditorTools: Ref } /** @@ -21,17 +24,22 @@ export const useAppState = createSharedComposable((): UseAppStateComposable => { * Current authenticated user */ const user = ref(null); + 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/domain/entities/EditorTool.ts b/src/domain/entities/EditorTool.ts new file mode 100644 index 00000000..4a8acbf0 --- /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 id of the tool, like 'header', 'list', 'linkTool' + */ + pluginId: string; + + /** + * User-friendly plugin title + */ + name: string; + + /** + * Name of the tool class. Since it's imported globally, + * we need the class name to properly connect the tool to the editor + */ + class: 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..f13e0cee 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,8 @@ export default interface UserRepositoryInterface { * Return stored user data */ getUser: () => User | null; + + loadUserEditorTools: () => Promise; + + getUserEditorTools: () => EditorTool[]; } diff --git a/src/domain/user.service.ts b/src/domain/user.service.ts index 9818875d..d2cd5038 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(); }); } diff --git a/src/infrastructure/storage/user.ts b/src/infrastructure/storage/user.ts index 9f90a3c9..15f2b262 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,8 @@ export type UserStoreData = { * User data */ user: User | null; + + editorTools: EditorTool[]; }; /** @@ -30,4 +33,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; + } + + /** + * Setter + * + * @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..9bb9de86 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,23 @@ 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 + */ + public async loadUserEditorTools(): Promise { + const response = await this.transport.get<{ data: EditorTool[] }>('/user/editor-tools'); + + this.store.setUserEditorTools(response.data); + } + + /** + * load tools + */ + public getUserEditorTools(): EditorTool[] { + return this.store.getUserEditorTools(); + } } diff --git a/src/presentation/pages/Settings.vue b/src/presentation/pages/Settings.vue index cadbd555..315fa79b 100644 --- a/src/presentation/pages/Settings.vue +++ b/src/presentation/pages/Settings.vue @@ -1,14 +1,25 @@ - + From 7f1f2f522777885424b7b33fd6b20f8494e034f9 Mon Sep 17 00:00:00 2001 From: GeekaN2 Date: Sun, 22 Oct 2023 01:23:06 +0300 Subject: [PATCH 2/7] feat: editor tools better naming --- src/domain/entities/EditorTool.ts | 8 ++++---- src/domain/user.repository.interface.ts | 6 ++++++ src/infrastructure/storage/user.ts | 4 ++-- src/presentation/pages/Settings.vue | 2 +- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/domain/entities/EditorTool.ts b/src/domain/entities/EditorTool.ts index 4a8acbf0..e272ec9f 100644 --- a/src/domain/entities/EditorTool.ts +++ b/src/domain/entities/EditorTool.ts @@ -8,20 +8,20 @@ export default interface EditorTool { id: string; /** - * Technical id of the tool, like 'header', 'list', 'linkTool' + * Technical name of the tool, like 'header', 'list', 'linkTool' */ - pluginId: string; + name: string; /** * User-friendly plugin title */ - name: string; + 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 */ - class: string; + exportName: string; /** * Is plugin included by default in the editor diff --git a/src/domain/user.repository.interface.ts b/src/domain/user.repository.interface.ts index f13e0cee..6883a6df 100644 --- a/src/domain/user.repository.interface.ts +++ b/src/domain/user.repository.interface.ts @@ -15,7 +15,13 @@ export default interface UserRepositoryInterface { */ getUser: () => User | null; + /** + * Loads and store editor tools from user extensions + */ loadUserEditorTools: () => Promise; + /** + * Returns array of editor tools + */ getUserEditorTools: () => EditorTool[]; } diff --git a/src/infrastructure/storage/user.ts b/src/infrastructure/storage/user.ts index 15f2b262..c5da095d 100644 --- a/src/infrastructure/storage/user.ts +++ b/src/infrastructure/storage/user.ts @@ -35,14 +35,14 @@ export class UserStore extends SubscribableStore { } /** - * array of tools + * Array of tools */ public getUserEditorTools(): EditorTool[] { return this.data.editorTools; } /** - * Setter + * Set editor tools that are used in notes creation * * @param editorTools - editor plugins */ diff --git a/src/presentation/pages/Settings.vue b/src/presentation/pages/Settings.vue index 315fa79b..a2df3072 100644 --- a/src/presentation/pages/Settings.vue +++ b/src/presentation/pages/Settings.vue @@ -6,7 +6,7 @@ :key="tool.id" >
  • - {{ tool.name }} + {{ tool.title }}
  • From 06400121a9f9d3cdf391211a2161de1e7e20de30 Mon Sep 17 00:00:00 2001 From: GeekaN2 Date: Sun, 22 Oct 2023 02:14:08 +0300 Subject: [PATCH 3/7] fix: added comments --- src/application/services/useAppState.ts | 4 ++++ src/infrastructure/storage/user.ts | 3 +++ 2 files changed, 7 insertions(+) diff --git a/src/application/services/useAppState.ts b/src/application/services/useAppState.ts index 91f86b28..3493a363 100644 --- a/src/application/services/useAppState.ts +++ b/src/application/services/useAppState.ts @@ -24,6 +24,10 @@ export const useAppState = createSharedComposable((): UseAppStateComposable => { * Current authenticated user */ const user = ref(null); + + /** + * User editor tools that are used in notes creation + */ const userEditorTools = ref([]); /** diff --git a/src/infrastructure/storage/user.ts b/src/infrastructure/storage/user.ts index c5da095d..2496d66b 100644 --- a/src/infrastructure/storage/user.ts +++ b/src/infrastructure/storage/user.ts @@ -11,6 +11,9 @@ export type UserStoreData = { */ user: User | null; + /** + * User editor tools that are used in notes creation + */ editorTools: EditorTool[]; }; From cbb802435daeb0b8f42584eabf3bff3580dbbd52 Mon Sep 17 00:00:00 2001 From: GeekaN2 Date: Sun, 22 Oct 2023 02:16:01 +0300 Subject: [PATCH 4/7] fix: add comment --- src/application/services/useAppState.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/application/services/useAppState.ts b/src/application/services/useAppState.ts index 3493a363..ff14989e 100644 --- a/src/application/services/useAppState.ts +++ b/src/application/services/useAppState.ts @@ -13,6 +13,9 @@ interface UseAppStateComposable { */ user: Ref; + /** + * User editor tools that are used in notes creation + */ userEditorTools: Ref } From db047a236d13ef36013462af17a0db7f1215b8b8 Mon Sep 17 00:00:00 2001 From: GeekaN2 Date: Sun, 22 Oct 2023 02:23:37 +0300 Subject: [PATCH 5/7] fix: comments in user repository --- src/infrastructure/user.repository.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/infrastructure/user.repository.ts b/src/infrastructure/user.repository.ts index 9bb9de86..46d1d06e 100644 --- a/src/infrastructure/user.repository.ts +++ b/src/infrastructure/user.repository.ts @@ -43,7 +43,7 @@ export default class UserRepository extends Repository } /** - * load tools + * Load tools and set it */ public async loadUserEditorTools(): Promise { const response = await this.transport.get<{ data: EditorTool[] }>('/user/editor-tools'); @@ -52,7 +52,7 @@ export default class UserRepository extends Repository } /** - * load tools + * Returns current user editor tools */ public getUserEditorTools(): EditorTool[] { return this.store.getUserEditorTools(); From 527ce589717b1493349f7705fde75910ab6e8c43 Mon Sep 17 00:00:00 2001 From: GeekaN2 Date: Sun, 22 Oct 2023 22:57:04 +0300 Subject: [PATCH 6/7] feat: watch user tools --- src/presentation/components/editor/Editor.vue | 168 ++++++++++++------ src/presentation/pages/Settings.vue | 6 +- 2 files changed, 118 insertions(+), 56 deletions(-) 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 a2df3072..dd272f10 100644 --- a/src/presentation/pages/Settings.vue +++ b/src/presentation/pages/Settings.vue @@ -9,6 +9,10 @@ {{ tool.title }} + @@ -18,8 +22,8 @@ import ThemeButton from '@/presentation/components/theme/ThemeButton.vue'; import { useAppState } from '@/application/services/useAppState'; const { userEditorTools } = useAppState(); - const { t } = useI18n(); + From 017d04226e1b600e9ff049d12cf349f589ca14df Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Mon, 23 Oct 2023 00:37:12 +0300 Subject: [PATCH 7/7] feat(user-settings): marketplace imitations --- src/application/services/useUserSettings.ts | 30 +++++++++++ src/domain/user.repository.interface.ts | 7 +++ src/domain/user.service.ts | 9 ++++ src/infrastructure/user.repository.ts | 13 +++++ src/presentation/pages/Settings.vue | 57 +++++++++++++++++++-- 5 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 src/application/services/useUserSettings.ts 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/user.repository.interface.ts b/src/domain/user.repository.interface.ts index 6883a6df..61e45091 100644 --- a/src/domain/user.repository.interface.ts +++ b/src/domain/user.repository.interface.ts @@ -24,4 +24,11 @@ export default interface UserRepositoryInterface { * 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 d2cd5038..1ae88013 100644 --- a/src/domain/user.service.ts +++ b/src/domain/user.service.ts @@ -36,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/user.repository.ts b/src/infrastructure/user.repository.ts index 46d1d06e..1f0374b3 100644 --- a/src/infrastructure/user.repository.ts +++ b/src/infrastructure/user.repository.ts @@ -57,4 +57,17 @@ export default class UserRepository extends Repository 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/pages/Settings.vue b/src/presentation/pages/Settings.vue index dd272f10..0b95644f 100644 --- a/src/presentation/pages/Settings.vue +++ b/src/presentation/pages/Settings.vue @@ -9,21 +9,68 @@ {{ tool.title }} - + +
    +

    + 🎡 Marketplace +

    + + Insert Tool Id and press Enter: + +
    - +