fix(notes): don't save empty content over a note that hasn't loaded
All checks were successful
CI / verify (push) Successful in 1m9s
All checks were successful
CI / verify (push) Successful in 1m9s
Entering and exiting edit mode while a note was still loading (or after a failed fetch) triggered a save of the empty placeholder, clobbering the real file on GitHub with an empty commit. Gate the dirty check, performSave, and the edit button on a loaded baseline.
This commit is contained in:
@@ -188,6 +188,19 @@ const loadNote = async () => {
|
|||||||
|
|
||||||
onMounted(loadNote)
|
onMounted(loadNote)
|
||||||
|
|
||||||
|
// A note only has a trustworthy baseline once it has finished loading. While
|
||||||
|
// it is still loading (or after a failed load) rawContent holds the empty
|
||||||
|
// placeholder and initialRawContent is null, so a naive
|
||||||
|
// `rawContent !== initialRawContent` check reads as a huge edit — and saving
|
||||||
|
// that would clobber the real file on GitHub with an empty commit. Gate every
|
||||||
|
// "is this dirty?" decision on a loaded baseline.
|
||||||
|
const isDirty = computed(
|
||||||
|
() =>
|
||||||
|
loadStatus.value === "ready" &&
|
||||||
|
initialRawContent.value !== null &&
|
||||||
|
rawContent.value !== initialRawContent.value
|
||||||
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
path,
|
path,
|
||||||
(p) => {
|
(p) => {
|
||||||
@@ -215,6 +228,13 @@ const performSave = async (overrideSha?: string) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Defence in depth: never push content we didn't successfully load, or we'd
|
||||||
|
// overwrite the file on GitHub with the empty placeholder.
|
||||||
|
if (loadStatus.value !== "ready" || initialRawContent.value === null) {
|
||||||
|
console.warn("refusing to save a note that hasn't finished loading")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const editedSha = overrideSha ?? (await getEditedSha()) ?? sha.value
|
const editedSha = overrideSha ?? (await getEditedSha()) ?? sha.value
|
||||||
const { sha: newSha, conflict } = await updateFile({
|
const { sha: newSha, conflict } = await updateFile({
|
||||||
content: rawContent.value,
|
content: rawContent.value,
|
||||||
@@ -287,8 +307,7 @@ watch(mode, async (newMode) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasUserFinishedToEdit =
|
const hasUserFinishedToEdit = newMode === "read" && isDirty.value
|
||||||
newMode === "read" && rawContent.value !== initialRawContent.value
|
|
||||||
|
|
||||||
if (!hasUserFinishedToEdit) {
|
if (!hasUserFinishedToEdit) {
|
||||||
return
|
return
|
||||||
@@ -336,7 +355,7 @@ const onBadgeClick = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasUnsavedEdits = rawContent.value !== initialRawContent.value
|
const hasUnsavedEdits = isDirty.value
|
||||||
if (hasUnsavedEdits) {
|
if (hasUnsavedEdits) {
|
||||||
await handleConflict()
|
await handleConflict()
|
||||||
return
|
return
|
||||||
@@ -377,7 +396,7 @@ const onBadgeClick = async () => {
|
|||||||
class="action"
|
class="action"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="isMarkdown && canPush"
|
v-if="isMarkdown && canPush && loadStatus === 'ready'"
|
||||||
class="action button is-text is-light"
|
class="action button is-text is-light"
|
||||||
:class="{ 'is-link': mode === 'edit' }"
|
:class="{ 'is-link': mode === 'edit' }"
|
||||||
:style="mode === 'edit' ? 'color: var(--color-primary)' : ''"
|
:style="mode === 'edit' ? 'color: var(--color-primary)' : ''"
|
||||||
|
|||||||
Reference in New Issue
Block a user