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 {
|
const {
|
||||||
path,
|
path,
|
||||||
|
newerSha,
|
||||||
content,
|
content,
|
||||||
rawContent,
|
rawContent,
|
||||||
getRawContent,
|
getRawContent,
|
||||||
saveCacheNote,
|
saveCacheNote,
|
||||||
getEditedSha
|
getEditedSha
|
||||||
} = useFile(sha)
|
} = 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 initialRawContent = ref<string | null>(null)
|
||||||
const isMarkdown = computed(() =>
|
const isMarkdown = computed(() =>
|
||||||
path.value ? isMarkdownPath(path.value) : true
|
path.value ? isMarkdownPath(path.value) : true
|
||||||
@@ -438,6 +443,16 @@ const onBadgeClick = async () => {
|
|||||||
<edit-note :key="editKey" v-model="rawContent" />
|
<edit-note :key="editKey" v-model="rawContent" />
|
||||||
</div>
|
</div>
|
||||||
<template v-else-if="mode === 'read'">
|
<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
|
<div
|
||||||
v-if="displayedContent"
|
v-if="displayedContent"
|
||||||
class="note-content"
|
class="note-content"
|
||||||
@@ -519,6 +534,28 @@ $border-color: rgba(18, 19, 58, 0.2);
|
|||||||
display: none;
|
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 {
|
.action {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { computed, Ref, ref, toValue } from "vue"
|
|||||||
|
|
||||||
import { markdownBuilder } from "@/hooks/useMarkdown.hook"
|
import { markdownBuilder } from "@/hooks/useMarkdown.hook"
|
||||||
import { prepareNoteCache } from "@/modules/note/cache/prepareNoteCache"
|
import { prepareNoteCache } from "@/modules/note/cache/prepareNoteCache"
|
||||||
|
import { latestShaIfOlder } from "@/modules/note/snapshotStatus"
|
||||||
import { queryFileContent } from "@/modules/repo/services/repo"
|
import { queryFileContent } from "@/modules/repo/services/repo"
|
||||||
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
|
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
|
||||||
|
|
||||||
@@ -14,6 +15,16 @@ export const useFile = (sha: Ref<string> | string, retrieveContent = true) => {
|
|||||||
return file?.path
|
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 {
|
const {
|
||||||
render,
|
render,
|
||||||
renderFromUTF8,
|
renderFromUTF8,
|
||||||
@@ -47,6 +58,8 @@ export const useFile = (sha: Ref<string> | string, retrieveContent = true) => {
|
|||||||
fromCache.value = !!cachedNote
|
fromCache.value = !!cachedNote
|
||||||
|
|
||||||
if (cachedNote) {
|
if (cachedNote) {
|
||||||
|
cachedNotePath.value = cachedNote.path ?? cachedNotePath.value
|
||||||
|
|
||||||
if (from === "path") {
|
if (from === "path") {
|
||||||
queryFileContent(store.user, store.repo, shaValue).then(
|
queryFileContent(store.user, store.repo, shaValue).then(
|
||||||
(fileContent) => {
|
(fileContent) => {
|
||||||
@@ -104,6 +117,7 @@ export const useFile = (sha: Ref<string> | string, retrieveContent = true) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
path,
|
path,
|
||||||
|
newerSha,
|
||||||
content,
|
content,
|
||||||
rawContent,
|
rawContent,
|
||||||
getRawContent,
|
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),
|
_id: generateId(DataType.Note, sha),
|
||||||
$type: DataType.Note,
|
$type: DataType.Note,
|
||||||
content,
|
content,
|
||||||
editedSha
|
editedSha,
|
||||||
|
path
|
||||||
}
|
}
|
||||||
return path
|
return path
|
||||||
? [base, { ...base, _id: generateId(DataType.Note, path) }]
|
? [base, { ...base, _id: generateId(DataType.Note, path) }]
|
||||||
@@ -71,7 +72,8 @@ export const prepareNoteCache = (sha: string, path?: string) => {
|
|||||||
_id: contentId,
|
_id: contentId,
|
||||||
$type: DataType.Note,
|
$type: DataType.Note,
|
||||||
content,
|
content,
|
||||||
editedSha: params?.editedSha
|
editedSha: params?.editedSha,
|
||||||
|
path: params?.path ?? path
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params && params.path) {
|
if (params && params.path) {
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import { Model } from "@/data/models/Model"
|
|||||||
export interface Note extends Model<DataType.Note> {
|
export interface Note extends Model<DataType.Note> {
|
||||||
content: string
|
content: string
|
||||||
editedSha?: 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 {
|
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