148 lines
4.8 KiB
TypeScript
148 lines
4.8 KiB
TypeScript
import { computed, ref } from 'vue'
|
|
import { defineStore } from 'pinia'
|
|
import type { WorkspaceTab, WorkspaceTabCandidate, WorkspaceTabsSnapshot } from './types'
|
|
|
|
const STORAGE_PREFIX = 'cc-admin-workspace-tabs:v1'
|
|
export const WORKSPACE_TAB_LIMIT = 12
|
|
export const MAX_CACHED_WORKSPACE_VIEWS = 24
|
|
|
|
function storageKey(userId: string) {
|
|
return `${STORAGE_PREFIX}:${userId}`
|
|
}
|
|
|
|
function validTab(value: unknown): value is WorkspaceTab {
|
|
if (!value || typeof value !== 'object') return false
|
|
const tab = value as Partial<WorkspaceTab>
|
|
return typeof tab.id === 'string'
|
|
&& Boolean(tab.id)
|
|
&& typeof tab.title === 'string'
|
|
&& typeof tab.path === 'string'
|
|
&& tab.path.startsWith('/')
|
|
&& typeof tab.lastVisitedAt === 'number'
|
|
&& Number.isFinite(tab.lastVisitedAt)
|
|
}
|
|
|
|
export function readWorkspaceTabsSnapshot(userId: string): WorkspaceTabsSnapshot {
|
|
if (!userId) return { tabs: [], activeId: null }
|
|
try {
|
|
const parsed = JSON.parse(sessionStorage.getItem(storageKey(userId)) || 'null') as Partial<WorkspaceTabsSnapshot> | null
|
|
if (!parsed || !Array.isArray(parsed.tabs)) return { tabs: [], activeId: null }
|
|
const tabs = parsed.tabs.filter(validTab)
|
|
const activeId = typeof parsed.activeId === 'string' && tabs.some((tab) => tab.id === parsed.activeId)
|
|
? parsed.activeId
|
|
: null
|
|
return { tabs, activeId }
|
|
} catch {
|
|
return { tabs: [], activeId: null }
|
|
}
|
|
}
|
|
|
|
export function readStoredActiveWorkspacePath(userId: string) {
|
|
const snapshot = readWorkspaceTabsSnapshot(userId)
|
|
return snapshot.tabs.find((tab) => tab.id === snapshot.activeId)?.path ?? null
|
|
}
|
|
|
|
export const useWorkspaceTabsStore = defineStore('workspace-tabs', () => {
|
|
const tabs = ref<WorkspaceTab[]>([])
|
|
const activeId = ref<string | null>(null)
|
|
const initializedUserId = ref('')
|
|
const cacheEpochs = ref<Record<string, number>>({})
|
|
|
|
const activeTab = computed(() => tabs.value.find((tab) => tab.id === activeId.value) ?? null)
|
|
|
|
function persist() {
|
|
if (!initializedUserId.value) return
|
|
sessionStorage.setItem(storageKey(initializedUserId.value), JSON.stringify({
|
|
tabs: tabs.value,
|
|
activeId: activeId.value,
|
|
} satisfies WorkspaceTabsSnapshot))
|
|
}
|
|
|
|
function initialize(userId: string) {
|
|
if (!userId || initializedUserId.value === userId) return
|
|
initializedUserId.value = userId
|
|
const snapshot = readWorkspaceTabsSnapshot(userId)
|
|
tabs.value = snapshot.tabs
|
|
activeId.value = snapshot.activeId
|
|
}
|
|
|
|
function bumpCacheEpoch(tabId: string) {
|
|
cacheEpochs.value = {
|
|
...cacheEpochs.value,
|
|
[tabId]: (cacheEpochs.value[tabId] ?? 0) + 1,
|
|
}
|
|
}
|
|
|
|
function removeByIds(ids: readonly string[]) {
|
|
const idSet = new Set(ids)
|
|
if (idSet.size === 0) return activeTab.value
|
|
const activeWasRemoved = activeId.value !== null && idSet.has(activeId.value)
|
|
for (const id of idSet) bumpCacheEpoch(id)
|
|
tabs.value = tabs.value.filter((tab) => !idSet.has(tab.id))
|
|
if (activeWasRemoved) {
|
|
activeId.value = [...tabs.value].sort((left, right) => right.lastVisitedAt - left.lastVisitedAt)[0]?.id ?? null
|
|
} else if (activeId.value && !tabs.value.some((tab) => tab.id === activeId.value)) {
|
|
activeId.value = null
|
|
}
|
|
persist()
|
|
return activeTab.value
|
|
}
|
|
|
|
function enforceLimit(dirtyTabIds: ReadonlySet<string>) {
|
|
while (tabs.value.length > WORKSPACE_TAB_LIMIT) {
|
|
const candidate = tabs.value
|
|
.filter((tab) => tab.id !== activeId.value && !dirtyTabIds.has(tab.id))
|
|
.sort((left, right) => left.lastVisitedAt - right.lastVisitedAt)[0]
|
|
if (!candidate) break
|
|
removeByIds([candidate.id])
|
|
}
|
|
}
|
|
|
|
function open(candidate: WorkspaceTabCandidate, dirtyTabIds: ReadonlySet<string> = new Set()) {
|
|
const now = Date.now()
|
|
const existing = tabs.value.find((tab) => tab.id === candidate.id)
|
|
if (existing) {
|
|
existing.title = candidate.title
|
|
existing.path = candidate.path
|
|
existing.lastVisitedAt = now
|
|
} else {
|
|
tabs.value.push({ ...candidate, lastVisitedAt: now })
|
|
}
|
|
activeId.value = candidate.id
|
|
enforceLimit(dirtyTabIds)
|
|
persist()
|
|
}
|
|
|
|
function prune(keep: (tab: WorkspaceTab) => boolean) {
|
|
const removedIds = tabs.value.filter((tab) => !keep(tab)).map((tab) => tab.id)
|
|
return { removedIds, fallback: removeByIds(removedIds) }
|
|
}
|
|
|
|
function cacheKey(tabId: string | null) {
|
|
if (!tabId) return 'workspace-route'
|
|
return `${tabId}:${cacheEpochs.value[tabId] ?? 0}`
|
|
}
|
|
|
|
function reset(removePersisted = false) {
|
|
if (removePersisted && initializedUserId.value) sessionStorage.removeItem(storageKey(initializedUserId.value))
|
|
tabs.value = []
|
|
activeId.value = null
|
|
cacheEpochs.value = {}
|
|
initializedUserId.value = ''
|
|
}
|
|
|
|
return {
|
|
tabs,
|
|
activeId,
|
|
activeTab,
|
|
initializedUserId,
|
|
initialize,
|
|
open,
|
|
removeByIds,
|
|
prune,
|
|
cacheKey,
|
|
bumpCacheEpoch,
|
|
reset,
|
|
}
|
|
})
|