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 }) }