From 51a2d02b8bbc96c5c3832341aa0abcc32689c626 Mon Sep 17 00:00:00 2001 From: Xharles Date: Wed, 21 Jan 2026 13:46:49 +0100 Subject: [PATCH 1/3] feat: Add addOrUpdateSubscribedView action and update useView hook --- packages/frontend/src/api/hooks/useView.ts | 2 +- .../frontend/src/api/store/slices/views.ts | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/api/hooks/useView.ts b/packages/frontend/src/api/hooks/useView.ts index c24ad0229..cd64b61fb 100644 --- a/packages/frontend/src/api/hooks/useView.ts +++ b/packages/frontend/src/api/hooks/useView.ts @@ -278,7 +278,7 @@ export const useView: () => IView = () => { sort_order: view.sort_order, updated_at: view.updated_at, }; - dispatch(viewsStore.updateSubscribedViewsCache([viewPreview])); + dispatch(viewsStore.addOrUpdateSubscribedView(viewPreview)); dispatch( viewsStore.setViewsCache({ ...viewsCache, diff --git a/packages/frontend/src/api/store/slices/views.ts b/packages/frontend/src/api/store/slices/views.ts index b3d734336..6f2d54a3d 100644 --- a/packages/frontend/src/api/store/slices/views.ts +++ b/packages/frontend/src/api/store/slices/views.ts @@ -177,6 +177,39 @@ const viewsSlice = createSlice({ } draft.subscribedViewsCache = { ...draftViewsCache }; }, + addOrUpdateSubscribedView( + draft, + action: PayloadAction + ) { + const { payload: subscribedView } = action; + const { subscribedViewsCache } = draft; + + const prevState = subscribedViewsCache + ? subscribedViewsCache[subscribedView.id] + : undefined; + + const updatedView: TSubscribedView = + prevState !== undefined + ? { + ...prevState, + lastSynced: new Date().toISOString(), + data: { + ...subscribedView, + }, + } + : { + lastModified: undefined, + lastSynced: new Date().toISOString(), + data: { + ...subscribedView, + }, + }; + + draft.subscribedViewsCache = { + ...(subscribedViewsCache ?? {}), + [subscribedView.id]: updatedView, + }; + }, syncView(draft, action: PayloadAction) { const remoteView = mapRemoteView(action.payload); Logger.debug( @@ -761,6 +794,7 @@ export const { addWidgetsToView, removeWidgetFromView, updateSubscribedViewsCache, + addOrUpdateSubscribedView, setSubscribedViewsCache, } = viewsSlice.actions; export default viewsSlice.reducer; From 25b748d2d8cfe75739dfa4ef6c0e0ba2b9cbee2f Mon Sep 17 00:00:00 2001 From: Xharles Date: Wed, 21 Jan 2026 13:49:28 +0100 Subject: [PATCH 2/3] test: Add tests for addOrUpdateSubscribedView and updateSubscribedViewsCache functionality --- .../src/api/store/slices/views.test.ts | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/packages/frontend/src/api/store/slices/views.test.ts b/packages/frontend/src/api/store/slices/views.test.ts index 917a93c47..8e8b0ad0c 100644 --- a/packages/frontend/src/api/store/slices/views.test.ts +++ b/packages/frontend/src/api/store/slices/views.test.ts @@ -31,6 +31,7 @@ import viewsReducer, { selectedViewSelector, getSelectedViewRefFromDraft, updateSubscribedViewsCache, + addOrUpdateSubscribedView, setSubscribedViewsCache, } from "./views"; @@ -308,6 +309,117 @@ describe("updateSubscribedViewsCache", () => { expect(newSubscribedViews).toStrictEqual(updatedSubscribedViews); }); + + it("should replace entire cache with payload (not merge)", () => { + // Start with cache containing views 1, 2, 3 + const initialCache = { + 1: fakeSubscribedViewsCacheMock(1), + 2: fakeSubscribedViewsCacheMock(2), + 3: fakeSubscribedViewsCacheMock(3), + }; + + initialState = { + ...initialState, + subscribedViewsCache: initialCache, + }; + + // Update with only views 2 and 4 + const newSubscribedViews = [ + fakeSubscribedView(2), + fakeSubscribedView(4), + ]; + + const newViewState = viewsReducer( + initialState, + updateSubscribedViewsCache(newSubscribedViews) + ); + + // Should have only views 2 and 4 (views 1 and 3 removed) + expect(Object.keys(newViewState.subscribedViewsCache || {})).toEqual([ + "2", + "4", + ]); + expect(newViewState.subscribedViewsCache?.[1]).toBeUndefined(); + expect(newViewState.subscribedViewsCache?.[3]).toBeUndefined(); + expect(newViewState.subscribedViewsCache?.[2]).toBeDefined(); + expect(newViewState.subscribedViewsCache?.[4]).toBeDefined(); + }); +}); + +describe("addOrUpdateSubscribedView", () => { + it("should add a new subscribed view without affecting others", () => { + // Start with cache containing views 1, 2, 3 + const initialCache = { + 1: fakeSubscribedViewsCacheMock(1), + 2: fakeSubscribedViewsCacheMock(2), + 3: fakeSubscribedViewsCacheMock(3), + }; + + initialState = { + ...initialState, + subscribedViewsCache: initialCache, + }; + + // Add view 4 + const newView = fakeSubscribedView(4); + + const newViewState = viewsReducer( + initialState, + addOrUpdateSubscribedView(newView) + ); + + // Should have all 4 views + expect(Object.keys(newViewState.subscribedViewsCache || {})).toHaveLength(4); + expect(newViewState.subscribedViewsCache?.[1]).toBeDefined(); + expect(newViewState.subscribedViewsCache?.[2]).toBeDefined(); + expect(newViewState.subscribedViewsCache?.[3]).toBeDefined(); + expect(newViewState.subscribedViewsCache?.[4]).toBeDefined(); + }); + + it("should update an existing subscribed view without affecting others", async () => { + // Start with cache containing views 1, 2, 3 + const initialCache = { + 1: fakeSubscribedViewsCacheMock(1), + 2: fakeSubscribedViewsCacheMock(2), + 3: fakeSubscribedViewsCacheMock(3), + }; + + initialState = { + ...initialState, + subscribedViewsCache: initialCache, + }; + + // Wait 1 second to ensure lastSynced will be different + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Update view 2 + const updatedView = { + ...fakeSubscribedView(2), + name: "Updated View 2", + }; + + const newViewState = viewsReducer( + initialState, + addOrUpdateSubscribedView(updatedView) + ); + + // Should still have 3 views + expect(Object.keys(newViewState.subscribedViewsCache || {})).toHaveLength(3); + + // View 2 should be updated + expect(newViewState.subscribedViewsCache?.[2]?.data.name).toBe("Updated View 2"); + + // lastSynced should be more recent than initial + expect( + moment(newViewState.subscribedViewsCache?.[2]?.lastSynced).isAfter( + initialCache[2].lastSynced + ) + ).toBe(true); + + // Views 1 and 3 should be unchanged + expect(newViewState.subscribedViewsCache?.[1]).toEqual(initialCache[1]); + expect(newViewState.subscribedViewsCache?.[3]).toEqual(initialCache[3]); + }); }); describe("removeSharedViewFromCache", () => { From 363daf2c8758cf921680e4449184716458e49b0a Mon Sep 17 00:00:00 2001 From: Xharles Date: Wed, 21 Jan 2026 14:06:18 +0100 Subject: [PATCH 3/3] refactor: Improve promise handling and formatting in views tests --- .../src/api/store/slices/views.test.ts | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/frontend/src/api/store/slices/views.test.ts b/packages/frontend/src/api/store/slices/views.test.ts index 8e8b0ad0c..44b7e0f17 100644 --- a/packages/frontend/src/api/store/slices/views.test.ts +++ b/packages/frontend/src/api/store/slices/views.test.ts @@ -243,8 +243,9 @@ describe("updateSubscribedViewsCache", () => { const viewIds = newSubscribedViews.map((view) => view.id); // delay the last modified date by 1 second - // eslint-disable-next-line no-promise-executor-return - await new Promise((resolve) => setTimeout(resolve, 1000)); + await new Promise((resolve) => { + setTimeout(resolve, 1000); + }); const newSubscribedViewsState = viewsReducer( initialState, @@ -369,7 +370,9 @@ describe("addOrUpdateSubscribedView", () => { ); // Should have all 4 views - expect(Object.keys(newViewState.subscribedViewsCache || {})).toHaveLength(4); + expect( + Object.keys(newViewState.subscribedViewsCache || {}) + ).toHaveLength(4); expect(newViewState.subscribedViewsCache?.[1]).toBeDefined(); expect(newViewState.subscribedViewsCache?.[2]).toBeDefined(); expect(newViewState.subscribedViewsCache?.[3]).toBeDefined(); @@ -390,7 +393,9 @@ describe("addOrUpdateSubscribedView", () => { }; // Wait 1 second to ensure lastSynced will be different - await new Promise((resolve) => setTimeout(resolve, 1000)); + await new Promise((resolve) => { + setTimeout(resolve, 1000); + }); // Update view 2 const updatedView = { @@ -404,10 +409,14 @@ describe("addOrUpdateSubscribedView", () => { ); // Should still have 3 views - expect(Object.keys(newViewState.subscribedViewsCache || {})).toHaveLength(3); + expect( + Object.keys(newViewState.subscribedViewsCache || {}) + ).toHaveLength(3); // View 2 should be updated - expect(newViewState.subscribedViewsCache?.[2]?.data.name).toBe("Updated View 2"); + expect(newViewState.subscribedViewsCache?.[2]?.data.name).toBe( + "Updated View 2" + ); // lastSynced should be more recent than initial expect(