feat(notes): banner when viewing an older shared version
All checks were successful
CI / verify (push) Successful in 1m3s
All checks were successful
CI / verify (push) Successful in 1m3s
Cache the note path on each snapshot so an old shared sha can find its current version, and show a calm "older shared version — View latest" banner. The exact snapshot is shown as-is; the banner only offers a jump to the latest, never swapping content.
This commit is contained in:
@@ -68,12 +68,17 @@ const advanceStackTo = (newSha: string | null) => {
|
||||
|
||||
const {
|
||||
path,
|
||||
newerSha,
|
||||
content,
|
||||
rawContent,
|
||||
getRawContent,
|
||||
saveCacheNote,
|
||||
getEditedSha
|
||||
} = useFile(sha)
|
||||
|
||||
// When this is an older snapshot of a still-existing note, jump the stack to
|
||||
// its current version (the exact snapshot is never swapped underneath you).
|
||||
const viewLatest = () => advanceStackTo(newerSha.value)
|
||||
const initialRawContent = ref<string | null>(null)
|
||||
const isMarkdown = computed(() =>
|
||||
path.value ? isMarkdownPath(path.value) : true
|
||||
@@ -438,6 +443,16 @@ const onBadgeClick = async () => {
|
||||
<edit-note :key="editKey" v-model="rawContent" />
|
||||
</div>
|
||||
<template v-else-if="mode === 'read'">
|
||||
<div v-if="newerSha" class="snapshot-banner">
|
||||
<span>You're viewing an older shared version.</span>
|
||||
<button
|
||||
type="button"
|
||||
class="snapshot-banner-action"
|
||||
@click="viewLatest"
|
||||
>
|
||||
View latest
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="displayedContent"
|
||||
class="note-content"
|
||||
@@ -519,6 +534,28 @@ $border-color: rgba(18, 19, 58, 0.2);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.snapshot-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin: 0 0 1rem;
|
||||
padding: 0.4rem 0.75rem;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.snapshot-banner-action {
|
||||
flex-shrink: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.action {
|
||||
margin: 0;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { computed, Ref, ref, toValue } from "vue"
|
||||
|
||||
import { markdownBuilder } from "@/hooks/useMarkdown.hook"
|
||||
import { prepareNoteCache } from "@/modules/note/cache/prepareNoteCache"
|
||||
import { latestShaIfOlder } from "@/modules/note/snapshotStatus"
|
||||
import { queryFileContent } from "@/modules/repo/services/repo"
|
||||
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
|
||||
|
||||
@@ -14,6 +15,16 @@ export const useFile = (sha: Ref<string> | string, retrieveContent = true) => {
|
||||
return file?.path
|
||||
})
|
||||
|
||||
// Path recovered from the cached note doc — lets us locate the latest version
|
||||
// even when this (old) sha is no longer in store.files.
|
||||
const cachedNotePath = ref<string | undefined>()
|
||||
|
||||
// When viewing an older snapshot of a known note, the note's current sha
|
||||
// (null when already viewing the latest).
|
||||
const newerSha = computed(() =>
|
||||
latestShaIfOlder(shaValue, path.value ?? cachedNotePath.value, store.files)
|
||||
)
|
||||
|
||||
const {
|
||||
render,
|
||||
renderFromUTF8,
|
||||
@@ -47,6 +58,8 @@ export const useFile = (sha: Ref<string> | string, retrieveContent = true) => {
|
||||
fromCache.value = !!cachedNote
|
||||
|
||||
if (cachedNote) {
|
||||
cachedNotePath.value = cachedNote.path ?? cachedNotePath.value
|
||||
|
||||
if (from === "path") {
|
||||
queryFileContent(store.user, store.repo, shaValue).then(
|
||||
(fileContent) => {
|
||||
@@ -104,6 +117,7 @@ export const useFile = (sha: Ref<string> | string, retrieveContent = true) => {
|
||||
|
||||
return {
|
||||
path,
|
||||
newerSha,
|
||||
content,
|
||||
rawContent,
|
||||
getRawContent,
|
||||
|
||||
6
src/modules/note/cache/prepareNoteCache.ts
vendored
6
src/modules/note/cache/prepareNoteCache.ts
vendored
@@ -21,7 +21,8 @@ export const buildNoteDocs = (
|
||||
_id: generateId(DataType.Note, sha),
|
||||
$type: DataType.Note,
|
||||
content,
|
||||
editedSha
|
||||
editedSha,
|
||||
path
|
||||
}
|
||||
return path
|
||||
? [base, { ...base, _id: generateId(DataType.Note, path) }]
|
||||
@@ -71,7 +72,8 @@ export const prepareNoteCache = (sha: string, path?: string) => {
|
||||
_id: contentId,
|
||||
$type: DataType.Note,
|
||||
content,
|
||||
editedSha: params?.editedSha
|
||||
editedSha: params?.editedSha,
|
||||
path: params?.path ?? path
|
||||
}
|
||||
|
||||
if (params && params.path) {
|
||||
|
||||
@@ -4,6 +4,9 @@ import { Model } from "@/data/models/Model"
|
||||
export interface Note extends Model<DataType.Note> {
|
||||
content: string
|
||||
editedSha?: string
|
||||
// The note's path, stored so a cached snapshot can find its latest version
|
||||
// even when its (old) sha is no longer in the repo file list.
|
||||
path?: string
|
||||
}
|
||||
|
||||
export interface PublicNoteListItem {
|
||||
|
||||
23
src/modules/note/snapshotStatus.spec.ts
Normal file
23
src/modules/note/snapshotStatus.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
|
||||
import { latestShaIfOlder } from "./snapshotStatus"
|
||||
|
||||
const files = [
|
||||
{ path: "notes/a.md", sha: "current" },
|
||||
{ path: "notes/b.md", sha: "other" }
|
||||
]
|
||||
|
||||
describe("latestShaIfOlder", () => {
|
||||
it("returns the current sha when viewing an older version of a known note", () => {
|
||||
expect(latestShaIfOlder("old", "notes/a.md", files)).toBe("current")
|
||||
})
|
||||
|
||||
it("returns null when viewing the current version", () => {
|
||||
expect(latestShaIfOlder("current", "notes/a.md", files)).toBeNull()
|
||||
})
|
||||
|
||||
it("returns null when the path is unknown", () => {
|
||||
expect(latestShaIfOlder("old", undefined, files)).toBeNull()
|
||||
expect(latestShaIfOlder("old", "notes/missing.md", files)).toBeNull()
|
||||
})
|
||||
})
|
||||
25
src/modules/note/snapshotStatus.ts
Normal file
25
src/modules/note/snapshotStatus.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Return the note's current (latest) sha when `viewedSha` is NOT it — i.e. you
|
||||
* are viewing an older version — otherwise null. "Older" is not inferred from
|
||||
* the sha (a content hash has no order); it means "not the current sha for this
|
||||
* path" per the repo file list.
|
||||
*
|
||||
* The caller must supply `notePath`. For a current sha it comes from the file
|
||||
* list; for an older sha it comes from the cached snapshot's stored `path`
|
||||
* (notes viewed while current carry it). When the path can't be resolved
|
||||
* (foreign / evicted / pre-upgrade snapshot) this returns null and no banner is
|
||||
* shown — graceful, never a false claim. Content is never swapped.
|
||||
*/
|
||||
export const latestShaIfOlder = (
|
||||
viewedSha: string,
|
||||
notePath: string | undefined,
|
||||
files: ReadonlyArray<{ path?: string; sha?: string }>
|
||||
): string | null => {
|
||||
if (!notePath) {
|
||||
return null
|
||||
}
|
||||
|
||||
const currentSha = files.find((file) => file.path === notePath)?.sha
|
||||
|
||||
return currentSha && currentSha !== viewedSha ? currentSha : null
|
||||
}
|
||||
Reference in New Issue
Block a user