156 lines
3.8 KiB
TypeScript
156 lines
3.8 KiB
TypeScript
import { createSharedComposable } from '@vueuse/core'
|
|
|
|
type ChangelogEntry = {
|
|
hash: string
|
|
shortHash: string
|
|
subject: string
|
|
authorName: string
|
|
committedAt: string
|
|
}
|
|
|
|
type ChangelogSeenState = {
|
|
lastOpenedAt: string | null
|
|
latestSeenHash: string | null
|
|
}
|
|
|
|
const defaultSeenState = (): ChangelogSeenState => ({
|
|
lastOpenedAt: null,
|
|
latestSeenHash: null
|
|
})
|
|
|
|
let changelogRequest: Promise<void> | null = null
|
|
|
|
const _useChangelog = () => {
|
|
const auth = useAuthStore()
|
|
|
|
const entries = useState<ChangelogEntry[]>('changelog:entries', () => [])
|
|
const pending = useState<boolean>('changelog:pending', () => false)
|
|
const error = useState<string | null>('changelog:error', () => null)
|
|
const loadedKey = useState<string | null>('changelog:loaded-key', () => null)
|
|
const seenState = useState<ChangelogSeenState>('changelog:seen-state', defaultSeenState)
|
|
|
|
const scopeKey = computed(() => {
|
|
const userId = auth.user?.id
|
|
const tenantId = auth.activeTenant
|
|
|
|
if (!userId || !tenantId) return null
|
|
|
|
return `${userId}:${tenantId}`
|
|
})
|
|
|
|
const storageKey = computed(() => {
|
|
if (!scopeKey.value) return null
|
|
|
|
return `fedeo:changelog:last-opened:${scopeKey.value}`
|
|
})
|
|
|
|
const latestEntry = computed(() => entries.value[0] || null)
|
|
|
|
const hasUnread = computed(() => {
|
|
if (!latestEntry.value?.hash) return false
|
|
|
|
return latestEntry.value.hash !== seenState.value.latestSeenHash
|
|
})
|
|
|
|
function loadSeenState() {
|
|
if (!process.client || !storageKey.value) {
|
|
seenState.value = defaultSeenState()
|
|
return
|
|
}
|
|
|
|
try {
|
|
const raw = localStorage.getItem(storageKey.value)
|
|
|
|
if (!raw) {
|
|
seenState.value = defaultSeenState()
|
|
return
|
|
}
|
|
|
|
const parsed = JSON.parse(raw)
|
|
|
|
seenState.value = {
|
|
lastOpenedAt: parsed?.lastOpenedAt || null,
|
|
latestSeenHash: parsed?.latestSeenHash || null
|
|
}
|
|
} catch (err) {
|
|
console.error('Could not parse changelog seen state', err)
|
|
seenState.value = defaultSeenState()
|
|
}
|
|
}
|
|
|
|
async function refresh(force = false) {
|
|
if (!process.client || !scopeKey.value) return
|
|
if (!force && loadedKey.value === scopeKey.value && entries.value.length) return
|
|
if (changelogRequest) return changelogRequest
|
|
|
|
changelogRequest = (async () => {
|
|
pending.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
const response = await useNuxtApp().$api('/api/functions/changelog', {
|
|
query: { limit: 20 }
|
|
})
|
|
|
|
entries.value = Array.isArray(response?.entries) ? response.entries : []
|
|
loadedKey.value = scopeKey.value
|
|
} catch (err: any) {
|
|
error.value = err?.data?.error || err?.message || 'Changelog konnte nicht geladen werden.'
|
|
} finally {
|
|
pending.value = false
|
|
}
|
|
})()
|
|
|
|
try {
|
|
await changelogRequest
|
|
} finally {
|
|
changelogRequest = null
|
|
}
|
|
}
|
|
|
|
function markAsSeen() {
|
|
if (!process.client || !storageKey.value) return
|
|
|
|
const nextState = {
|
|
lastOpenedAt: new Date().toISOString(),
|
|
latestSeenHash: latestEntry.value?.hash || null
|
|
}
|
|
|
|
seenState.value = nextState
|
|
|
|
try {
|
|
localStorage.setItem(storageKey.value, JSON.stringify(nextState))
|
|
} catch (err) {
|
|
console.error('Could not persist changelog seen state', err)
|
|
}
|
|
}
|
|
|
|
watch(storageKey, () => {
|
|
loadSeenState()
|
|
}, { immediate: true })
|
|
|
|
watch(scopeKey, (nextScopeKey, previousScopeKey) => {
|
|
if (!process.client || !nextScopeKey) return
|
|
|
|
if (nextScopeKey !== previousScopeKey) {
|
|
entries.value = []
|
|
loadedKey.value = null
|
|
}
|
|
|
|
void refresh(true)
|
|
}, { immediate: true })
|
|
|
|
return {
|
|
entries,
|
|
pending,
|
|
error,
|
|
latestEntry,
|
|
hasUnread,
|
|
seenState,
|
|
refresh,
|
|
markAsSeen
|
|
}
|
|
}
|
|
|
|
export const useChangelog = createSharedComposable(_useChangelog)
|