Compare commits
3 Commits
44c8729787
...
e7f3e2a6d4
| Author | SHA1 | Date | |
|---|---|---|---|
| e7f3e2a6d4 | |||
| 4f6f7cb881 | |||
| d348d6f2b3 |
@ -5,6 +5,7 @@ import { Outlet, Routes, UNSAFE_LocationContext, useLocation } from 'react-route
|
|||||||
import { TableStateProvider } from '../context/TableStateContext'
|
import { TableStateProvider } from '../context/TableStateContext'
|
||||||
import {
|
import {
|
||||||
NavigationTabActiveContext,
|
NavigationTabActiveContext,
|
||||||
|
NavigationTabIdContext,
|
||||||
createNavigationTabActiveStore,
|
createNavigationTabActiveStore,
|
||||||
useNavigationTabs
|
useNavigationTabs
|
||||||
} from '../context/NavigationTabsContext'
|
} from '../context/NavigationTabsContext'
|
||||||
@ -23,7 +24,7 @@ const toTabLocation = (tabId, source) => ({
|
|||||||
pathname: source?.pathname || '/',
|
pathname: source?.pathname || '/',
|
||||||
search: source?.search ?? '',
|
search: source?.search ?? '',
|
||||||
hash: source?.hash ?? '',
|
hash: source?.hash ?? '',
|
||||||
state: source?.state,
|
state: source?.state ?? null,
|
||||||
key: `tab-${tabId}`
|
key: `tab-${tabId}`
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -32,7 +33,7 @@ const reuseTabLocation = (tabId, previous, next) => {
|
|||||||
previous &&
|
previous &&
|
||||||
locationsEqual(previous, next) &&
|
locationsEqual(previous, next) &&
|
||||||
previous.key === `tab-${tabId}` &&
|
previous.key === `tab-${tabId}` &&
|
||||||
previous.state === next.state
|
(previous.state ?? null) === (next.state ?? null)
|
||||||
) {
|
) {
|
||||||
return previous
|
return previous
|
||||||
}
|
}
|
||||||
@ -44,7 +45,7 @@ const entryToLocation = (entry, fallbackLocation, tabId) =>
|
|||||||
pathname: entry?.pathname || fallbackLocation.pathname,
|
pathname: entry?.pathname || fallbackLocation.pathname,
|
||||||
search: entry?.search ?? fallbackLocation.search ?? '',
|
search: entry?.search ?? fallbackLocation.search ?? '',
|
||||||
hash: entry?.hash ?? fallbackLocation.hash ?? '',
|
hash: entry?.hash ?? fallbackLocation.hash ?? '',
|
||||||
state: fallbackLocation.state
|
state: entry?.state ?? null
|
||||||
})
|
})
|
||||||
|
|
||||||
const getTabCurrentEntry = (tab) =>
|
const getTabCurrentEntry = (tab) =>
|
||||||
@ -56,6 +57,12 @@ const tabMatchesLocation = (tab, loc) => {
|
|||||||
return locationsEqual(entry, loc)
|
return locationsEqual(entry, loc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const locationBelongsToLeavingTab = (tab, frozen, loc) => {
|
||||||
|
if (!loc) return false
|
||||||
|
if (frozen && locationsEqual(frozen, loc)) return true
|
||||||
|
return tabMatchesLocation(tab, loc)
|
||||||
|
}
|
||||||
|
|
||||||
const CachedTabRouteLayout = () => (
|
const CachedTabRouteLayout = () => (
|
||||||
<TableStateProvider>
|
<TableStateProvider>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
@ -66,7 +73,7 @@ const areLocationsEqual = (left, right) =>
|
|||||||
left === right ||
|
left === right ||
|
||||||
(locationsEqual(left, right) &&
|
(locationsEqual(left, right) &&
|
||||||
left?.key === right?.key &&
|
left?.key === right?.key &&
|
||||||
left?.state === right?.state)
|
(left?.state ?? null) === (right?.state ?? null))
|
||||||
|
|
||||||
const arePanePropsEqual = (prev, next) =>
|
const arePanePropsEqual = (prev, next) =>
|
||||||
prev.tabId === next.tabId &&
|
prev.tabId === next.tabId &&
|
||||||
@ -129,9 +136,11 @@ const DashboardTabPane = memo(function DashboardTabPane({
|
|||||||
aria-hidden={!isActive}
|
aria-hidden={!isActive}
|
||||||
inert={!isActive}
|
inert={!isActive}
|
||||||
>
|
>
|
||||||
<NavigationTabActiveContext.Provider value={storeRef.current}>
|
<NavigationTabIdContext.Provider value={tabId}>
|
||||||
<DashboardTabPaneRoutes loc={loc} />
|
<NavigationTabActiveContext.Provider value={storeRef.current}>
|
||||||
</NavigationTabActiveContext.Provider>
|
<DashboardTabPaneRoutes loc={loc} />
|
||||||
|
</NavigationTabActiveContext.Provider>
|
||||||
|
</NavigationTabIdContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}, arePanePropsEqual)
|
}, arePanePropsEqual)
|
||||||
@ -163,14 +172,20 @@ const DashboardTabPanes = () => {
|
|||||||
const switchedTabs = Boolean(prevActiveId && activeTabId && prevActiveId !== activeTabId)
|
const switchedTabs = Boolean(prevActiveId && activeTabId && prevActiveId !== activeTabId)
|
||||||
|
|
||||||
if (switchedTabs) {
|
if (switchedTabs) {
|
||||||
frozenRef.current.set(
|
const previousTab = tabs.find((tab) => tab.id === prevActiveId)
|
||||||
prevActiveId,
|
const previousFrozen = frozenRef.current.get(prevActiveId)
|
||||||
reuseTabLocation(
|
const lastLoc = lastLocationRef.current
|
||||||
|
if (locationBelongsToLeavingTab(previousTab, previousFrozen, lastLoc)) {
|
||||||
|
frozenRef.current.set(
|
||||||
prevActiveId,
|
prevActiveId,
|
||||||
frozenRef.current.get(prevActiveId),
|
reuseTabLocation(prevActiveId, previousFrozen, lastLoc)
|
||||||
lastLocationRef.current
|
|
||||||
)
|
)
|
||||||
)
|
} else if (!previousFrozen && previousTab) {
|
||||||
|
frozenRef.current.set(
|
||||||
|
prevActiveId,
|
||||||
|
entryToLocation(getTabCurrentEntry(previousTab), lastLoc, prevActiveId)
|
||||||
|
)
|
||||||
|
}
|
||||||
pendingRestoreRef.current = true
|
pendingRestoreRef.current = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1778,7 +1778,8 @@ const ObjectTable = memo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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)
|
persistTableState(next, nextSorter)
|
||||||
}
|
}
|
||||||
const effective = buildEffectiveFilter(next)
|
const effective = buildEffectiveFilter(next)
|
||||||
|
|||||||
@ -62,23 +62,27 @@ const ActionsProvider = ({ children }) => {
|
|||||||
const [ctrlDown, setCtrlDown] = useState(false)
|
const [ctrlDown, setCtrlDown] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const isHoldKey = (key) => key === 'Meta' || key === 'Control'
|
||||||
const handleKeyDown = (event) => {
|
const handleKeyDown = (event) => {
|
||||||
const key = event.key
|
if (isHoldKey(event.key)) {
|
||||||
if (key === 'Meta') {
|
|
||||||
setCtrlDown(true)
|
setCtrlDown(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const handleKeyUp = (event) => {
|
const handleKeyUp = (event) => {
|
||||||
const key = event.key
|
if (isHoldKey(event.key)) {
|
||||||
if (key === 'Meta') {
|
|
||||||
setCtrlDown(false)
|
setCtrlDown(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const clearHold = () => setCtrlDown(false)
|
||||||
window.addEventListener('keydown', handleKeyDown)
|
window.addEventListener('keydown', handleKeyDown)
|
||||||
window.addEventListener('keyup', handleKeyUp)
|
window.addEventListener('keyup', handleKeyUp)
|
||||||
|
window.addEventListener('blur', clearHold)
|
||||||
|
document.addEventListener('visibilitychange', clearHold)
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', handleKeyDown)
|
window.removeEventListener('keydown', handleKeyDown)
|
||||||
window.removeEventListener('keyup', handleKeyUp)
|
window.removeEventListener('keyup', handleKeyUp)
|
||||||
|
window.removeEventListener('blur', clearHold)
|
||||||
|
document.removeEventListener('visibilitychange', clearHold)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -242,9 +246,10 @@ const ActionsProvider = ({ children }) => {
|
|||||||
setCurrentObjectType,
|
setCurrentObjectType,
|
||||||
callAction,
|
callAction,
|
||||||
clearAction,
|
clearAction,
|
||||||
setOnModalOk
|
setOnModalOk,
|
||||||
|
ctrlDown
|
||||||
}),
|
}),
|
||||||
[callAction, clearAction, currentObject, currentObjectType]
|
[callAction, clearAction, currentObject, currentObjectType, ctrlDown]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -20,6 +20,8 @@ const NavigationTabMetaContext = createContext({
|
|||||||
setTabPage: () => {},
|
setTabPage: () => {},
|
||||||
isElectron: false
|
isElectron: false
|
||||||
})
|
})
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
|
export const NavigationTabIdContext = createContext(null)
|
||||||
const NavigationTabPreviewsContext = createContext({
|
const NavigationTabPreviewsContext = createContext({
|
||||||
tabPreviews: {},
|
tabPreviews: {},
|
||||||
tabPreviewCapturing: {}
|
tabPreviewCapturing: {}
|
||||||
@ -83,13 +85,128 @@ const entryToPath = (entry) => {
|
|||||||
|
|
||||||
const entriesEqual = (left, right) => entryToPath(left) === entryToPath(right)
|
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 = {}) => ({
|
const createTabFromLocation = (location, extras = {}) => ({
|
||||||
id: createTabId(),
|
id: createTabId(),
|
||||||
title: extras.title || 'Farm Control',
|
title: extras.title || 'Farm Control',
|
||||||
modelName: extras.modelName || null,
|
modelName: extras.modelName || null,
|
||||||
iconKey: extras.iconKey || null,
|
iconKey: extras.iconKey || null,
|
||||||
history: [locationToEntry(location)],
|
history: [locationToEntry(location)],
|
||||||
historyIndex: 0
|
historyIndex: 0,
|
||||||
|
listState: cloneListState(extras.listState)
|
||||||
})
|
})
|
||||||
|
|
||||||
const cloneCurrentPageTab = (tab, location) => ({
|
const cloneCurrentPageTab = (tab, location) => ({
|
||||||
@ -98,7 +215,8 @@ const cloneCurrentPageTab = (tab, location) => ({
|
|||||||
modelName: tab?.modelName || null,
|
modelName: tab?.modelName || null,
|
||||||
iconKey: tab?.iconKey || null,
|
iconKey: tab?.iconKey || null,
|
||||||
history: [locationToEntry(location)],
|
history: [locationToEntry(location)],
|
||||||
historyIndex: 0
|
historyIndex: 0,
|
||||||
|
listState: cloneListState(tab?.listState)
|
||||||
})
|
})
|
||||||
|
|
||||||
const getTabCurrentEntry = (tab) =>
|
const getTabCurrentEntry = (tab) =>
|
||||||
@ -117,13 +235,18 @@ const normalizeTab = (tab, fallbackLocation) => {
|
|||||||
const rawIndex = Number.isInteger(tab?.historyIndex)
|
const rawIndex = Number.isInteger(tab?.historyIndex)
|
||||||
? tab.historyIndex
|
? tab.historyIndex
|
||||||
: history.length - 1
|
: history.length - 1
|
||||||
return {
|
const normalized = {
|
||||||
id: tab?.id || createTabId(),
|
id: tab?.id || createTabId(),
|
||||||
title: tab?.title || 'Farm Control',
|
title: tab?.title || 'Farm Control',
|
||||||
modelName: tab?.modelName || null,
|
modelName: tab?.modelName || null,
|
||||||
iconKey: tab?.iconKey || null,
|
iconKey: tab?.iconKey || null,
|
||||||
history,
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,6 +257,13 @@ const applyLocationToTab = (tab, entry) => {
|
|||||||
if (!tab.history?.length) {
|
if (!tab.history?.length) {
|
||||||
return { ...tab, history: [entry], historyIndex: 0 }
|
return { ...tab, history: [entry], historyIndex: 0 }
|
||||||
}
|
}
|
||||||
|
const currentPath = currentEntry?.pathname || '/'
|
||||||
|
const nextPath = entry.pathname || '/'
|
||||||
|
if (currentPath === nextPath) {
|
||||||
|
const history = [...tab.history]
|
||||||
|
history[tab.historyIndex] = entry
|
||||||
|
return { ...tab, history }
|
||||||
|
}
|
||||||
const truncated = tab.history.slice(0, tab.historyIndex + 1)
|
const truncated = tab.history.slice(0, tab.historyIndex + 1)
|
||||||
return {
|
return {
|
||||||
...tab,
|
...tab,
|
||||||
@ -205,19 +335,38 @@ export const NavigationTabsProvider = ({ children }) => {
|
|||||||
const previewThemeRef = useRef(isDarkMode ? 'dark' : 'light')
|
const previewThemeRef = useRef(isDarkMode ? 'dark' : 'light')
|
||||||
const tabStripRootRef = useRef(null)
|
const tabStripRootRef = useRef(null)
|
||||||
const scrollTabStripToEndIntervalRef = 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
|
tabsRef.current = tabs
|
||||||
activeTabIdRef.current = activeTabId
|
activeTabIdRef.current = activeTabId
|
||||||
locationRef.current = location
|
locationRef.current = location
|
||||||
previewThemeRef.current = isDarkMode ? 'dark' : 'light'
|
previewThemeRef.current = isDarkMode ? 'dark' : 'light'
|
||||||
|
|
||||||
const isRestoreLocation = useCallback((entry) => {
|
const markForeignLocation = useCallback((previousTabId) => {
|
||||||
const restoring = isRestoringRef.current
|
if (!previousTabId) {
|
||||||
if (!restoring) return false
|
foreignLocationRef.current = null
|
||||||
if (restoring === true) return true
|
return
|
||||||
return (
|
}
|
||||||
entriesEqual(restoring.target, entry) ||
|
foreignLocationRef.current = {
|
||||||
entriesEqual(restoring.from, entry)
|
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) => {
|
const shouldIgnoreRestoredLocation = useCallback((entry) => {
|
||||||
@ -239,11 +388,15 @@ export const NavigationTabsProvider = ({ children }) => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const restoreToEntry = useCallback(
|
const restoreToEntry = useCallback(
|
||||||
(entry) => {
|
(entry, { previousTabId } = {}) => {
|
||||||
if (!entry) return
|
if (!entry) return
|
||||||
isRestoringRef.current = {
|
isRestoringRef.current = {
|
||||||
target: locationToEntry(entry),
|
target: locationToEntry(entry),
|
||||||
from: locationToEntry(locationRef.current)
|
from: locationToEntry(locationRef.current),
|
||||||
|
previousTabId:
|
||||||
|
previousTabId === undefined
|
||||||
|
? activeTabIdRef.current
|
||||||
|
: previousTabId
|
||||||
}
|
}
|
||||||
navigate(entryToPath(entry), { replace: true })
|
navigate(entryToPath(entry), { replace: true })
|
||||||
},
|
},
|
||||||
@ -256,16 +409,19 @@ export const NavigationTabsProvider = ({ children }) => {
|
|||||||
const nextTab = tabsRef.current.find((tab) => tab.id === tabId)
|
const nextTab = tabsRef.current.find((tab) => tab.id === tabId)
|
||||||
if (!nextTab) return
|
if (!nextTab) return
|
||||||
|
|
||||||
|
const previousTabId = activeTabIdRef.current
|
||||||
const nextEntry = getTabCurrentEntry(nextTab)
|
const nextEntry = getTabCurrentEntry(nextTab)
|
||||||
|
markForeignLocation(previousTabId)
|
||||||
setActiveTabId(tabId)
|
setActiveTabId(tabId)
|
||||||
|
activeTabIdRef.current = tabId
|
||||||
|
|
||||||
if (entriesEqual(nextEntry, locationRef.current)) {
|
if (entriesEqual(nextEntry, locationRef.current)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreToEntry(nextEntry)
|
restoreToEntry(nextEntry, { previousTabId })
|
||||||
},
|
},
|
||||||
[restoreToEntry]
|
[markForeignLocation, restoreToEntry]
|
||||||
)
|
)
|
||||||
|
|
||||||
const registerTabStripRoot = useCallback((element) => {
|
const registerTabStripRoot = useCallback((element) => {
|
||||||
@ -325,17 +481,19 @@ export const NavigationTabsProvider = ({ children }) => {
|
|||||||
? createTabFromLocation(entry)
|
? createTabFromLocation(entry)
|
||||||
: cloneCurrentPageTab(currentTab, location)
|
: cloneCurrentPageTab(currentTab, location)
|
||||||
|
|
||||||
|
const previousTabId = activeTabIdRef.current
|
||||||
setTabs((current) => [...current, nextTab])
|
setTabs((current) => [...current, nextTab])
|
||||||
|
markForeignLocation(previousTabId)
|
||||||
setActiveTabId(nextTab.id)
|
setActiveTabId(nextTab.id)
|
||||||
activeTabIdRef.current = nextTab.id
|
activeTabIdRef.current = nextTab.id
|
||||||
|
|
||||||
if (targetUrl && !entriesEqual(entry, locationRef.current)) {
|
if (targetUrl && !entriesEqual(entry, locationRef.current)) {
|
||||||
restoreToEntry(entry)
|
restoreToEntry(entry, { previousTabId })
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollTabStripToEnd()
|
scrollTabStripToEnd()
|
||||||
},
|
},
|
||||||
[location, restoreToEntry, scrollTabStripToEnd]
|
[location, markForeignLocation, restoreToEntry, scrollTabStripToEnd]
|
||||||
)
|
)
|
||||||
|
|
||||||
const removeTab = useCallback(
|
const removeTab = useCallback(
|
||||||
@ -371,15 +529,18 @@ export const NavigationTabsProvider = ({ children }) => {
|
|||||||
return nextCapturing
|
return nextCapturing
|
||||||
})
|
})
|
||||||
if (nextActive) {
|
if (nextActive) {
|
||||||
|
const previousTabId = activeTabIdRef.current
|
||||||
|
markForeignLocation(previousTabId)
|
||||||
setActiveTabId(nextActive.id)
|
setActiveTabId(nextActive.id)
|
||||||
|
activeTabIdRef.current = nextActive.id
|
||||||
const nextEntry = getTabCurrentEntry(nextActive)
|
const nextEntry = getTabCurrentEntry(nextActive)
|
||||||
if (!entriesEqual(nextEntry, locationRef.current)) {
|
if (!entriesEqual(nextEntry, locationRef.current)) {
|
||||||
restoreToEntry(nextEntry)
|
restoreToEntry(nextEntry, { previousTabId })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
[handleWindowControl, restoreToEntry]
|
[handleWindowControl, markForeignLocation, restoreToEntry]
|
||||||
)
|
)
|
||||||
|
|
||||||
const closeTab = useCallback(
|
const closeTab = useCallback(
|
||||||
@ -409,13 +570,16 @@ export const NavigationTabsProvider = ({ children }) => {
|
|||||||
next.splice(insertBefore ? targetIndex : targetIndex + 1, 0, tab)
|
next.splice(insertBefore ? targetIndex : targetIndex + 1, 0, tab)
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
|
const previousTabId = activeTabIdRef.current
|
||||||
|
markForeignLocation(previousTabId)
|
||||||
setActiveTabId(tab.id)
|
setActiveTabId(tab.id)
|
||||||
|
activeTabIdRef.current = tab.id
|
||||||
const nextEntry = getTabCurrentEntry(tab)
|
const nextEntry = getTabCurrentEntry(tab)
|
||||||
if (!entriesEqual(nextEntry, locationRef.current)) {
|
if (!entriesEqual(nextEntry, locationRef.current)) {
|
||||||
restoreToEntry(nextEntry)
|
restoreToEntry(nextEntry, { previousTabId })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[restoreToEntry]
|
[markForeignLocation, restoreToEntry]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleTabDragStart = useCallback(
|
const handleTabDragStart = useCallback(
|
||||||
@ -549,31 +713,45 @@ export const NavigationTabsProvider = ({ children }) => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const setTabPage = useCallback(
|
const setTabPage = useCallback(
|
||||||
({ title, modelName, iconKey, location: pageLocation } = {}) => {
|
({ title, modelName, iconKey } = {}) => {
|
||||||
const activeId = activeTabIdRef.current
|
const activeId = activeTabIdRef.current
|
||||||
if (!activeId) return
|
if (!activeId) return
|
||||||
|
|
||||||
const pageEntry = pageLocation ? locationToEntry(pageLocation) : null
|
|
||||||
|
|
||||||
setTabs((current) => {
|
setTabs((current) => {
|
||||||
if (current.length === 0) return current
|
if (current.length === 0) return current
|
||||||
|
|
||||||
let changed = false
|
let changed = false
|
||||||
const next = current.map((tab) => {
|
const next = current.map((tab) => {
|
||||||
if (tab.id !== activeId) return tab
|
if (tab.id !== activeId) return tab
|
||||||
let updated = applyPageMetaToTab(tab, { title, modelName, iconKey })
|
const updated = applyPageMetaToTab(tab, { title, modelName, iconKey })
|
||||||
if (pageEntry && !isRestoreLocation(pageEntry)) {
|
|
||||||
updated = applyLocationToTab(updated, pageEntry)
|
|
||||||
}
|
|
||||||
if (updated !== tab) changed = true
|
if (updated !== tab) changed = true
|
||||||
return updated
|
return updated
|
||||||
})
|
})
|
||||||
return changed ? next : current
|
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 goBack = useCallback(() => {
|
||||||
const currentTab = tabsRef.current.find(
|
const currentTab = tabsRef.current.find(
|
||||||
(tab) => tab.id === activeTabIdRef.current
|
(tab) => tab.id === activeTabIdRef.current
|
||||||
@ -676,8 +854,58 @@ export const NavigationTabsProvider = ({ children }) => {
|
|||||||
if (!isElectron || !hydrated) return
|
if (!isElectron || !hydrated) return
|
||||||
|
|
||||||
const entry = locationToEntry(location)
|
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
|
||||||
|
}
|
||||||
|
// Same-path query cleanup on the tab we just opened (action params, etc.)
|
||||||
|
if (entry.pathname === restoring.target?.pathname) {
|
||||||
|
isRestoringRef.current = null
|
||||||
|
} else if (restoring.previousTabId) {
|
||||||
|
// A delayed write from the previous tab (viewId, filters, etc.)
|
||||||
|
applyEntryToTabId(restoring.previousTabId, entry)
|
||||||
|
if (foreign) foreign.entry = entry
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldIgnoreRestoredLocation(entry)) return
|
if (shouldIgnoreRestoredLocation(entry)) return
|
||||||
|
|
||||||
|
if (
|
||||||
|
foreign &&
|
||||||
|
foreign.tabId &&
|
||||||
|
foreign.tabId !== activeTabIdRef.current &&
|
||||||
|
Date.now() < foreign.expiresAt
|
||||||
|
) {
|
||||||
|
if (entriesEqual(entry, foreign.entry)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const activeEntry = getTabCurrentEntry(
|
||||||
|
tabsRef.current.find((tab) => tab.id === activeTabIdRef.current)
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
entry.pathname === foreign.entry.pathname &&
|
||||||
|
entry.pathname !== activeEntry?.pathname
|
||||||
|
) {
|
||||||
|
applyEntryToTabId(foreign.tabId, entry)
|
||||||
|
foreign.entry = entry
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foreign && Date.now() >= foreign.expiresAt) {
|
||||||
|
foreignLocationRef.current = null
|
||||||
|
}
|
||||||
|
|
||||||
setTabs((current) => {
|
setTabs((current) => {
|
||||||
if (current.length === 0) {
|
if (current.length === 0) {
|
||||||
const initialTab = createTabFromLocation(location)
|
const initialTab = createTabFromLocation(location)
|
||||||
@ -694,7 +922,13 @@ export const NavigationTabsProvider = ({ children }) => {
|
|||||||
})
|
})
|
||||||
return changed ? next : current
|
return changed ? next : current
|
||||||
})
|
})
|
||||||
}, [hydrated, isElectron, location, shouldIgnoreRestoredLocation])
|
}, [
|
||||||
|
applyEntryToTabId,
|
||||||
|
hydrated,
|
||||||
|
isElectron,
|
||||||
|
location,
|
||||||
|
shouldIgnoreRestoredLocation
|
||||||
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isElectron || !hydrated || !syncWindowTabs) return undefined
|
if (!isElectron || !hydrated || !syncWindowTabs) return undefined
|
||||||
@ -737,6 +971,59 @@ export const NavigationTabsProvider = ({ children }) => {
|
|||||||
})
|
})
|
||||||
}, [isElectron, onTabMovedAway, removeTab])
|
}, [isElectron, onTabMovedAway, removeTab])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isElectron) return undefined
|
||||||
|
|
||||||
|
const digitIndexFromEvent = (event) => {
|
||||||
|
const code = event.code || ''
|
||||||
|
if (code.startsWith('Digit')) {
|
||||||
|
const digit = Number(code.slice(5))
|
||||||
|
if (digit >= 1 && digit <= 9) return digit - 1
|
||||||
|
if (digit === 0) return 9
|
||||||
|
}
|
||||||
|
const key = event.key
|
||||||
|
if (key >= '1' && key <= '9') return Number(key) - 1
|
||||||
|
if (key === '0') return 9
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectTabAtIndex = (index) => {
|
||||||
|
const nextTab = tabsRef.current[index]
|
||||||
|
if (!nextTab) return
|
||||||
|
selectTab(nextTab.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cycleTab = (delta) => {
|
||||||
|
const current = tabsRef.current
|
||||||
|
if (current.length < 2) return
|
||||||
|
const activeIndex = current.findIndex(
|
||||||
|
(tab) => tab.id === activeTabIdRef.current
|
||||||
|
)
|
||||||
|
const fromIndex = activeIndex === -1 ? 0 : activeIndex
|
||||||
|
const nextIndex = (fromIndex + delta + current.length) % current.length
|
||||||
|
selectTab(current[nextIndex].id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (event) => {
|
||||||
|
if (event.ctrlKey && event.key === 'Tab') {
|
||||||
|
event.preventDefault()
|
||||||
|
cycleTab(event.shiftKey ? -1 : 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(event.metaKey || event.ctrlKey) || event.repeat) return
|
||||||
|
const index = digitIndexFromEvent(event)
|
||||||
|
if (index == null) return
|
||||||
|
event.preventDefault()
|
||||||
|
selectTabAtIndex(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown, true)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown, true)
|
||||||
|
}
|
||||||
|
}, [isElectron, selectTab])
|
||||||
|
|
||||||
const activeTab = useMemo(
|
const activeTab = useMemo(
|
||||||
() => tabs.find((tab) => tab.id === activeTabId) || null,
|
() => tabs.find((tab) => tab.id === activeTabId) || null,
|
||||||
[activeTabId, tabs]
|
[activeTabId, tabs]
|
||||||
@ -768,7 +1055,9 @@ export const NavigationTabsProvider = ({ children }) => {
|
|||||||
createNewWindow,
|
createNewWindow,
|
||||||
registerTabPane,
|
registerTabPane,
|
||||||
registerTabStripRoot,
|
registerTabStripRoot,
|
||||||
captureTabPreview
|
captureTabPreview,
|
||||||
|
getTabListState,
|
||||||
|
setTabListState
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
activeTab,
|
activeTab,
|
||||||
@ -792,6 +1081,8 @@ export const NavigationTabsProvider = ({ children }) => {
|
|||||||
reorderTabs,
|
reorderTabs,
|
||||||
selectTab,
|
selectTab,
|
||||||
setTabPage,
|
setTabPage,
|
||||||
|
getTabListState,
|
||||||
|
setTabListState,
|
||||||
tabs
|
tabs
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@ -850,19 +1141,18 @@ export const useTabPreview = (tabId) => {
|
|||||||
export const useNavigationTabPage = ({ title, modelName, iconKey } = {}) => {
|
export const useNavigationTabPage = ({ title, modelName, iconKey } = {}) => {
|
||||||
const { setTabPage, isElectron } = useContext(NavigationTabMetaContext)
|
const { setTabPage, isElectron } = useContext(NavigationTabMetaContext)
|
||||||
const store = useContext(NavigationTabActiveContext)
|
const store = useContext(NavigationTabActiveContext)
|
||||||
const pageLocation = useLocation()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isElectron || !title) return undefined
|
if (!isElectron || !title) return undefined
|
||||||
|
|
||||||
const sync = () => {
|
const sync = () => {
|
||||||
if (!store.getSnapshot()) return
|
if (!store.getSnapshot()) return
|
||||||
setTabPage({ title, modelName, iconKey, location: pageLocation })
|
setTabPage({ title, modelName, iconKey })
|
||||||
}
|
}
|
||||||
|
|
||||||
sync()
|
sync()
|
||||||
return store.subscribe(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
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
|
|||||||
@ -15,7 +15,10 @@ import { useTableState } from './TableStateContext'
|
|||||||
import useViewMode from '../hooks/useViewMode'
|
import useViewMode from '../hooks/useViewMode'
|
||||||
import { DEFAULT_VIEW_MODE, normalizeViewMode } from '../common/viewModeUtils'
|
import { DEFAULT_VIEW_MODE, normalizeViewMode } from '../common/viewModeUtils'
|
||||||
import { getModelByName } from '../../../database/ObjectModels'
|
import { getModelByName } from '../../../database/ObjectModels'
|
||||||
import { useNavigationTabPage } from './NavigationTabsContext'
|
import {
|
||||||
|
useNavigationTabPage,
|
||||||
|
useNavigationTabs
|
||||||
|
} from './NavigationTabsContext'
|
||||||
|
|
||||||
const ObjectListViewContext = createContext()
|
const ObjectListViewContext = createContext()
|
||||||
|
|
||||||
@ -128,14 +131,10 @@ export const ObjectListViewProvider = ({ children, objectType, tableRef }) => {
|
|||||||
subscribeToObjectUpdates
|
subscribeToObjectUpdates
|
||||||
} = useContext(ApiServerContext)
|
} = useContext(ApiServerContext)
|
||||||
const { token, authInitialized } = useContext(AuthContext)
|
const { token, authInitialized } = useContext(AuthContext)
|
||||||
const { getViewFromUrl, persistView, setObjectListView } = useTableState()
|
const { getViewFromUrl, getPersistedView, persistView, setObjectListView } =
|
||||||
|
useTableState()
|
||||||
|
const { isElectron } = useNavigationTabs()
|
||||||
const listModel = getModelByName(objectType)
|
const listModel = getModelByName(objectType)
|
||||||
useNavigationTabPage({
|
|
||||||
title: listModel?.labelPlural
|
|
||||||
? `List - ${listModel.labelPlural}`
|
|
||||||
: 'List',
|
|
||||||
modelName: objectType
|
|
||||||
})
|
|
||||||
|
|
||||||
const [views, setViews] = useState(() => readCachedViews(objectType) || [])
|
const [views, setViews] = useState(() => readCachedViews(objectType) || [])
|
||||||
const [draftViews, setDraftViews] = useState(() => {
|
const [draftViews, setDraftViews] = useState(() => {
|
||||||
@ -203,6 +202,13 @@ export const ObjectListViewProvider = ({ children, objectType, tableRef }) => {
|
|||||||
return view
|
return view
|
||||||
}, [activeTabKey, displayedViews])
|
}, [activeTabKey, displayedViews])
|
||||||
|
|
||||||
|
const listLabel = listModel?.labelPlural || 'List'
|
||||||
|
const viewName = activeView?.name
|
||||||
|
useNavigationTabPage({
|
||||||
|
title: viewName ? `${listLabel} - ${viewName}` : listLabel,
|
||||||
|
modelName: objectType
|
||||||
|
})
|
||||||
|
|
||||||
viewsStateRef.current = { views, draftViews, isEditing }
|
viewsStateRef.current = { views, draftViews, isEditing }
|
||||||
activeViewRef.current = activeView
|
activeViewRef.current = activeView
|
||||||
|
|
||||||
@ -297,11 +303,13 @@ export const ObjectListViewProvider = ({ children, objectType, tableRef }) => {
|
|||||||
const name =
|
const name =
|
||||||
key && view && typeof view === 'object' ? view.name || null : null
|
key && view && typeof view === 'object' ? view.name || null : null
|
||||||
|
|
||||||
persistView(key)
|
persistView(key, { scope: objectType, name })
|
||||||
writeLastViewId(objectType, key)
|
if (!isElectron) {
|
||||||
|
writeLastViewId(objectType, key)
|
||||||
|
}
|
||||||
setObjectListView(objectType, key ? { id: key, name } : null)
|
setObjectListView(objectType, key ? { id: key, name } : null)
|
||||||
},
|
},
|
||||||
[objectType, persistView, setObjectListView]
|
[isElectron, objectType, persistView, setObjectListView]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
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
|
// Allow early restore from cache; only block when we have nothing to match against yet
|
||||||
if (initialLoading && views.length === 0) return
|
if (initialLoading && views.length === 0) return
|
||||||
|
|
||||||
const urlViewId = getViewFromUrl()
|
const persistedViewId = getPersistedView(objectType)?.id || null
|
||||||
const rememberedViewId = readLastViewId(objectType)
|
const urlViewId = isElectron ? null : getViewFromUrl()
|
||||||
const candidateViewId = urlViewId || rememberedViewId
|
const rememberedViewId = isElectron ? null : readLastViewId(objectType)
|
||||||
|
const candidateViewId = persistedViewId || urlViewId || rememberedViewId
|
||||||
|
|
||||||
if (!candidateViewId) {
|
if (!candidateViewId) {
|
||||||
urlSyncedRef.current = true
|
urlSyncedRef.current = true
|
||||||
@ -370,14 +379,18 @@ export const ObjectListViewProvider = ({ children, objectType, tableRef }) => {
|
|||||||
if (matched) {
|
if (matched) {
|
||||||
const matchedId = String(matched._id)
|
const matchedId = String(matched._id)
|
||||||
setActiveTabKey(matchedId)
|
setActiveTabKey(matchedId)
|
||||||
writeLastViewId(objectType, matchedId)
|
if (!isElectron) {
|
||||||
|
writeLastViewId(objectType, matchedId)
|
||||||
|
}
|
||||||
setObjectListView(objectType, {
|
setObjectListView(objectType, {
|
||||||
id: matchedId,
|
id: matchedId,
|
||||||
name: matched.name || null
|
name: matched.name || null
|
||||||
})
|
})
|
||||||
if (!matched._isDraft) {
|
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
|
urlSyncedRef.current = true
|
||||||
setViewSyncReady(true)
|
setViewSyncReady(true)
|
||||||
@ -400,6 +413,8 @@ export const ObjectListViewProvider = ({ children, objectType, tableRef }) => {
|
|||||||
draftViews,
|
draftViews,
|
||||||
isEditing,
|
isEditing,
|
||||||
objectType,
|
objectType,
|
||||||
|
isElectron,
|
||||||
|
getPersistedView,
|
||||||
getViewFromUrl,
|
getViewFromUrl,
|
||||||
persistView,
|
persistView,
|
||||||
rememberActiveView,
|
rememberActiveView,
|
||||||
@ -903,36 +918,33 @@ export const ObjectListViewProvider = ({ children, objectType, tableRef }) => {
|
|||||||
// route filter/sort changes into the draft instead of URL/session persistence.
|
// route filter/sort changes into the draft instead of URL/session persistence.
|
||||||
const activeObjectView = isEditing ? activeView : null
|
const activeObjectView = isEditing ? activeView : null
|
||||||
|
|
||||||
const handleViewModeChange = useCallback(
|
const handleViewModeChange = useCallback(async (viewMode) => {
|
||||||
async (viewMode) => {
|
const view = activeViewRef.current
|
||||||
const view = activeViewRef.current
|
if (!view || activeTabKeyRef.current === ALL_TAB_KEY) return
|
||||||
if (!view || activeTabKeyRef.current === ALL_TAB_KEY) return
|
|
||||||
|
|
||||||
if (isEditingRef.current) {
|
if (isEditingRef.current) {
|
||||||
const updateInList = (list) =>
|
const updateInList = (list) =>
|
||||||
list.map((item) =>
|
list.map((item) =>
|
||||||
String(item._id) === String(view._id) ? { ...item, viewMode } : item
|
String(item._id) === String(view._id) ? { ...item, viewMode } : item
|
||||||
)
|
)
|
||||||
|
|
||||||
activeViewRef.current = { ...view, viewMode }
|
activeViewRef.current = { ...view, viewMode }
|
||||||
|
|
||||||
setDraftViews((prev) => updateInList(prev))
|
setDraftViews((prev) => updateInList(prev))
|
||||||
viewsStateRef.current = {
|
viewsStateRef.current = {
|
||||||
...viewsStateRef.current,
|
...viewsStateRef.current,
|
||||||
draftViews: updateInList(viewsStateRef.current.draftViews)
|
draftViews: updateInList(viewsStateRef.current.draftViews)
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Not editing: store as a temporary user override instead of saving
|
// Not editing: store as a temporary user override instead of saving
|
||||||
setUserViewModeOverride(viewMode)
|
setUserViewModeOverride(viewMode)
|
||||||
userOverridesRef.current = {
|
userOverridesRef.current = {
|
||||||
...(userOverridesRef.current || {}),
|
...(userOverridesRef.current || {}),
|
||||||
viewMode
|
viewMode
|
||||||
}
|
}
|
||||||
},
|
}, [])
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
const isCustomView = activeTabKey !== ALL_TAB_KEY
|
const isCustomView = activeTabKey !== ALL_TAB_KEY
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,10 @@ import {
|
|||||||
} from 'react'
|
} from 'react'
|
||||||
import { useSearchParams } from 'react-router-dom'
|
import { useSearchParams } from 'react-router-dom'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
import {
|
||||||
|
NavigationTabIdContext,
|
||||||
|
useNavigationTabs
|
||||||
|
} from './NavigationTabsContext'
|
||||||
|
|
||||||
const TableStateContext = createContext(null)
|
const TableStateContext = createContext(null)
|
||||||
|
|
||||||
@ -166,6 +170,9 @@ const normalizeSorter = (sorter) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const TableStateProvider = ({ children }) => {
|
export const TableStateProvider = ({ children }) => {
|
||||||
|
const tabId = useContext(NavigationTabIdContext)
|
||||||
|
const { isElectron, getTabListState, setTabListState, activeTab } =
|
||||||
|
useNavigationTabs()
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
const [pageFilters, setPageFilters] = useState({})
|
const [pageFilters, setPageFilters] = useState({})
|
||||||
const [pageSorters, setPageSorters] = useState({})
|
const [pageSorters, setPageSorters] = useState({})
|
||||||
@ -214,8 +221,17 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
[setSearchParams]
|
[setSearchParams]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const resolveScopedTabId = useCallback(
|
||||||
|
() => tabId || (isElectron ? activeTab?.id : null),
|
||||||
|
[activeTab?.id, isElectron, tabId]
|
||||||
|
)
|
||||||
|
|
||||||
const getPersistedFilter = useCallback(
|
const getPersistedFilter = useCallback(
|
||||||
(scope, { useFilterInUrl = false, useFilterInSession = false } = {}) => {
|
(scope, { useFilterInUrl = false, useFilterInSession = false } = {}) => {
|
||||||
|
const scopedTabId = resolveScopedTabId()
|
||||||
|
if (isElectron && scopedTabId) {
|
||||||
|
return getTabListState(scopedTabId, scope).filter || {}
|
||||||
|
}
|
||||||
if (useFilterInUrl) {
|
if (useFilterInUrl) {
|
||||||
const fromUrl = readFilterFromUrl(searchParams)
|
const fromUrl = readFilterFromUrl(searchParams)
|
||||||
if (fromUrl) return fromUrl
|
if (fromUrl) return fromUrl
|
||||||
@ -226,11 +242,15 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
[searchParams]
|
[getTabListState, isElectron, resolveScopedTabId, searchParams]
|
||||||
)
|
)
|
||||||
|
|
||||||
const getPersistedSorter = useCallback(
|
const getPersistedSorter = useCallback(
|
||||||
(scope, { useSortInUrl = false, useSortInSession = false } = {}) => {
|
(scope, { useSortInUrl = false, useSortInSession = false } = {}) => {
|
||||||
|
const scopedTabId = resolveScopedTabId()
|
||||||
|
if (isElectron && scopedTabId) {
|
||||||
|
return getTabListState(scopedTabId, scope).sort || {}
|
||||||
|
}
|
||||||
if (useSortInUrl) {
|
if (useSortInUrl) {
|
||||||
const fromUrl = readSortFromUrl(searchParams)
|
const fromUrl = readSortFromUrl(searchParams)
|
||||||
if (fromUrl) return fromUrl
|
if (fromUrl) return fromUrl
|
||||||
@ -241,7 +261,7 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
[searchParams]
|
[getTabListState, isElectron, resolveScopedTabId, searchParams]
|
||||||
)
|
)
|
||||||
|
|
||||||
const getViewFromUrl = useCallback(
|
const getViewFromUrl = useCallback(
|
||||||
@ -249,8 +269,36 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
[searchParams]
|
[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(
|
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) => {
|
updateSearchParams((next) => {
|
||||||
// Drop legacy `view` param that previously stored `_reference`
|
// Drop legacy `view` param that previously stored `_reference`
|
||||||
next.delete('view')
|
next.delete('view')
|
||||||
@ -266,7 +314,7 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[updateSearchParams]
|
[isElectron, resolveScopedTabId, setTabListState, updateSearchParams]
|
||||||
)
|
)
|
||||||
|
|
||||||
const persistFilter = useCallback(
|
const persistFilter = useCallback(
|
||||||
@ -276,6 +324,13 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
{ saveFilterInSession = false, saveFilterInUrl = false } = {}
|
{ saveFilterInSession = false, saveFilterInUrl = false } = {}
|
||||||
) => {
|
) => {
|
||||||
const active = getActiveFilterValues(filterState)
|
const active = getActiveFilterValues(filterState)
|
||||||
|
const scopedTabId = resolveScopedTabId()
|
||||||
|
if (isElectron && scopedTabId) {
|
||||||
|
if (saveFilterInSession || saveFilterInUrl) {
|
||||||
|
setTabListState(scopedTabId, scope, { filter: active })
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
if (saveFilterInSession) {
|
if (saveFilterInSession) {
|
||||||
writeFilterToSession(scope, active)
|
writeFilterToSession(scope, active)
|
||||||
}
|
}
|
||||||
@ -289,7 +344,7 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[updateSearchParams]
|
[isElectron, resolveScopedTabId, setTabListState, updateSearchParams]
|
||||||
)
|
)
|
||||||
|
|
||||||
const persistSort = useCallback(
|
const persistSort = useCallback(
|
||||||
@ -299,6 +354,13 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
{ saveSortInSession = false, saveSortInUrl = false } = {}
|
{ saveSortInSession = false, saveSortInUrl = false } = {}
|
||||||
) => {
|
) => {
|
||||||
const nextSorter = normalizeSorter(sorter)
|
const nextSorter = normalizeSorter(sorter)
|
||||||
|
const scopedTabId = resolveScopedTabId()
|
||||||
|
if (isElectron && scopedTabId) {
|
||||||
|
if (saveSortInSession || saveSortInUrl) {
|
||||||
|
setTabListState(scopedTabId, scope, { sort: nextSorter })
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
if (saveSortInSession) {
|
if (saveSortInSession) {
|
||||||
writeSortToSession(scope, nextSorter)
|
writeSortToSession(scope, nextSorter)
|
||||||
}
|
}
|
||||||
@ -312,7 +374,7 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[updateSearchParams]
|
[isElectron, resolveScopedTabId, setTabListState, updateSearchParams]
|
||||||
)
|
)
|
||||||
|
|
||||||
const persistTableState = useCallback(
|
const persistTableState = useCallback(
|
||||||
@ -329,6 +391,21 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
) => {
|
) => {
|
||||||
const active = getActiveFilterValues(filterState)
|
const active = getActiveFilterValues(filterState)
|
||||||
const nextSorter = normalizeSorter(sorter)
|
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) {
|
if (saveFilterInSession) {
|
||||||
writeFilterToSession(scope, active)
|
writeFilterToSession(scope, active)
|
||||||
@ -356,7 +433,7 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[updateSearchParams]
|
[isElectron, resolveScopedTabId, setTabListState, updateSearchParams]
|
||||||
)
|
)
|
||||||
|
|
||||||
const setPageFilter = useCallback((path, filter) => {
|
const setPageFilter = useCallback((path, filter) => {
|
||||||
@ -389,7 +466,12 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
const setObjectListFilter = useCallback((objectType, filter) => {
|
const setObjectListFilter = useCallback((objectType, filter) => {
|
||||||
if (!objectType) return
|
if (!objectType) return
|
||||||
const active = getActiveFilterValues(filter)
|
const active = getActiveFilterValues(filter)
|
||||||
writeListFilterToSession(objectType, active)
|
const scopedTabId = resolveScopedTabId()
|
||||||
|
if (isElectron && scopedTabId) {
|
||||||
|
setTabListState(scopedTabId, objectType, { listFilter: active })
|
||||||
|
} else {
|
||||||
|
writeListFilterToSession(objectType, active)
|
||||||
|
}
|
||||||
setObjectListFilters((prev) => {
|
setObjectListFilters((prev) => {
|
||||||
const hasActive = Object.keys(active).length > 0
|
const hasActive = Object.keys(active).length > 0
|
||||||
if (!hasActive) {
|
if (!hasActive) {
|
||||||
@ -400,12 +482,17 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
return { ...prev, [objectType]: active }
|
return { ...prev, [objectType]: active }
|
||||||
})
|
})
|
||||||
}, [])
|
}, [isElectron, resolveScopedTabId, setTabListState])
|
||||||
|
|
||||||
const setObjectListSorter = useCallback((objectType, sorter) => {
|
const setObjectListSorter = useCallback((objectType, sorter) => {
|
||||||
if (!objectType) return
|
if (!objectType) return
|
||||||
const nextSorter = normalizeSorter(sorter)
|
const nextSorter = normalizeSorter(sorter)
|
||||||
writeListSortToSession(objectType, nextSorter)
|
const scopedTabId = resolveScopedTabId()
|
||||||
|
if (isElectron && scopedTabId) {
|
||||||
|
setTabListState(scopedTabId, objectType, { listSort: nextSorter })
|
||||||
|
} else {
|
||||||
|
writeListSortToSession(objectType, nextSorter)
|
||||||
|
}
|
||||||
setObjectListSorters((prev) => {
|
setObjectListSorters((prev) => {
|
||||||
const hasSort = nextSorter?.field && nextSorter?.order
|
const hasSort = nextSorter?.field && nextSorter?.order
|
||||||
if (!hasSort) {
|
if (!hasSort) {
|
||||||
@ -416,7 +503,7 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
return { ...prev, [objectType]: nextSorter }
|
return { ...prev, [objectType]: nextSorter }
|
||||||
})
|
})
|
||||||
}, [])
|
}, [isElectron, resolveScopedTabId, setTabListState])
|
||||||
|
|
||||||
const setObjectListView = useCallback((objectType, view) => {
|
const setObjectListView = useCallback((objectType, view) => {
|
||||||
if (!objectType) return
|
if (!objectType) return
|
||||||
@ -424,7 +511,15 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
view?.id != null
|
view?.id != null
|
||||||
? { id: String(view.id), name: view.name || null }
|
? { id: String(view.id), name: view.name || null }
|
||||||
: 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) => {
|
setObjectListViews((prev) => {
|
||||||
if (!nextView) {
|
if (!nextView) {
|
||||||
if (!(objectType in prev)) return prev
|
if (!(objectType in prev)) return prev
|
||||||
@ -442,7 +537,7 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
return { ...prev, [objectType]: nextView }
|
return { ...prev, [objectType]: nextView }
|
||||||
})
|
})
|
||||||
}, [])
|
}, [isElectron, resolveScopedTabId, setTabListState])
|
||||||
|
|
||||||
const getObjectListFilter = useCallback(
|
const getObjectListFilter = useCallback(
|
||||||
(objectType) => {
|
(objectType) => {
|
||||||
@ -450,9 +545,15 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
if (objectListFilters[objectType]) {
|
if (objectListFilters[objectType]) {
|
||||||
return objectListFilters[objectType]
|
return objectListFilters[objectType]
|
||||||
}
|
}
|
||||||
|
const scopedTabId = resolveScopedTabId()
|
||||||
|
if (isElectron && scopedTabId) {
|
||||||
|
return getActiveFilterValues(
|
||||||
|
getTabListState(scopedTabId, objectType).listFilter || {}
|
||||||
|
)
|
||||||
|
}
|
||||||
return getActiveFilterValues(readListFilterFromSession(objectType) || {})
|
return getActiveFilterValues(readListFilterFromSession(objectType) || {})
|
||||||
},
|
},
|
||||||
[objectListFilters]
|
[getTabListState, isElectron, objectListFilters, resolveScopedTabId]
|
||||||
)
|
)
|
||||||
|
|
||||||
const getObjectListSorter = useCallback(
|
const getObjectListSorter = useCallback(
|
||||||
@ -461,9 +562,13 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
if (objectListSorters[objectType]) {
|
if (objectListSorters[objectType]) {
|
||||||
return objectListSorters[objectType]
|
return objectListSorters[objectType]
|
||||||
}
|
}
|
||||||
|
const scopedTabId = resolveScopedTabId()
|
||||||
|
if (isElectron && scopedTabId) {
|
||||||
|
return getTabListState(scopedTabId, objectType).listSort || {}
|
||||||
|
}
|
||||||
return readListSortFromSession(objectType) || {}
|
return readListSortFromSession(objectType) || {}
|
||||||
},
|
},
|
||||||
[objectListSorters]
|
[getTabListState, isElectron, objectListSorters, resolveScopedTabId]
|
||||||
)
|
)
|
||||||
|
|
||||||
const getObjectListView = useCallback(
|
const getObjectListView = useCallback(
|
||||||
@ -472,9 +577,16 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
if (objectListViews[objectType]) {
|
if (objectListViews[objectType]) {
|
||||||
return 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)
|
return readListViewFromSession(objectType)
|
||||||
},
|
},
|
||||||
[objectListViews]
|
[getTabListState, isElectron, objectListViews, resolveScopedTabId]
|
||||||
)
|
)
|
||||||
|
|
||||||
const hasPageFilter = useCallback(
|
const hasPageFilter = useCallback(
|
||||||
@ -484,6 +596,17 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
|
|
||||||
const hasStoredFilter = useCallback((scope) => {
|
const hasStoredFilter = useCallback((scope) => {
|
||||||
if (!scope) return false
|
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
|
// 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
|
// already accounts for the view). Don't fall back to the All-tab session
|
||||||
// filter — that belongs to a different context.
|
// filter — that belongs to a different context.
|
||||||
@ -493,12 +616,19 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
if (hasStoredSessionFilter(scope)) return true
|
if (hasStoredSessionFilter(scope)) return true
|
||||||
if (Object.keys(objectListFilters[scope] || {}).length > 0) return true
|
if (Object.keys(objectListFilters[scope] || {}).length > 0) return true
|
||||||
return Object.keys(getActiveFilterValues(readListFilterFromSession(scope) || {})).length > 0
|
return Object.keys(getActiveFilterValues(readListFilterFromSession(scope) || {})).length > 0
|
||||||
}, [objectListFilters, objectListViews])
|
}, [
|
||||||
|
getTabListState,
|
||||||
|
isElectron,
|
||||||
|
objectListFilters,
|
||||||
|
objectListViews,
|
||||||
|
resolveScopedTabId
|
||||||
|
])
|
||||||
|
|
||||||
const value = useMemo(
|
const value = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
getPersistedFilter,
|
getPersistedFilter,
|
||||||
getPersistedSorter,
|
getPersistedSorter,
|
||||||
|
getPersistedView,
|
||||||
getViewFromUrl,
|
getViewFromUrl,
|
||||||
persistView,
|
persistView,
|
||||||
persistFilter,
|
persistFilter,
|
||||||
@ -523,6 +653,7 @@ export const TableStateProvider = ({ children }) => {
|
|||||||
[
|
[
|
||||||
getPersistedFilter,
|
getPersistedFilter,
|
||||||
getPersistedSorter,
|
getPersistedSorter,
|
||||||
|
getPersistedView,
|
||||||
getViewFromUrl,
|
getViewFromUrl,
|
||||||
persistView,
|
persistView,
|
||||||
persistFilter,
|
persistFilter,
|
||||||
|
|||||||
@ -5,9 +5,7 @@ import { AuthContext } from '../context/AuthContext'
|
|||||||
import { useNavigationTabPage } from '../context/NavigationTabsContext'
|
import { useNavigationTabPage } from '../context/NavigationTabsContext'
|
||||||
|
|
||||||
const formatPageLabel = (pageName) =>
|
const formatPageLabel = (pageName) =>
|
||||||
pageName
|
pageName ? `${pageName.charAt(0).toUpperCase()}${pageName.slice(1)}` : 'Page'
|
||||||
? `${pageName.charAt(0).toUpperCase()}${pageName.slice(1)}`
|
|
||||||
: 'Page'
|
|
||||||
|
|
||||||
const getObjectDisplayName = (objectData, model) => {
|
const getObjectDisplayName = (objectData, model) => {
|
||||||
if (objectData?.name) return objectData.name
|
if (objectData?.name) return objectData.name
|
||||||
@ -17,18 +15,9 @@ const getObjectDisplayName = (objectData, model) => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const formatObjectPageTabTitle = ({
|
export const formatObjectPageTabTitle = ({ pageName, objectName } = {}) => {
|
||||||
pageName,
|
|
||||||
model,
|
|
||||||
objectName
|
|
||||||
} = {}) => {
|
|
||||||
const pageLabel = formatPageLabel(pageName)
|
const pageLabel = formatPageLabel(pageName)
|
||||||
const collectionLabel = model?.labelPlural
|
if (objectName) return `${objectName} - ${pageLabel}`
|
||||||
if (objectName && collectionLabel) {
|
|
||||||
return `${pageLabel} (${objectName}) - ${collectionLabel}`
|
|
||||||
}
|
|
||||||
if (objectName) return `${pageLabel} (${objectName})`
|
|
||||||
if (collectionLabel) return `${pageLabel} - ${collectionLabel}`
|
|
||||||
return pageLabel
|
return pageLabel
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,7 +63,7 @@ export const useObjectNavigationTabPage = ({
|
|||||||
}, [connected, model, modelName, objectId, subscribeToObjectUpdates])
|
}, [connected, model, modelName, objectId, subscribeToObjectUpdates])
|
||||||
|
|
||||||
useNavigationTabPage({
|
useNavigationTabPage({
|
||||||
title: formatObjectPageTabTitle({ pageName, model, objectName }),
|
title: formatObjectPageTabTitle({ pageName, objectName }),
|
||||||
modelName
|
modelName
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user