From 5abeac74eb63d82481eb3da8b912805ec98bdb98 Mon Sep 17 00:00:00 2001 From: Paul Tirk Date: Fri, 4 Oct 2024 17:10:30 +0200 Subject: [PATCH 01/51] allow registering settings for plugins --- src/plugins/Plugin.ts | 4 +++- src/plugins/PluginManager.ts | 19 +++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/plugins/Plugin.ts b/src/plugins/Plugin.ts index 7a8a751..6156c71 100644 --- a/src/plugins/Plugin.ts +++ b/src/plugins/Plugin.ts @@ -1,5 +1,7 @@ +import type { PluginInitOptions } from './PluginManager'; + export abstract class Plugin { public abstract getPluginId(): string; - public abstract init(): Promise; + public abstract init({ registerSettings }: PluginInitOptions): Promise; } diff --git a/src/plugins/PluginManager.ts b/src/plugins/PluginManager.ts index 6b79136..9c92912 100644 --- a/src/plugins/PluginManager.ts +++ b/src/plugins/PluginManager.ts @@ -1,7 +1,10 @@ +import type { SettingsConfig } from '../types/SettingsTypes'; import type { Plugin } from './Plugin'; export class PluginManager { - private plugins: Array = []; + private readonly plugins: Array = []; + + private readonly settings: Array = []; constructor(...plugins: Array) { this.plugins.push(...plugins); @@ -9,7 +12,11 @@ export class PluginManager { public async init(): Promise { for (const plugin of this.plugins) { - await plugin.init(); + await plugin.init({ + registerSettings: (settings) => { + this.settings.push(...settings); + } + }); } } @@ -18,4 +25,12 @@ export class PluginManager { ): T | null { return this.plugins.find((p) => p.constructor === pluginClass) ?? null; } + + public getSettings(): Array { + return this.settings; + } } + +export type PluginInitOptions = { + registerSettings: (settings: Array) => void; +}; From c827ce1cab9446985b3a368454a7460b1f539ace Mon Sep 17 00:00:00 2001 From: Paul Tirk Date: Fri, 4 Oct 2024 17:10:30 +0200 Subject: [PATCH 02/51] add changelog for import/export plugin --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2df468e..0f01851 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## [Unreleased] +- add import/export plugin + ## [0.9.0] - 2024-09-27 - allow creation of labels in selection dialog From b74f874c3a6d29ab717f4765e20539eca4e52581 Mon Sep 17 00:00:00 2001 From: Paul Tirk Date: Fri, 4 Oct 2024 17:10:30 +0200 Subject: [PATCH 03/51] allow setting name for a test store --- src/stores/TestStore.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/stores/TestStore.ts b/src/stores/TestStore.ts index a970319..054ce11 100644 --- a/src/stores/TestStore.ts +++ b/src/stores/TestStore.ts @@ -8,10 +8,10 @@ import { type UpdateEntity } from './Store'; -export class TestStore extends Store<'test', TestEntity> { +export class TestStore extends Store { public migrationSpy; - constructor({ version }: { version: number }) { + constructor({ name = 'test', version }: { name?: string; version: number }) { const migrationSpy = sinon.spy>((entities) => { return entities.map((entity) => { return { @@ -24,7 +24,7 @@ export class TestStore extends Store<'test', TestEntity> { }); super({ - tableName: 'test', + tableName: name, migrationConfig: { version, migrationFunction: migrationSpy From d04466517c6d6e44bbe85fcfad766ea53f4c2ca4 Mon Sep 17 00:00:00 2001 From: Paul Tirk Date: Fri, 4 Oct 2024 17:10:30 +0200 Subject: [PATCH 04/51] add tests for storage manager --- src/stores/StorageManager.spec.ts | 85 +++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 src/stores/StorageManager.spec.ts diff --git a/src/stores/StorageManager.spec.ts b/src/stores/StorageManager.spec.ts new file mode 100644 index 0000000..0834a72 --- /dev/null +++ b/src/stores/StorageManager.spec.ts @@ -0,0 +1,85 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import sinon, { type SinonFakeTimers } from 'sinon'; + +import { + StoreAlreadyRegisteredError, + useStorageManager +} from './StorageManager'; +import { TestStore } from './TestStore'; +import { useLocalStorage } from './LocalStorage/useLocalStorage'; + +describe('StorageManager', () => { + it('should throw an error if store is already registered', () => { + const { createStore } = setupEnvironment(); + + createStore(); + + expect(() => { + createStore(); + }).toThrow(StoreAlreadyRegisteredError); + }); + + it('should notify subscribers if an entity is created', async () => { + const { createStore } = setupEnvironment(); + + const store = createStore(); + + const storageManager = useStorageManager(); + + const spy = sinon.spy(); + storageManager.subscribeEntityUpserted(spy); + + const id = await store.create({ testValue: 'abc' }); + + expect(spy.getCalls().map((c) => c.args)).to.deep.equal([ + ['test', { id, testValue: 'abc', createdAt: 100, updatedAt: 100 }] + ]); + }); + + it('should notify subscribers if an entity is updated', async () => { + const { createStore } = setupEnvironment(); + + const store = createStore(); + + const storageManager = useStorageManager(); + + const id = await store.create({ testValue: 'abc' }); + + const spy = sinon.spy(); + storageManager.subscribeEntityUpserted(spy); + + await store.update({ id, testValue: 'def' }); + + expect(spy.getCalls().map((c) => c.args)).to.deep.equal([ + ['test', { id, testValue: 'def', createdAt: 100, updatedAt: 100 }] + ]); + }); + + let clock: SinonFakeTimers; + + beforeEach(() => { + clock = sinon.useFakeTimers(100); + }); + + afterEach(async () => { + const storageManager = useStorageManager(); + storageManager.clear(); + + const localStorage = await useLocalStorage('test'); + await localStorage.clear(); + + clock.restore(); + }); + + function setupEnvironment(): { + createStore: (name?: string) => TestStore; + } { + return { + createStore: (name = 'test') => + new TestStore({ + name, + version: 0 + }) + }; + } +}); From 28f44f082441f0216fefd8087c54661d68e68acb Mon Sep 17 00:00:00 2001 From: Paul Tirk Date: Fri, 4 Oct 2024 17:10:30 +0200 Subject: [PATCH 05/51] add test for data export of stores --- src/stores/StorageManager.spec.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/stores/StorageManager.spec.ts b/src/stores/StorageManager.spec.ts index 0834a72..7dce465 100644 --- a/src/stores/StorageManager.spec.ts +++ b/src/stores/StorageManager.spec.ts @@ -55,6 +55,26 @@ describe('StorageManager', () => { ]); }); + it('should export data of all stores', async () => { + const { createStore } = setupEnvironment(); + + const store1 = createStore(); + const id = await store1.create({ testValue: 'test1' }); + + createStore('store2'); + + const storageManager = useStorageManager(); + + expect(await storageManager.exportData()).to.deep.equal({ + test: { + entities: [{ id, testValue: 'test1', createdAt: 100, updatedAt: 100 }] + }, + store2: { + entities: [] + } + }); + }); + let clock: SinonFakeTimers; beforeEach(() => { From d2bb9abb1319ad29b3187be62536cfd865e011d3 Mon Sep 17 00:00:00 2001 From: Paul Tirk Date: Fri, 4 Oct 2024 17:10:30 +0200 Subject: [PATCH 06/51] add method for exporting data of all stores --- src/stores/StorageManager.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/stores/StorageManager.ts b/src/stores/StorageManager.ts index 8ef6cfa..bb9f044 100644 --- a/src/stores/StorageManager.ts +++ b/src/stores/StorageManager.ts @@ -59,6 +59,18 @@ export class StorageManager { }); } + public async exportData(): Promise }>> { + const exportData: Record }> = {}; + + for (const [storeName, store] of this.stores.entries()) { + exportData[storeName] = { + entities: await store.getAll() + }; + } + + return exportData; + } + private notifyEntityRemoved(tableName: string, entityId: string): void { this.notifySubscribers('entityRemoved', tableName, entityId); } From a25b61c5333281086d2f4eb4c0729697b5b135ed Mon Sep 17 00:00:00 2001 From: Paul Tirk Date: Fri, 4 Oct 2024 17:10:30 +0200 Subject: [PATCH 07/51] add settings view & route --- i18n/en.json | 2 ++ src/components/NavigationBar.vue | 13 ++++++++++++- src/router.ts | 11 +++++++++++ src/views/SettingsView.vue | 5 +++++ 4 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/views/SettingsView.vue diff --git a/i18n/en.json b/i18n/en.json index 90ddaa3..dcb1e78 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -29,6 +29,7 @@ "NavigationBar": { "labels": "Labels", "noLabels": "no labels", + "settings": "Settings", "todos": "Todos" }, "todos": { @@ -51,6 +52,7 @@ }, "routes": { "label": "Label", + "settings": "Settings", "todos": "Todos" } } diff --git a/src/components/NavigationBar.vue b/src/components/NavigationBar.vue index 244774f..83c3862 100644 --- a/src/components/NavigationBar.vue +++ b/src/components/NavigationBar.vue @@ -54,6 +54,17 @@ /> + +
+ + + +
@@ -70,7 +81,7 @@ From 6c35188972959a161dfddd1ff76c3b2ae636c81f Mon Sep 17 00:00:00 2001 From: Paul Tirk Date: Fri, 4 Oct 2024 17:10:30 +0200 Subject: [PATCH 08/51] use input group setting type instead of button group --- src/types/SettingsTypes.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/types/SettingsTypes.ts b/src/types/SettingsTypes.ts index 396db5b..35968cf 100644 --- a/src/types/SettingsTypes.ts +++ b/src/types/SettingsTypes.ts @@ -9,7 +9,7 @@ export enum SettingInputType { DROPDOWN = 'dropdown', RADIOBUTTON = 'radiobutton', BUTTON = 'button', - BUTTON_GROUP = 'button_group' + INPUT_GROUP = 'input_group' } export type SettingsConfig = { @@ -22,7 +22,7 @@ export type SettingDefinition = | SelectDefinition | CheckboxDefinition | ButtonDefinition - | ButtonGroupDefinition; + | InputGroupDefinition; type InputDefinition = CommonDefinition & { type: @@ -48,9 +48,9 @@ type ButtonDefinition = CommonDefinition & { handler: () => Promise; }; -type ButtonGroupDefinition = CommonDefinition & { - type: SettingInputType.BUTTON_GROUP; - buttons: Array; +type InputGroupDefinition = CommonDefinition & { + type: SettingInputType.INPUT_GROUP; + children: Array; }; type CommonDefinition = { From 0a4f447258021622cba27ed36b77b013a477df84 Mon Sep 17 00:00:00 2001 From: Paul Tirk Date: Fri, 4 Oct 2024 17:10:30 +0200 Subject: [PATCH 09/51] add setting input component --- src/components/settings/SettingInput.vue | 27 ++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/components/settings/SettingInput.vue diff --git a/src/components/settings/SettingInput.vue b/src/components/settings/SettingInput.vue new file mode 100644 index 0000000..01ebedb --- /dev/null +++ b/src/components/settings/SettingInput.vue @@ -0,0 +1,27 @@ + + + From f4348573fa5edc3381fbbd3edd617d57bf7fdf07 Mon Sep 17 00:00:00 2001 From: Paul Tirk Date: Fri, 4 Oct 2024 17:10:30 +0200 Subject: [PATCH 10/51] add settings section component --- src/components/settings/SettingsSection.vue | 31 +++++++++++++++++++++ src/types/SettingsTypes.ts | 1 + 2 files changed, 32 insertions(+) create mode 100644 src/components/settings/SettingsSection.vue diff --git a/src/components/settings/SettingsSection.vue b/src/components/settings/SettingsSection.vue new file mode 100644 index 0000000..f046af0 --- /dev/null +++ b/src/components/settings/SettingsSection.vue @@ -0,0 +1,31 @@ + + + diff --git a/src/types/SettingsTypes.ts b/src/types/SettingsTypes.ts index 35968cf..10b9038 100644 --- a/src/types/SettingsTypes.ts +++ b/src/types/SettingsTypes.ts @@ -14,6 +14,7 @@ export enum SettingInputType { export type SettingsConfig = { name: string; + labelTk: string; settings: Record; }; From f60c57892f4bb6d214e23fe80e8ccd588bb131b3 Mon Sep 17 00:00:00 2001 From: Paul Tirk Date: Fri, 4 Oct 2024 17:10:30 +0200 Subject: [PATCH 11/51] show plugin settings in settings view --- i18n/en.json | 5 +++++ src/components/settings/PluginSettings.vue | 23 ++++++++++++++++++++++ src/views/SettingsView.vue | 15 ++++++++++++-- 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 src/components/settings/PluginSettings.vue diff --git a/i18n/en.json b/i18n/en.json index dcb1e78..839e67c 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -54,5 +54,10 @@ "label": "Label", "settings": "Settings", "todos": "Todos" + }, + "views": { + "settings": { + "plugins": "Plugins" + } } } diff --git a/src/components/settings/PluginSettings.vue b/src/components/settings/PluginSettings.vue new file mode 100644 index 0000000..0873e5d --- /dev/null +++ b/src/components/settings/PluginSettings.vue @@ -0,0 +1,23 @@ + + + diff --git a/src/views/SettingsView.vue b/src/views/SettingsView.vue index 8f01dcf..d592fdd 100644 --- a/src/views/SettingsView.vue +++ b/src/views/SettingsView.vue @@ -1,5 +1,16 @@ - + From a9b624a33532e1dfd79ae8d02456a6500566ba4d Mon Sep 17 00:00:00 2001 From: Paul Tirk Date: Fri, 4 Oct 2024 17:10:30 +0200 Subject: [PATCH 12/51] implement button setting input component --- src/components/settings/SettingInput.vue | 29 ++++++++++++++++++++++-- src/types/SettingsTypes.ts | 2 +- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/components/settings/SettingInput.vue b/src/components/settings/SettingInput.vue index 01ebedb..40f01ab 100644 --- a/src/components/settings/SettingInput.vue +++ b/src/components/settings/SettingInput.vue @@ -5,9 +5,19 @@ From e632d7d2cf2eb8eaa9e92bd0989bcde17f67b3fe Mon Sep 17 00:00:00 2001 From: Paul Tirk Date: Sun, 6 Oct 2024 23:18:38 +0200 Subject: [PATCH 51/51] correct import/export test expectations --- src/stores/StorageManager.spec.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/stores/StorageManager.spec.ts b/src/stores/StorageManager.spec.ts index 5497efe..0fd537f 100644 --- a/src/stores/StorageManager.spec.ts +++ b/src/stores/StorageManager.spec.ts @@ -66,12 +66,17 @@ describe('StorageManager', () => { const storageManager = useStorageManager(); expect(await storageManager.exportData()).to.deep.equal({ + lastUpdatedAt: 100, exportedAt: 100, stores: { test: { + version: 0, + lastUpdatedAt: 100, entities: [{ id, testValue: 'test1', createdAt: 100, updatedAt: 100 }] }, store2: { + version: 0, + lastUpdatedAt: 0, entities: [] } } @@ -89,16 +94,22 @@ describe('StorageManager', () => { const storageManager = useStorageManager(); await storageManager.importData({ + lastUpdatedAt: 100, exportedAt: 100, stores: { test: { + version: 0, + lastUpdatedAt: 0, entities: [] }, store2: { + version: 0, + lastUpdatedAt: 100, entities: [ { id: '2-1', - testValue: 'test1-Store2' + testValue: 'test1-Store2', + updatedAt: 100 } ] } @@ -106,16 +117,22 @@ describe('StorageManager', () => { }); expect(await storageManager.exportData()).to.deep.equal({ + lastUpdatedAt: 100, exportedAt: 100, stores: { test: { + version: 0, + lastUpdatedAt: 0, entities: [] }, store2: { + version: 0, + lastUpdatedAt: 100, entities: [ { id: '2-1', - testValue: 'test1-Store2' + testValue: 'test1-Store2', + updatedAt: 100 } ] }