Skip to content

Commit 1860d0c

Browse files
kavinvenkatachalamCopilotvjaris42
authored
Fix: clear AppBuilder store when navigating to the dashboard (#16267)
* fix: clear app builder store when navigating to the dashboard Co-authored-by: Copilot <[email protected]> * fix: update subproject commits for frontend and server components * fix: correct import path for resetAppBuilderStore in AppLoader * feat: implement timer registry for managing timeouts and intervals * feat: implement reset functionality for app state and dependency graphs Co-authored-by: Copilot <[email protected]> * chore: update subproject commits for frontend and server * Fix: git-sync header race condition on rapid app switching useGitSyncConfig now falls back to workspaceBranchesStore.orgGitConfig when AppBuilder store's orgGit is null (during the reset→fetchAppGit gap). This prevents the header from briefly rendering non-git state. * fix: enhance app loader to handle browser navigation and store reinitialization * update submodule --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: Vijaykant Yadav <[email protected]>
1 parent fc08dff commit 1860d0c

12 files changed

Lines changed: 143 additions & 8 deletions

File tree

frontend/ee

Submodule ee updated from 75ade01 to 28fe519

frontend/src/AppBuilder/Widgets/NewTable/_stores/tableStore.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { create, zustandDevTools } from '@/_stores/utils';
1+
import { create, zustandDevTools } from '@/AppBuilder/_stores/utils';
22
import { immer } from 'zustand/middleware/immer';
33
// eslint-disable-next-line import/no-unresolved
44
import { enableMapSet } from 'immer';
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
const timeoutIds = new Set();
2+
const intervalIds = new Set();
3+
const rafIds = new Set();
4+
5+
export const timerRegistry = {
6+
trackedSetTimeout(fn, delay, ...args) {
7+
const id = window.setTimeout(fn, delay, ...args);
8+
timeoutIds.add(id);
9+
return id;
10+
},
11+
trackedSetInterval(fn, delay, ...args) {
12+
const id = window.setInterval(fn, delay, ...args);
13+
intervalIds.add(id);
14+
return id;
15+
},
16+
trackedRequestAnimationFrame(fn) {
17+
const id = window.requestAnimationFrame(fn);
18+
rafIds.add(id);
19+
return id;
20+
},
21+
trackedClearTimeout(id) {
22+
timeoutIds.delete(id);
23+
window.clearTimeout(id);
24+
},
25+
trackedClearInterval(id) {
26+
intervalIds.delete(id);
27+
window.clearInterval(id);
28+
},
29+
trackedCancelAnimationFrame(id) {
30+
rafIds.delete(id);
31+
window.cancelAnimationFrame(id);
32+
},
33+
clearAll() {
34+
timeoutIds.forEach((id) => window.clearTimeout(id));
35+
intervalIds.forEach((id) => window.clearInterval(id));
36+
rafIds.forEach((id) => window.cancelAnimationFrame(id));
37+
timeoutIds.clear();
38+
intervalIds.clear();
39+
rafIds.clear();
40+
},
41+
};
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import useStore from '@/AppBuilder/_stores/store';
2+
import { useWorkspaceBranchesStore } from '@/_stores/workspaceBranchesStore';
23

34
export const useGitSyncConfig = () => {
45
const orgGit = useStore((state) => state.orgGit);
5-
const isGitSyncEnabled = orgGit?.git_ssh?.is_enabled || orgGit?.git_https?.is_enabled || orgGit?.git_lab?.is_enabled;
6+
const orgGitConfig = useWorkspaceBranchesStore((state) => state.orgGitConfig);
7+
8+
const isGitSyncEnabled =
9+
orgGit?.git_ssh?.is_enabled ||
10+
orgGit?.git_https?.is_enabled ||
11+
orgGit?.git_lab?.is_enabled ||
12+
!!(orgGitConfig?.isEnabled ?? orgGitConfig?.is_enabled);
13+
614
const defaultBranch = orgGit?.git_https?.github_branch || orgGit?.git_ssh?.github_branch || 'main';
715
return { isGitSyncEnabled, defaultBranch };
816
};

frontend/src/AppBuilder/_stores/batchManager.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ export function createBatchManager<S extends StoreWithDependencies>(
4949
return {
5050
isBatching: () => _depth > 0,
5151

52+
// Drops any open batch without applying it. For store resets: an async flow
53+
// (e.g. switchPage's doSwitch) can open a batch and then never flush because its
54+
// owner unmounted — this closure state survives resetAllStores, so it must be
55+
// cleared explicitly or every subsequent write stays buffered forever.
56+
reset: () => {
57+
_depth = 0;
58+
_mutations = [];
59+
_depPaths = [];
60+
_postFlushKeys = new Set();
61+
_postFlushCallbacks = [];
62+
},
63+
5264
startBatch: () => {
5365
_depth++;
5466
if (_depth === 1) {

frontend/src/AppBuilder/_stores/slices/dependencySlice.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import DependencyGraph from './DependencyClass';
22
import { createBatchManager } from '../batchManager';
3+
import { registerResetter } from '../utils';
34

45
const initialState = {
56
dependencyGraph: {
@@ -16,6 +17,22 @@ export const createDependencySlice = (set, get) => {
1617
// so flush must return { ...state } to notify Zustand instead of relying on draft patches.
1718
// No dep path cascade needed here (this batch is for graph construction, not runtime updates).
1819
const _depBatch = createBatchManager(set, get, { useShallowReturn: true });
20+
registerResetter(() => _depBatch.reset());
21+
// The canvas graph instance lives inside the captured initial state and is mutated
22+
// in place across sessions (Immer can't draft class instances), so the state replace
23+
// in resetAllStores restores a graph still holding the previous app's nodes. Recreate
24+
// it after the state reset so the next app session starts from an empty graph.
25+
registerResetter(
26+
() =>
27+
set(
28+
(state) => {
29+
state.dependencyGraph.modules = { canvas: { graph: new DependencyGraph() } };
30+
},
31+
false,
32+
'resetDependencyGraph'
33+
),
34+
{ phase: 'post' }
35+
);
1936

2037
return {
2138
...initialState,

frontend/src/AppBuilder/_stores/slices/queryPanelSlice.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import moment from 'moment';
88
import axios from 'axios';
99
import { validateMultilineCode } from '@/_helpers/utility';
1010
import { convertMapSet, getQueryVariables } from '@/AppBuilder/_utils/queryPanel';
11+
import { timerRegistry } from '@/AppBuilder/_helpers/timerRegistry';
1112
import { deepClone } from '@/_helpers/utilities/utils.helpers';
1213

1314
const queryManagerPreferences = JSON.parse(localStorage.getItem('queryManagerPreferences')) ?? {};
@@ -1603,6 +1604,12 @@ export const createQueryPanelSlice = (set, get) => ({
16031604
'variables',
16041605
'actions',
16051606
'constants',
1607+
'setTimeout',
1608+
'setInterval',
1609+
'clearTimeout',
1610+
'clearInterval',
1611+
'requestAnimationFrame',
1612+
'cancelAnimationFrame',
16061613
...(!_.isEmpty(formattedParams) ? ['parameters'] : []), // Parameters are supported if builder has added atleast one parameter to the query
16071614
...(appType === 'module' ? ['input'] : []), // Include 'input' only for module,
16081615
...Object.keys(libraryRegistry),
@@ -1621,6 +1628,12 @@ export const createQueryPanelSlice = (set, get) => ({
16211628
deepClone(resolvedState.variables),
16221629
actions,
16231630
resolvedState?.constants,
1631+
timerRegistry.trackedSetTimeout.bind(timerRegistry),
1632+
timerRegistry.trackedSetInterval.bind(timerRegistry),
1633+
timerRegistry.trackedClearTimeout.bind(timerRegistry),
1634+
timerRegistry.trackedClearInterval.bind(timerRegistry),
1635+
timerRegistry.trackedRequestAnimationFrame.bind(timerRegistry),
1636+
timerRegistry.trackedCancelAnimationFrame.bind(timerRegistry),
16241637
...(!_.isEmpty(formattedParams) ? [formattedParams] : []), // Parameters are supported if builder has added atleast one parameter to the query
16251638
...(appType === 'module' ? [resolvedState.input] : []), // Include 'input' only for module
16261639
...Object.values(libraryRegistry),

frontend/src/AppBuilder/_stores/slices/resolvedSlice.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { resolveDynamicValues } from '../utils';
1+
import { resolveDynamicValues, registerResetter } from '../utils';
22
import { extractAndReplaceReferencesFromString } from '@/AppBuilder/_stores/ast';
33
import { componentTypeDefinitionMap } from '@/AppBuilder/WidgetManager';
44
import { createBatchManager } from '@/AppBuilder/_stores/batchManager';
@@ -55,6 +55,7 @@ const buildExposedValueMutation = (componentId, property, value, moduleId) => (s
5555

5656
export const createResolvedSlice = (set, get) => {
5757
const _exposedValueBatch = createBatchManager(set, get);
58+
registerResetter(() => _exposedValueBatch.reset());
5859

5960
// Implicit microtask batch: coalesces dep cascades from setVariable / setExposedValue
6061
// calls that happen outside an explicit batch window (ListView/Form bracket).

frontend/src/AppBuilder/_stores/utils.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,24 @@ export const create = (fn) => {
4848
return store;
4949
};
5050

51+
// Slice factories hold state in closures (e.g. batch managers) that setState-based
52+
// resetters can't reach. Slices register an explicit resetter here so resetAllStores
53+
// clears that closure state too.
54+
// phase 'post' runs AFTER the state replace — needed when the resetter must repair
55+
// values inside the restored initial state (e.g. class instances that were mutated
56+
// in place, which the captured initialState shares by reference).
57+
const postResetters = [];
58+
export const registerResetter = (fn, { phase = 'pre' } = {}) => {
59+
(phase === 'post' ? postResetters : resetters).push(fn);
60+
};
61+
5162
export const resetAllStores = () => {
5263
for (const resetter of resetters) {
5364
resetter();
5465
}
66+
for (const resetter of postResetters) {
67+
resetter();
68+
}
5569
};
5670

5771
// The following function will be resposible to convert dynamic values like `Hello {{components.compId.value}} {{components.compId2.value}}` to `Hello 123 456 `

frontend/src/AppLoader/AppLoader.jsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import React, { useLayoutEffect } from 'react';
1+
import React, { useEffect, useLayoutEffect } from 'react';
22
import { withTranslation } from 'react-i18next';
33
import _ from 'lodash';
44
import { resetAllStores } from '@/_stores/utils';
5+
import { resetAllStores as resetAppBuilderStore } from '@/AppBuilder/_stores/utils';
6+
import { timerRegistry } from '@/AppBuilder/_helpers/timerRegistry';
57
import RenderWorkflow from '@/modules/RenderWorkflow';
68
import RenderAppBuilder from './RenderAppBuilder';
79

@@ -10,6 +12,28 @@ const AppLoader = (props) => {
1012

1113
useLayoutEffect(() => {
1214
resetAllStores();
15+
return () => {
16+
timerRegistry.clearAll();
17+
resetAppBuilderStore();
18+
};
19+
}, []);
20+
21+
// Force a hard reload when browser back/forward navigates away from the editor.
22+
// This ensures stores (git sync state, branch config, etc.) are fully re-initialized
23+
// on the destination page, matching the behavior apps already exhibit.
24+
// Use capture phase to fire before React Router's listener can process the navigation.
25+
useEffect(() => {
26+
const handlePopState = (e) => {
27+
// Don't reload for in-app page switches (multi-page navigation within the editor)
28+
const navState = e.state?.usr;
29+
if (navState?.isSwitchingPage) return;
30+
// Prevent React Router from processing this navigation
31+
e.stopImmediatePropagation();
32+
// The popstate has already updated window.location — reload to that URL.
33+
window.location.reload();
34+
};
35+
window.addEventListener('popstate', handlePopState, true);
36+
return () => window.removeEventListener('popstate', handlePopState, true);
1337
}, []);
1438

1539
switch (appType) {

0 commit comments

Comments
 (0)