From 7a74eae5c3a6ed6e05e13c305b6d7183cc8a505a Mon Sep 17 00:00:00 2001 From: Bruce Schultz Date: Mon, 31 Aug 2026 11:57:24 +0200 Subject: [PATCH 1/3] fix(settings): refresh setting toggle for agg nodes --- app/assets/css/preferences.css | 8 ++++++++ app/components/preferences/PreferencesDialog.vue | 13 ++++++++++--- .../preferences/RequireDataStoreField.vue | 12 +++++++++++- app/stores/nodeSettingsStore.ts | 5 ++++- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/app/assets/css/preferences.css b/app/assets/css/preferences.css index 5e1eb24..2df677e 100644 --- a/app/assets/css/preferences.css +++ b/app/assets/css/preferences.css @@ -36,3 +36,11 @@ height: 2.5rem; width: 8rem; } + +.setting-not-applicable-text { + display: block; + margin-top: 0.5rem; + font-size: 0.875rem; + font-style: italic; + color: var(--p-text-muted-color); +} diff --git a/app/components/preferences/PreferencesDialog.vue b/app/components/preferences/PreferencesDialog.vue index 2c8977f..395ef4c 100644 --- a/app/components/preferences/PreferencesDialog.vue +++ b/app/components/preferences/PreferencesDialog.vue @@ -12,7 +12,11 @@ const loading = ref(false); const toast = useToast(); const store = useNodeSettingsStore(); -const draftRequireDataStore = ref(store.requireDataStore); +const draftRequireDataStore = ref(store.requireDataStoreSetting); +// Aggregator nodes never require a data store +const requireDataStoreDisabled = computed( + () => store.nodeType === "aggregator", +); const draftAutostart = ref({ enabled: store.autostartEnabled, interval: store.autostartInterval, @@ -20,7 +24,7 @@ const draftAutostart = ref({ watch(preferencesVisible, (visible) => { if (visible) { - draftRequireDataStore.value = store.requireDataStore; + draftRequireDataStore.value = store.requireDataStoreSetting; draftAutostart.value.enabled = store.autostartEnabled; draftAutostart.value.interval = store.autostartInterval; } @@ -68,7 +72,10 @@ async function onSubmitPreferences() { >
- +
diff --git a/app/components/preferences/RequireDataStoreField.vue b/app/components/preferences/RequireDataStoreField.vue index e7262f9..6eb96f4 100644 --- a/app/components/preferences/RequireDataStoreField.vue +++ b/app/components/preferences/RequireDataStoreField.vue @@ -2,6 +2,8 @@ import ToggleSwitch from "primevue/toggleswitch"; const model = defineModel({ required: true }); + +withDefaults(defineProps<{ disabled?: boolean }>(), { disabled: false }); diff --git a/app/stores/nodeSettingsStore.ts b/app/stores/nodeSettingsStore.ts index 6b7d914..8ecf832 100644 --- a/app/stores/nodeSettingsStore.ts +++ b/app/stores/nodeSettingsStore.ts @@ -8,9 +8,12 @@ export const useNodeSettingsStore = defineStore("nodeSettings", { nodeType: null as string | null, }), getters: { + requireDataStoreSetting(state): boolean { + return Boolean(state.settings?.require_data_store ?? true); + }, requireDataStore(state): boolean { if (state.nodeType === "aggregator") return false; - return Boolean(state.settings?.require_data_store ?? true); + return this.requireDataStoreSetting; }, autostartEnabled(state): boolean { return Boolean(state.settings?.autostart?.enabled ?? false); From 94897d95721016c91a344930f12509cc610f54e9 Mon Sep 17 00:00:00 2001 From: Bruce Schultz Date: Mon, 31 Aug 2026 11:58:15 +0200 Subject: [PATCH 2/3] test(settings): update tests for agg node specifics --- .../preferences/PreferencesDialog.spec.ts | 3 + .../PreferencesDialogStore.spec.ts | 137 ++++++++++++++++++ .../preferences/RequireDataStoreField.spec.ts | 26 +++- 3 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 test/components/preferences/PreferencesDialogStore.spec.ts diff --git a/test/components/preferences/PreferencesDialog.spec.ts b/test/components/preferences/PreferencesDialog.spec.ts index df04dd4..68bdb14 100644 --- a/test/components/preferences/PreferencesDialog.spec.ts +++ b/test/components/preferences/PreferencesDialog.spec.ts @@ -56,6 +56,7 @@ describe("PreferencesDialog.vue", () => { mockUpdateSettings = vi.fn().mockResolvedValue(defaultSettingsResponse); vi.mocked(useNodeSettingsStore).mockReturnValue({ requireDataStore: true, + requireDataStoreSetting: true, autostartEnabled: false, autostartInterval: 60, updateSettings: mockUpdateSettings, @@ -91,6 +92,7 @@ describe("PreferencesDialog.vue", () => { it("initializes RequireDataStoreField as false when store has false", () => { vi.mocked(useNodeSettingsStore).mockReturnValue({ requireDataStore: false, + requireDataStoreSetting: false, autostartEnabled: true, autostartInterval: 30, updateSettings: mockUpdateSettings, @@ -159,6 +161,7 @@ describe("PreferencesDialog.vue", () => { it("resets draft values from the store when the dialog reopens", async () => { vi.mocked(useNodeSettingsStore).mockReturnValue({ requireDataStore: false, + requireDataStoreSetting: false, autostartEnabled: true, autostartInterval: 30, updateSettings: mockUpdateSettings, diff --git a/test/components/preferences/PreferencesDialogStore.spec.ts b/test/components/preferences/PreferencesDialogStore.spec.ts new file mode 100644 index 0000000..d8a43a2 --- /dev/null +++ b/test/components/preferences/PreferencesDialogStore.spec.ts @@ -0,0 +1,137 @@ +import { flushPromises, mount } from "@vue/test-utils"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { createPinia, setActivePinia } from "pinia"; +import PreferencesDialog from "~/components/preferences/PreferencesDialog.vue"; +import { useNodeSettingsStore } from "~/stores/nodeSettingsStore"; +import type { UserSettings } from "~/services/Api"; + +const DialogStub = { + props: ["visible", "header", "modal", "style"], + template: '
', +}; + +const RequireDataStoreFieldStub = { + name: "RequireDataStoreField", + props: ["modelValue", "disabled"], + emits: ["update:modelValue"], + template: + '
diff --git a/test/components/preferences/PreferencesDialogStore.spec.ts b/test/components/preferences/PreferencesDialogStore.spec.ts index d8a43a2..28d80c4 100644 --- a/test/components/preferences/PreferencesDialogStore.spec.ts +++ b/test/components/preferences/PreferencesDialogStore.spec.ts @@ -22,7 +22,8 @@ const AutostartFieldStub = { name: "AutostartField", props: ["modelValue"], emits: ["update:modelValue"], - template: '
', + template: + '
', }; const globalStubs = { @@ -134,4 +135,101 @@ describe("PreferencesDialog.vue + nodeSettingsStore", () => { wrapper.find(".require-datastore-stub").attributes("data-disabled"), ).toBe("true"); }); + + describe("when settings have not loaded yet", () => { + function seedUnloaded() { + const store = useNodeSettingsStore(); + store.settings = null; + store.nodeType = "default"; + store.updateSettings = vi.fn(async (patch: UserSettings) => { + store.settings = JSON.parse(JSON.stringify(patch)); + return store.settings; + }) as never; + return store; + } + + it("cannot be saved before the settings arrive", async () => { + seedUnloaded(); + + const wrapper = mount(PreferencesDialog, { + props: { preferencesVisible: true }, + global: { stubs: globalStubs }, + }); + + const button = wrapper.find(".preferences-update-btn button"); + expect(button.attributes("disabled")).toBeDefined(); + }); + + it("re-seeds the drafts when settings arrive while the dialog is open", async () => { + const store = seedUnloaded(); + + const wrapper = mount(PreferencesDialog, { + props: { preferencesVisible: false }, + global: { stubs: globalStubs }, + }); + await wrapper.setProps({ preferencesVisible: true }); + await flushPromises(); + + store.settings = { + require_data_store: false, + autostart: { enabled: true, interval: 30 }, + }; + await flushPromises(); + + expect( + wrapper.find(".require-datastore-stub").attributes("data-value"), + ).toBe("false"); + const autostart = wrapper.find(".autostart-stub"); + expect(autostart.attributes("data-enabled")).toBe("true"); + expect(autostart.attributes("data-interval")).toBe("30"); + }); + + it("saves the loaded values, not the pre-load defaults", async () => { + const store = seedUnloaded(); + + const wrapper = mount(PreferencesDialog, { + props: { preferencesVisible: true }, + global: { stubs: globalStubs }, + }); + + store.settings = { + require_data_store: false, + autostart: { enabled: true, interval: 30 }, + }; + await flushPromises(); + + await wrapper.find(".preferences-update-btn button").trigger("click"); + await flushPromises(); + + expect(store.updateSettings).toHaveBeenCalledWith({ + require_data_store: false, + autostart: { enabled: true, interval: 30 }, + }); + }); + }); + + it("does not discard in-progress edits when a background refetch lands", async () => { + const store = seedStore("default", { + require_data_store: true, + autostart: { enabled: false, interval: 60 }, + }); + + const wrapper = mount(PreferencesDialog, { + props: { preferencesVisible: true }, + global: { stubs: globalStubs }, + }); + + await wrapper.find(".require-datastore-stub").trigger("click"); // user edits + + // A periodic session refresh re-runs fetchSettings and replaces `settings`. + store.settings = { + require_data_store: true, + autostart: { enabled: false, interval: 60 }, + }; + await flushPromises(); + + expect( + wrapper.find(".require-datastore-stub").attributes("data-value"), + ).toBe("false"); + }); });