diff --git a/src/components/Dashboard/common/DashboardTabPanes.jsx b/src/components/Dashboard/common/DashboardTabPanes.jsx
index 85c82935..a381cd10 100644
--- a/src/components/Dashboard/common/DashboardTabPanes.jsx
+++ b/src/components/Dashboard/common/DashboardTabPanes.jsx
@@ -5,6 +5,7 @@ import { Outlet, Routes, UNSAFE_LocationContext, useLocation } from 'react-route
import { TableStateProvider } from '../context/TableStateContext'
import {
NavigationTabActiveContext,
+ NavigationTabIdContext,
createNavigationTabActiveStore,
useNavigationTabs
} from '../context/NavigationTabsContext'
@@ -129,9 +130,11 @@ const DashboardTabPane = memo(function DashboardTabPane({
aria-hidden={!isActive}
inert={!isActive}
>
-
-
-
+
+
+
+
+
)
}, arePanePropsEqual)
diff --git a/src/components/Dashboard/common/ObjectTable.jsx b/src/components/Dashboard/common/ObjectTable.jsx
index 3111dce4..8e9f80e4 100644
--- a/src/components/Dashboard/common/ObjectTable.jsx
+++ b/src/components/Dashboard/common/ObjectTable.jsx
@@ -1778,7 +1778,8 @@ const ObjectTable = memo(
}
}
} else {
- // All tab: persist personal filter/sort to URL/session
+ // All tab: persist personal filter/sort (session/URL, or this
+ // Electron tab's listState JSON when isElectron).
persistTableState(next, nextSorter)
}
const effective = buildEffectiveFilter(next)
diff --git a/src/components/Dashboard/context/NavigationTabsContext.jsx b/src/components/Dashboard/context/NavigationTabsContext.jsx
index 6ad923de..7503e655 100644
--- a/src/components/Dashboard/context/NavigationTabsContext.jsx
+++ b/src/components/Dashboard/context/NavigationTabsContext.jsx
@@ -20,6 +20,8 @@ const NavigationTabMetaContext = createContext({
setTabPage: () => {},
isElectron: false
})
+// eslint-disable-next-line react-refresh/only-export-components
+export const NavigationTabIdContext = createContext(null)
const NavigationTabPreviewsContext = createContext({
tabPreviews: {},
tabPreviewCapturing: {}
@@ -83,13 +85,128 @@ const entryToPath = (entry) => {
const entriesEqual = (left, right) => entryToPath(left) === entryToPath(right)
+const EMPTY_LIST_STATE_ENTRY = {
+ viewId: null,
+ viewName: null,
+ filter: {},
+ sort: {},
+ listFilter: {},
+ listSort: {}
+}
+
+const parseSearchJson = (raw) => {
+ if (!raw) return null
+ try {
+ const parsed = JSON.parse(raw)
+ return parsed && typeof parsed === 'object' ? parsed : null
+ } catch {
+ return null
+ }
+}
+
+const normalizeSorterValue = (sorter) =>
+ sorter?.field && sorter?.order
+ ? { field: sorter.field, order: sorter.order }
+ : {}
+
+const normalizeListStateEntry = (value) => {
+ if (!value || typeof value !== 'object') {
+ return { ...EMPTY_LIST_STATE_ENTRY }
+ }
+ return {
+ viewId: value.viewId ? String(value.viewId) : null,
+ viewName: value.viewName || null,
+ filter:
+ value.filter && typeof value.filter === 'object' ? value.filter : {},
+ sort: normalizeSorterValue(value.sort),
+ listFilter:
+ value.listFilter && typeof value.listFilter === 'object'
+ ? value.listFilter
+ : {},
+ listSort: normalizeSorterValue(value.listSort)
+ }
+}
+
+const normalizeListState = (listState) => {
+ if (!listState || typeof listState !== 'object' || Array.isArray(listState)) {
+ return {}
+ }
+ return Object.fromEntries(
+ Object.entries(listState).map(([scope, value]) => [
+ scope,
+ normalizeListStateEntry(value)
+ ])
+ )
+}
+
+const cloneListState = (listState) =>
+ JSON.parse(JSON.stringify(normalizeListState(listState)))
+
+const getTabListStateEntry = (tab, scope) => {
+ if (!scope) return { ...EMPTY_LIST_STATE_ENTRY }
+ return tab?.listState?.[scope]
+ ? normalizeListStateEntry(tab.listState[scope])
+ : { ...EMPTY_LIST_STATE_ENTRY }
+}
+
+const listStateEntriesEqual = (left, right) =>
+ JSON.stringify(normalizeListStateEntry(left)) ===
+ JSON.stringify(normalizeListStateEntry(right))
+
+const applyListStateToTab = (tab, scope, patch = {}) => {
+ if (!tab || !scope) return tab
+ const current = getTabListStateEntry(tab, scope)
+ const next = normalizeListStateEntry({
+ ...current,
+ ...patch,
+ viewId: patch.viewId !== undefined ? patch.viewId : current.viewId,
+ viewName: patch.viewName !== undefined ? patch.viewName : current.viewName,
+ filter: patch.filter !== undefined ? patch.filter : current.filter,
+ sort: patch.sort !== undefined ? patch.sort : current.sort,
+ listFilter:
+ patch.listFilter !== undefined ? patch.listFilter : current.listFilter,
+ listSort: patch.listSort !== undefined ? patch.listSort : current.listSort
+ })
+ if (listStateEntriesEqual(current, next) && tab.listState?.[scope]) {
+ return tab
+ }
+ return {
+ ...tab,
+ listState: {
+ ...(tab.listState || {}),
+ [scope]: next
+ }
+ }
+}
+
+const seedListStateFromHistory = (tab) => {
+ if (!tab?.modelName) return normalizeListState(tab?.listState)
+ if (tab.listState?.[tab.modelName]) {
+ return normalizeListState(tab.listState)
+ }
+ const entry = getTabCurrentEntry(tab)
+ const params = new URLSearchParams(entry?.search || '')
+ const viewId = params.get('viewId')
+ const filter = parseSearchJson(params.get('filter'))
+ const sort = parseSearchJson(params.get('sort'))
+ if (!viewId && !filter && !sort?.field) {
+ return normalizeListState(tab.listState)
+ }
+ return applyListStateToTab(tab, tab.modelName, {
+ viewId,
+ filter: filter || {},
+ sort: sort || {}
+ }).listState
+}
+
const createTabFromLocation = (location, extras = {}) => ({
id: createTabId(),
title: extras.title || 'Farm Control',
modelName: extras.modelName || null,
iconKey: extras.iconKey || null,
history: [locationToEntry(location)],
- historyIndex: 0
+ historyIndex: 0,
+ listState: cloneListState(extras.listState)
})
const cloneCurrentPageTab = (tab, location) => ({
@@ -98,7 +215,8 @@ const cloneCurrentPageTab = (tab, location) => ({
modelName: tab?.modelName || null,
iconKey: tab?.iconKey || null,
history: [locationToEntry(location)],
- historyIndex: 0
+ historyIndex: 0,
+ listState: cloneListState(tab?.listState)
})
const getTabCurrentEntry = (tab) =>
@@ -117,13 +235,18 @@ const normalizeTab = (tab, fallbackLocation) => {
const rawIndex = Number.isInteger(tab?.historyIndex)
? tab.historyIndex
: history.length - 1
- return {
+ const normalized = {
id: tab?.id || createTabId(),
title: tab?.title || 'Farm Control',
modelName: tab?.modelName || null,
iconKey: tab?.iconKey || null,
history,
- historyIndex: Math.min(Math.max(rawIndex, 0), history.length - 1)
+ historyIndex: Math.min(Math.max(rawIndex, 0), history.length - 1),
+ listState: normalizeListState(tab?.listState)
+ }
+ return {
+ ...normalized,
+ listState: seedListStateFromHistory(normalized)
}
}
@@ -205,19 +328,38 @@ export const NavigationTabsProvider = ({ children }) => {
const previewThemeRef = useRef(isDarkMode ? 'dark' : 'light')
const tabStripRootRef = useRef(null)
const scrollTabStripToEndIntervalRef = useRef(null)
+ // Late URL writes from the tab we just left (e.g. persistView) must not
+ // attach to the newly selected tab.
+ const foreignLocationRef = useRef(null)
tabsRef.current = tabs
activeTabIdRef.current = activeTabId
locationRef.current = location
previewThemeRef.current = isDarkMode ? 'dark' : 'light'
- const isRestoreLocation = useCallback((entry) => {
- const restoring = isRestoringRef.current
- if (!restoring) return false
- if (restoring === true) return true
- return (
- entriesEqual(restoring.target, entry) ||
- entriesEqual(restoring.from, entry)
- )
+ const markForeignLocation = useCallback((previousTabId) => {
+ if (!previousTabId) {
+ foreignLocationRef.current = null
+ return
+ }
+ foreignLocationRef.current = {
+ tabId: previousTabId,
+ entry: locationToEntry(locationRef.current),
+ expiresAt: Date.now() + 250
+ }
+ }, [])
+
+ const applyEntryToTabId = useCallback((tabId, entry) => {
+ if (!tabId || !entry) return
+ setTabs((current) => {
+ let changed = false
+ const next = current.map((tab) => {
+ if (tab.id !== tabId) return tab
+ const updated = applyLocationToTab(tab, entry)
+ if (updated !== tab) changed = true
+ return updated
+ })
+ return changed ? next : current
+ })
}, [])
const shouldIgnoreRestoredLocation = useCallback((entry) => {
@@ -239,11 +381,15 @@ export const NavigationTabsProvider = ({ children }) => {
}, [])
const restoreToEntry = useCallback(
- (entry) => {
+ (entry, { previousTabId } = {}) => {
if (!entry) return
isRestoringRef.current = {
target: locationToEntry(entry),
- from: locationToEntry(locationRef.current)
+ from: locationToEntry(locationRef.current),
+ previousTabId:
+ previousTabId === undefined
+ ? activeTabIdRef.current
+ : previousTabId
}
navigate(entryToPath(entry), { replace: true })
},
@@ -256,16 +402,19 @@ export const NavigationTabsProvider = ({ children }) => {
const nextTab = tabsRef.current.find((tab) => tab.id === tabId)
if (!nextTab) return
+ const previousTabId = activeTabIdRef.current
const nextEntry = getTabCurrentEntry(nextTab)
+ markForeignLocation(previousTabId)
setActiveTabId(tabId)
+ activeTabIdRef.current = tabId
if (entriesEqual(nextEntry, locationRef.current)) {
return
}
- restoreToEntry(nextEntry)
+ restoreToEntry(nextEntry, { previousTabId })
},
- [restoreToEntry]
+ [markForeignLocation, restoreToEntry]
)
const registerTabStripRoot = useCallback((element) => {
@@ -325,17 +474,19 @@ export const NavigationTabsProvider = ({ children }) => {
? createTabFromLocation(entry)
: cloneCurrentPageTab(currentTab, location)
+ const previousTabId = activeTabIdRef.current
setTabs((current) => [...current, nextTab])
+ markForeignLocation(previousTabId)
setActiveTabId(nextTab.id)
activeTabIdRef.current = nextTab.id
if (targetUrl && !entriesEqual(entry, locationRef.current)) {
- restoreToEntry(entry)
+ restoreToEntry(entry, { previousTabId })
}
scrollTabStripToEnd()
},
- [location, restoreToEntry, scrollTabStripToEnd]
+ [location, markForeignLocation, restoreToEntry, scrollTabStripToEnd]
)
const removeTab = useCallback(
@@ -371,15 +522,18 @@ export const NavigationTabsProvider = ({ children }) => {
return nextCapturing
})
if (nextActive) {
+ const previousTabId = activeTabIdRef.current
+ markForeignLocation(previousTabId)
setActiveTabId(nextActive.id)
+ activeTabIdRef.current = nextActive.id
const nextEntry = getTabCurrentEntry(nextActive)
if (!entriesEqual(nextEntry, locationRef.current)) {
- restoreToEntry(nextEntry)
+ restoreToEntry(nextEntry, { previousTabId })
}
}
return true
},
- [handleWindowControl, restoreToEntry]
+ [handleWindowControl, markForeignLocation, restoreToEntry]
)
const closeTab = useCallback(
@@ -409,13 +563,16 @@ export const NavigationTabsProvider = ({ children }) => {
next.splice(insertBefore ? targetIndex : targetIndex + 1, 0, tab)
return next
})
+ const previousTabId = activeTabIdRef.current
+ markForeignLocation(previousTabId)
setActiveTabId(tab.id)
+ activeTabIdRef.current = tab.id
const nextEntry = getTabCurrentEntry(tab)
if (!entriesEqual(nextEntry, locationRef.current)) {
- restoreToEntry(nextEntry)
+ restoreToEntry(nextEntry, { previousTabId })
}
},
- [restoreToEntry]
+ [markForeignLocation, restoreToEntry]
)
const handleTabDragStart = useCallback(
@@ -549,31 +706,45 @@ export const NavigationTabsProvider = ({ children }) => {
}, [])
const setTabPage = useCallback(
- ({ title, modelName, iconKey, location: pageLocation } = {}) => {
+ ({ title, modelName, iconKey } = {}) => {
const activeId = activeTabIdRef.current
if (!activeId) return
- const pageEntry = pageLocation ? locationToEntry(pageLocation) : null
-
setTabs((current) => {
if (current.length === 0) return current
let changed = false
const next = current.map((tab) => {
if (tab.id !== activeId) return tab
- let updated = applyPageMetaToTab(tab, { title, modelName, iconKey })
- if (pageEntry && !isRestoreLocation(pageEntry)) {
- updated = applyLocationToTab(updated, pageEntry)
- }
+ const updated = applyPageMetaToTab(tab, { title, modelName, iconKey })
if (updated !== tab) changed = true
return updated
})
return changed ? next : current
})
},
- [isRestoreLocation]
+ []
)
+ const getTabListState = useCallback((tabId, scope) => {
+ const tab = tabsRef.current.find((item) => item.id === tabId)
+ return getTabListStateEntry(tab, scope)
+ }, [])
+
+ const setTabListState = useCallback((tabId, scope, patch) => {
+ if (!tabId || !scope) return
+ setTabs((current) => {
+ let changed = false
+ const next = current.map((tab) => {
+ if (tab.id !== tabId) return tab
+ const updated = applyListStateToTab(tab, scope, patch)
+ if (updated !== tab) changed = true
+ return updated
+ })
+ return changed ? next : current
+ })
+ }, [])
+
const goBack = useCallback(() => {
const currentTab = tabsRef.current.find(
(tab) => tab.id === activeTabIdRef.current
@@ -676,8 +847,47 @@ export const NavigationTabsProvider = ({ children }) => {
if (!isElectron || !hydrated) return
const entry = locationToEntry(location)
+ const restoring = isRestoringRef.current
+ const foreign = foreignLocationRef.current
+
+ if (restoring && restoring !== true) {
+ if (entriesEqual(restoring.target, entry)) {
+ isRestoringRef.current = null
+ return
+ }
+ if (entriesEqual(restoring.from, entry)) {
+ return
+ }
+ // A delayed write from the previous tab (viewId, filters, etc.)
+ if (restoring.previousTabId) {
+ applyEntryToTabId(restoring.previousTabId, entry)
+ if (foreign) foreign.entry = entry
+ }
+ return
+ }
+
if (shouldIgnoreRestoredLocation(entry)) return
+ if (
+ foreign &&
+ foreign.tabId &&
+ foreign.tabId !== activeTabIdRef.current &&
+ Date.now() < foreign.expiresAt
+ ) {
+ if (entriesEqual(entry, foreign.entry)) {
+ return
+ }
+ if (entry.pathname === foreign.entry.pathname) {
+ applyEntryToTabId(foreign.tabId, entry)
+ foreign.entry = entry
+ return
+ }
+ }
+
+ if (foreign && Date.now() >= foreign.expiresAt) {
+ foreignLocationRef.current = null
+ }
+
setTabs((current) => {
if (current.length === 0) {
const initialTab = createTabFromLocation(location)
@@ -694,7 +904,13 @@ export const NavigationTabsProvider = ({ children }) => {
})
return changed ? next : current
})
- }, [hydrated, isElectron, location, shouldIgnoreRestoredLocation])
+ }, [
+ applyEntryToTabId,
+ hydrated,
+ isElectron,
+ location,
+ shouldIgnoreRestoredLocation
+ ])
useEffect(() => {
if (!isElectron || !hydrated || !syncWindowTabs) return undefined
@@ -768,7 +984,9 @@ export const NavigationTabsProvider = ({ children }) => {
createNewWindow,
registerTabPane,
registerTabStripRoot,
- captureTabPreview
+ captureTabPreview,
+ getTabListState,
+ setTabListState
}),
[
activeTab,
@@ -792,6 +1010,8 @@ export const NavigationTabsProvider = ({ children }) => {
reorderTabs,
selectTab,
setTabPage,
+ getTabListState,
+ setTabListState,
tabs
]
)
@@ -850,19 +1070,18 @@ export const useTabPreview = (tabId) => {
export const useNavigationTabPage = ({ title, modelName, iconKey } = {}) => {
const { setTabPage, isElectron } = useContext(NavigationTabMetaContext)
const store = useContext(NavigationTabActiveContext)
- const pageLocation = useLocation()
useEffect(() => {
if (!isElectron || !title) return undefined
const sync = () => {
if (!store.getSnapshot()) return
- setTabPage({ title, modelName, iconKey, location: pageLocation })
+ setTabPage({ title, modelName, iconKey })
}
sync()
return store.subscribe(sync)
- }, [iconKey, isElectron, modelName, pageLocation, setTabPage, store, title])
+ }, [iconKey, isElectron, modelName, setTabPage, store, title])
}
// eslint-disable-next-line react-refresh/only-export-components
diff --git a/src/components/Dashboard/context/ObjectListViewContext.jsx b/src/components/Dashboard/context/ObjectListViewContext.jsx
index e48e21ea..81a47d77 100644
--- a/src/components/Dashboard/context/ObjectListViewContext.jsx
+++ b/src/components/Dashboard/context/ObjectListViewContext.jsx
@@ -15,7 +15,10 @@ import { useTableState } from './TableStateContext'
import useViewMode from '../hooks/useViewMode'
import { DEFAULT_VIEW_MODE, normalizeViewMode } from '../common/viewModeUtils'
import { getModelByName } from '../../../database/ObjectModels'
-import { useNavigationTabPage } from './NavigationTabsContext'
+import {
+ useNavigationTabPage,
+ useNavigationTabs
+} from './NavigationTabsContext'
const ObjectListViewContext = createContext()
@@ -128,14 +131,10 @@ export const ObjectListViewProvider = ({ children, objectType, tableRef }) => {
subscribeToObjectUpdates
} = useContext(ApiServerContext)
const { token, authInitialized } = useContext(AuthContext)
- const { getViewFromUrl, persistView, setObjectListView } = useTableState()
+ const { getViewFromUrl, getPersistedView, persistView, setObjectListView } =
+ useTableState()
+ const { isElectron } = useNavigationTabs()
const listModel = getModelByName(objectType)
- useNavigationTabPage({
- title: listModel?.labelPlural
- ? `List - ${listModel.labelPlural}`
- : 'List',
- modelName: objectType
- })
const [views, setViews] = useState(() => readCachedViews(objectType) || [])
const [draftViews, setDraftViews] = useState(() => {
@@ -203,6 +202,13 @@ export const ObjectListViewProvider = ({ children, objectType, tableRef }) => {
return view
}, [activeTabKey, displayedViews])
+ const listLabel = listModel?.labelPlural || 'List'
+ const viewName = activeView?.name
+ useNavigationTabPage({
+ title: viewName ? `${listLabel} - ${viewName}` : listLabel,
+ modelName: objectType
+ })
+
viewsStateRef.current = { views, draftViews, isEditing }
activeViewRef.current = activeView
@@ -297,11 +303,13 @@ export const ObjectListViewProvider = ({ children, objectType, tableRef }) => {
const name =
key && view && typeof view === 'object' ? view.name || null : null
- persistView(key)
- writeLastViewId(objectType, key)
+ persistView(key, { scope: objectType, name })
+ if (!isElectron) {
+ writeLastViewId(objectType, key)
+ }
setObjectListView(objectType, key ? { id: key, name } : null)
},
- [objectType, persistView, setObjectListView]
+ [isElectron, objectType, persistView, setObjectListView]
)
useEffect(() => {
@@ -353,9 +361,10 @@ export const ObjectListViewProvider = ({ children, objectType, tableRef }) => {
// Allow early restore from cache; only block when we have nothing to match against yet
if (initialLoading && views.length === 0) return
- const urlViewId = getViewFromUrl()
- const rememberedViewId = readLastViewId(objectType)
- const candidateViewId = urlViewId || rememberedViewId
+ const persistedViewId = getPersistedView(objectType)?.id || null
+ const urlViewId = isElectron ? null : getViewFromUrl()
+ const rememberedViewId = isElectron ? null : readLastViewId(objectType)
+ const candidateViewId = persistedViewId || urlViewId || rememberedViewId
if (!candidateViewId) {
urlSyncedRef.current = true
@@ -370,14 +379,18 @@ export const ObjectListViewProvider = ({ children, objectType, tableRef }) => {
if (matched) {
const matchedId = String(matched._id)
setActiveTabKey(matchedId)
- writeLastViewId(objectType, matchedId)
+ if (!isElectron) {
+ writeLastViewId(objectType, matchedId)
+ }
setObjectListView(objectType, {
id: matchedId,
name: matched.name || null
})
if (!matched._isDraft) {
- // Keep viewId in the URL and strip any leftover filter/sort params
- persistView(matchedId)
+ persistView(matchedId, {
+ scope: objectType,
+ name: matched.name || null
+ })
}
urlSyncedRef.current = true
setViewSyncReady(true)
@@ -400,6 +413,8 @@ export const ObjectListViewProvider = ({ children, objectType, tableRef }) => {
draftViews,
isEditing,
objectType,
+ isElectron,
+ getPersistedView,
getViewFromUrl,
persistView,
rememberActiveView,
@@ -903,36 +918,33 @@ export const ObjectListViewProvider = ({ children, objectType, tableRef }) => {
// route filter/sort changes into the draft instead of URL/session persistence.
const activeObjectView = isEditing ? activeView : null
- const handleViewModeChange = useCallback(
- async (viewMode) => {
- const view = activeViewRef.current
- if (!view || activeTabKeyRef.current === ALL_TAB_KEY) return
+ const handleViewModeChange = useCallback(async (viewMode) => {
+ const view = activeViewRef.current
+ if (!view || activeTabKeyRef.current === ALL_TAB_KEY) return
- if (isEditingRef.current) {
- const updateInList = (list) =>
- list.map((item) =>
- String(item._id) === String(view._id) ? { ...item, viewMode } : item
- )
+ if (isEditingRef.current) {
+ const updateInList = (list) =>
+ list.map((item) =>
+ String(item._id) === String(view._id) ? { ...item, viewMode } : item
+ )
- activeViewRef.current = { ...view, viewMode }
+ activeViewRef.current = { ...view, viewMode }
- setDraftViews((prev) => updateInList(prev))
- viewsStateRef.current = {
- ...viewsStateRef.current,
- draftViews: updateInList(viewsStateRef.current.draftViews)
- }
- return
+ setDraftViews((prev) => updateInList(prev))
+ viewsStateRef.current = {
+ ...viewsStateRef.current,
+ draftViews: updateInList(viewsStateRef.current.draftViews)
}
+ return
+ }
- // Not editing: store as a temporary user override instead of saving
- setUserViewModeOverride(viewMode)
- userOverridesRef.current = {
- ...(userOverridesRef.current || {}),
- viewMode
- }
- },
- []
- )
+ // Not editing: store as a temporary user override instead of saving
+ setUserViewModeOverride(viewMode)
+ userOverridesRef.current = {
+ ...(userOverridesRef.current || {}),
+ viewMode
+ }
+ }, [])
const isCustomView = activeTabKey !== ALL_TAB_KEY
diff --git a/src/components/Dashboard/context/TableStateContext.jsx b/src/components/Dashboard/context/TableStateContext.jsx
index 2eb8274d..b2c6835d 100644
--- a/src/components/Dashboard/context/TableStateContext.jsx
+++ b/src/components/Dashboard/context/TableStateContext.jsx
@@ -9,6 +9,10 @@ import {
} from 'react'
import { useSearchParams } from 'react-router-dom'
import PropTypes from 'prop-types'
+import {
+ NavigationTabIdContext,
+ useNavigationTabs
+} from './NavigationTabsContext'
const TableStateContext = createContext(null)
@@ -166,6 +170,9 @@ const normalizeSorter = (sorter) => {
}
export const TableStateProvider = ({ children }) => {
+ const tabId = useContext(NavigationTabIdContext)
+ const { isElectron, getTabListState, setTabListState, activeTab } =
+ useNavigationTabs()
const [searchParams, setSearchParams] = useSearchParams()
const [pageFilters, setPageFilters] = useState({})
const [pageSorters, setPageSorters] = useState({})
@@ -214,8 +221,17 @@ export const TableStateProvider = ({ children }) => {
[setSearchParams]
)
+ const resolveScopedTabId = useCallback(
+ () => tabId || (isElectron ? activeTab?.id : null),
+ [activeTab?.id, isElectron, tabId]
+ )
+
const getPersistedFilter = useCallback(
(scope, { useFilterInUrl = false, useFilterInSession = false } = {}) => {
+ const scopedTabId = resolveScopedTabId()
+ if (isElectron && scopedTabId) {
+ return getTabListState(scopedTabId, scope).filter || {}
+ }
if (useFilterInUrl) {
const fromUrl = readFilterFromUrl(searchParams)
if (fromUrl) return fromUrl
@@ -226,11 +242,15 @@ export const TableStateProvider = ({ children }) => {
}
return {}
},
- [searchParams]
+ [getTabListState, isElectron, resolveScopedTabId, searchParams]
)
const getPersistedSorter = useCallback(
(scope, { useSortInUrl = false, useSortInSession = false } = {}) => {
+ const scopedTabId = resolveScopedTabId()
+ if (isElectron && scopedTabId) {
+ return getTabListState(scopedTabId, scope).sort || {}
+ }
if (useSortInUrl) {
const fromUrl = readSortFromUrl(searchParams)
if (fromUrl) return fromUrl
@@ -241,7 +261,7 @@ export const TableStateProvider = ({ children }) => {
}
return {}
},
- [searchParams]
+ [getTabListState, isElectron, resolveScopedTabId, searchParams]
)
const getViewFromUrl = useCallback(
@@ -249,8 +269,36 @@ export const TableStateProvider = ({ children }) => {
[searchParams]
)
+ const getPersistedView = useCallback(
+ (scope) => {
+ const scopedTabId = resolveScopedTabId()
+ if (isElectron && scopedTabId) {
+ const entry = getTabListState(scopedTabId, scope)
+ return entry.viewId
+ ? { id: String(entry.viewId), name: entry.viewName || null }
+ : null
+ }
+ const fromUrl = readViewFromUrl(searchParams)
+ if (fromUrl) return { id: String(fromUrl), name: null }
+ return readListViewFromSession(scope)
+ },
+ [getTabListState, isElectron, resolveScopedTabId, searchParams]
+ )
+
const persistView = useCallback(
- (viewId, { clearFilterSort = Boolean(viewId) } = {}) => {
+ (
+ viewId,
+ { clearFilterSort = Boolean(viewId), scope, name } = {}
+ ) => {
+ const scopedTabId = resolveScopedTabId()
+ if (isElectron && scopedTabId && scope) {
+ setTabListState(scopedTabId, scope, {
+ viewId: viewId ? String(viewId) : null,
+ viewName: viewId ? name || null : null
+ })
+ return
+ }
+
updateSearchParams((next) => {
// Drop legacy `view` param that previously stored `_reference`
next.delete('view')
@@ -266,7 +314,7 @@ export const TableStateProvider = ({ children }) => {
}
})
},
- [updateSearchParams]
+ [isElectron, resolveScopedTabId, setTabListState, updateSearchParams]
)
const persistFilter = useCallback(
@@ -276,6 +324,13 @@ export const TableStateProvider = ({ children }) => {
{ saveFilterInSession = false, saveFilterInUrl = false } = {}
) => {
const active = getActiveFilterValues(filterState)
+ const scopedTabId = resolveScopedTabId()
+ if (isElectron && scopedTabId) {
+ if (saveFilterInSession || saveFilterInUrl) {
+ setTabListState(scopedTabId, scope, { filter: active })
+ }
+ return
+ }
if (saveFilterInSession) {
writeFilterToSession(scope, active)
}
@@ -289,7 +344,7 @@ export const TableStateProvider = ({ children }) => {
})
}
},
- [updateSearchParams]
+ [isElectron, resolveScopedTabId, setTabListState, updateSearchParams]
)
const persistSort = useCallback(
@@ -299,6 +354,13 @@ export const TableStateProvider = ({ children }) => {
{ saveSortInSession = false, saveSortInUrl = false } = {}
) => {
const nextSorter = normalizeSorter(sorter)
+ const scopedTabId = resolveScopedTabId()
+ if (isElectron && scopedTabId) {
+ if (saveSortInSession || saveSortInUrl) {
+ setTabListState(scopedTabId, scope, { sort: nextSorter })
+ }
+ return
+ }
if (saveSortInSession) {
writeSortToSession(scope, nextSorter)
}
@@ -312,7 +374,7 @@ export const TableStateProvider = ({ children }) => {
})
}
},
- [updateSearchParams]
+ [isElectron, resolveScopedTabId, setTabListState, updateSearchParams]
)
const persistTableState = useCallback(
@@ -329,6 +391,21 @@ export const TableStateProvider = ({ children }) => {
) => {
const active = getActiveFilterValues(filterState)
const nextSorter = normalizeSorter(sorter)
+ const scopedTabId = resolveScopedTabId()
+
+ if (isElectron && scopedTabId) {
+ const patch = {}
+ if (saveFilterInSession || saveFilterInUrl) {
+ patch.filter = active
+ }
+ if (saveSortInSession || saveSortInUrl) {
+ patch.sort = nextSorter
+ }
+ if (Object.keys(patch).length > 0) {
+ setTabListState(scopedTabId, scope, patch)
+ }
+ return
+ }
if (saveFilterInSession) {
writeFilterToSession(scope, active)
@@ -356,7 +433,7 @@ export const TableStateProvider = ({ children }) => {
})
}
},
- [updateSearchParams]
+ [isElectron, resolveScopedTabId, setTabListState, updateSearchParams]
)
const setPageFilter = useCallback((path, filter) => {
@@ -389,7 +466,12 @@ export const TableStateProvider = ({ children }) => {
const setObjectListFilter = useCallback((objectType, filter) => {
if (!objectType) return
const active = getActiveFilterValues(filter)
- writeListFilterToSession(objectType, active)
+ const scopedTabId = resolveScopedTabId()
+ if (isElectron && scopedTabId) {
+ setTabListState(scopedTabId, objectType, { listFilter: active })
+ } else {
+ writeListFilterToSession(objectType, active)
+ }
setObjectListFilters((prev) => {
const hasActive = Object.keys(active).length > 0
if (!hasActive) {
@@ -400,12 +482,17 @@ export const TableStateProvider = ({ children }) => {
}
return { ...prev, [objectType]: active }
})
- }, [])
+ }, [isElectron, resolveScopedTabId, setTabListState])
const setObjectListSorter = useCallback((objectType, sorter) => {
if (!objectType) return
const nextSorter = normalizeSorter(sorter)
- writeListSortToSession(objectType, nextSorter)
+ const scopedTabId = resolveScopedTabId()
+ if (isElectron && scopedTabId) {
+ setTabListState(scopedTabId, objectType, { listSort: nextSorter })
+ } else {
+ writeListSortToSession(objectType, nextSorter)
+ }
setObjectListSorters((prev) => {
const hasSort = nextSorter?.field && nextSorter?.order
if (!hasSort) {
@@ -416,7 +503,7 @@ export const TableStateProvider = ({ children }) => {
}
return { ...prev, [objectType]: nextSorter }
})
- }, [])
+ }, [isElectron, resolveScopedTabId, setTabListState])
const setObjectListView = useCallback((objectType, view) => {
if (!objectType) return
@@ -424,7 +511,15 @@ export const TableStateProvider = ({ children }) => {
view?.id != null
? { id: String(view.id), name: view.name || null }
: null
- writeListViewToSession(objectType, nextView)
+ const scopedTabId = resolveScopedTabId()
+ if (isElectron && scopedTabId) {
+ setTabListState(scopedTabId, objectType, {
+ viewId: nextView?.id || null,
+ viewName: nextView?.name || null
+ })
+ } else {
+ writeListViewToSession(objectType, nextView)
+ }
setObjectListViews((prev) => {
if (!nextView) {
if (!(objectType in prev)) return prev
@@ -442,7 +537,7 @@ export const TableStateProvider = ({ children }) => {
}
return { ...prev, [objectType]: nextView }
})
- }, [])
+ }, [isElectron, resolveScopedTabId, setTabListState])
const getObjectListFilter = useCallback(
(objectType) => {
@@ -450,9 +545,15 @@ export const TableStateProvider = ({ children }) => {
if (objectListFilters[objectType]) {
return objectListFilters[objectType]
}
+ const scopedTabId = resolveScopedTabId()
+ if (isElectron && scopedTabId) {
+ return getActiveFilterValues(
+ getTabListState(scopedTabId, objectType).listFilter || {}
+ )
+ }
return getActiveFilterValues(readListFilterFromSession(objectType) || {})
},
- [objectListFilters]
+ [getTabListState, isElectron, objectListFilters, resolveScopedTabId]
)
const getObjectListSorter = useCallback(
@@ -461,9 +562,13 @@ export const TableStateProvider = ({ children }) => {
if (objectListSorters[objectType]) {
return objectListSorters[objectType]
}
+ const scopedTabId = resolveScopedTabId()
+ if (isElectron && scopedTabId) {
+ return getTabListState(scopedTabId, objectType).listSort || {}
+ }
return readListSortFromSession(objectType) || {}
},
- [objectListSorters]
+ [getTabListState, isElectron, objectListSorters, resolveScopedTabId]
)
const getObjectListView = useCallback(
@@ -472,9 +577,16 @@ export const TableStateProvider = ({ children }) => {
if (objectListViews[objectType]) {
return objectListViews[objectType]
}
+ const scopedTabId = resolveScopedTabId()
+ if (isElectron && scopedTabId) {
+ const entry = getTabListState(scopedTabId, objectType)
+ return entry.viewId
+ ? { id: String(entry.viewId), name: entry.viewName || null }
+ : null
+ }
return readListViewFromSession(objectType)
},
- [objectListViews]
+ [getTabListState, isElectron, objectListViews, resolveScopedTabId]
)
const hasPageFilter = useCallback(
@@ -484,6 +596,17 @@ export const TableStateProvider = ({ children }) => {
const hasStoredFilter = useCallback((scope) => {
if (!scope) return false
+ const scopedTabId = resolveScopedTabId()
+ if (isElectron && scopedTabId) {
+ const entry = getTabListState(scopedTabId, scope)
+ if (entry.viewId) {
+ return Object.keys(entry.listFilter || {}).length > 0
+ }
+ return (
+ Object.keys(entry.filter || {}).length > 0 ||
+ Object.keys(entry.listFilter || {}).length > 0
+ )
+ }
// When a view is active, only report the effective list filter (which
// already accounts for the view). Don't fall back to the All-tab session
// filter — that belongs to a different context.
@@ -493,12 +616,19 @@ export const TableStateProvider = ({ children }) => {
if (hasStoredSessionFilter(scope)) return true
if (Object.keys(objectListFilters[scope] || {}).length > 0) return true
return Object.keys(getActiveFilterValues(readListFilterFromSession(scope) || {})).length > 0
- }, [objectListFilters, objectListViews])
+ }, [
+ getTabListState,
+ isElectron,
+ objectListFilters,
+ objectListViews,
+ resolveScopedTabId
+ ])
const value = useMemo(
() => ({
getPersistedFilter,
getPersistedSorter,
+ getPersistedView,
getViewFromUrl,
persistView,
persistFilter,
@@ -523,6 +653,7 @@ export const TableStateProvider = ({ children }) => {
[
getPersistedFilter,
getPersistedSorter,
+ getPersistedView,
getViewFromUrl,
persistView,
persistFilter,
diff --git a/src/components/Dashboard/hooks/useObjectNavigationTabPage.jsx b/src/components/Dashboard/hooks/useObjectNavigationTabPage.jsx
index 51805331..6b576017 100644
--- a/src/components/Dashboard/hooks/useObjectNavigationTabPage.jsx
+++ b/src/components/Dashboard/hooks/useObjectNavigationTabPage.jsx
@@ -5,9 +5,7 @@ import { AuthContext } from '../context/AuthContext'
import { useNavigationTabPage } from '../context/NavigationTabsContext'
const formatPageLabel = (pageName) =>
- pageName
- ? `${pageName.charAt(0).toUpperCase()}${pageName.slice(1)}`
- : 'Page'
+ pageName ? `${pageName.charAt(0).toUpperCase()}${pageName.slice(1)}` : 'Page'
const getObjectDisplayName = (objectData, model) => {
if (objectData?.name) return objectData.name
@@ -17,18 +15,9 @@ const getObjectDisplayName = (objectData, model) => {
return null
}
-export const formatObjectPageTabTitle = ({
- pageName,
- model,
- objectName
-} = {}) => {
+export const formatObjectPageTabTitle = ({ pageName, objectName } = {}) => {
const pageLabel = formatPageLabel(pageName)
- const collectionLabel = model?.labelPlural
- if (objectName && collectionLabel) {
- return `${pageLabel} (${objectName}) - ${collectionLabel}`
- }
- if (objectName) return `${pageLabel} (${objectName})`
- if (collectionLabel) return `${pageLabel} - ${collectionLabel}`
+ if (objectName) return `${objectName} - ${pageLabel}`
return pageLabel
}
@@ -74,7 +63,7 @@ export const useObjectNavigationTabPage = ({
}, [connected, model, modelName, objectId, subscribeToObjectUpdates])
useNavigationTabPage({
- title: formatObjectPageTabTitle({ pageName, model, objectName }),
+ title: formatObjectPageTabTitle({ pageName, objectName }),
modelName
})
}