From 49232f11cc7c86020bb5530d67d915d253a25097 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Tue, 28 Jul 2026 02:16:23 +0000 Subject: [PATCH] Fix rename flow: KeepAlive-cached view watchers cleared cursor on stale props Deactivated FileExplorer/Gallery instances stay alive in KeepAlive with frozen, potentially empty document props. Their empty-folder watcher cleared store.cursor and yanked focus to the breadcrumb on every cursor change, breaking rename via gallery pen and keyboard entry into the file list, and hiding the explorer rename button. - Guard cursor watchers in FileExplorer/Gallery with an isActive flag (set on activated, cleared on deactivated) - Declare emits in GalleryFigure (rename/menu fell through to the root anchor as native listeners) - Show the explorer rename button on row hover with a delayed fade-in instead of only on the keyboard-focused row --- frontend/src/components/FileExplorer.vue | 19 ++++++++++++++++++- frontend/src/components/Gallery.vue | 14 +++++++++++++- frontend/src/components/GalleryFigure.vue | 6 +++++- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/FileExplorer.vue b/frontend/src/components/FileExplorer.vue index 6f75589..4647b6e 100644 --- a/frontend/src/components/FileExplorer.vue +++ b/frontend/src/components/FileExplorer.vue @@ -53,7 +53,7 @@ {{ doc.name }} - + @@ -84,6 +84,7 @@ import ContextMenu from '@imengyu/vue3-context-menu' import { computed, nextTick, + onActivated, onDeactivated, onMounted, onUnmounted, @@ -336,9 +337,13 @@ const focusBreadcrumb = () => { const keyboardFollowScroll = createKeyboardFollowScroll() const markKeyboardFollow = keyboardFollowScroll.markKeyboardFollow const keepCursorVisibleSmooth = keyboardFollowScroll.keepVisible +// Deactivated (KeepAlive-cached) instances stay alive with frozen, potentially +// stale props - their watchers must not react to global store changes. +let isActive = true watch( () => store.cursor, cursor => { + if (!isActive) return if (cursor && editing.value && cursor !== editing.value.key) { exitEditing() } @@ -347,6 +352,7 @@ watch( watch( () => store.cursor, cursor => { + if (!isActive) return if (cursor && !editing.value) { const a = document.querySelector( `#file-${cursor} .name a` @@ -359,6 +365,7 @@ watch( watch( () => [props.documents.length, store.cursor, store.query, editing.value] as const, ([len, cursor, query, editingDoc]) => { + if (!isActive) return if (!len && cursor && !query && !editingDoc) { store.cursor = '' focusBreadcrumb() @@ -378,7 +385,11 @@ onMounted(() => { active.focus({ preventScroll: true }) } }) +onActivated(() => { + isActive = true +}) onDeactivated(() => { + isActive = false if (editing.value) exitEditing() }) onUnmounted(() => { @@ -617,6 +628,12 @@ table td { .name .rename-button { position: absolute; right: 0; + opacity: 0; + visibility: hidden; +} +tbody tr:hover .name .rename-button { + opacity: 1; + visibility: visible; animation: appear calc(5 * var(--transition-time)) linear; } @keyframes appear { diff --git a/frontend/src/components/Gallery.vue b/frontend/src/components/Gallery.vue index a59aabd..1ab52cb 100644 --- a/frontend/src/components/Gallery.vue +++ b/frontend/src/components/Gallery.vue @@ -8,7 +8,7 @@ :editing="editing === doc ? {rename, exit} : null" :style="{ '--gallery-figure-height': rowHeightsByKey[doc.key] ?? '15em' }" @menu="contextMenu($event, doc)" - @rename="editing = doc; store.cursor = doc.key" + @rename="onFigureRename(doc)" :class="{ 'folder-start': showFolderBreadcrumb(index) }" /> @@ -64,6 +64,10 @@ const editing = shallowRef(null) const exit = () => { editing.value = null } +const onFigureRename = (doc: Doc) => { + editing.value = doc + store.cursor = doc.key +} const rename = async (doc: Doc, newName: string) => { const oldName = doc.name doc.name = newName // We should get an update from watch but this is quicker @@ -396,9 +400,13 @@ const focusBreadcrumb = () => { const keyboardFollowScroll = createKeyboardFollowScroll() const markKeyboardFollow = keyboardFollowScroll.markKeyboardFollow const keepCursorVisibleSmooth = keyboardFollowScroll.keepVisible +// Deactivated (KeepAlive-cached) instances stay alive with frozen, potentially +// stale props - their watchers must not react to global store changes. +let isActive = true watch( () => store.cursor, cursor => { + if (!isActive) return if (cursor && editing.value && cursor !== editing.value.key) { exit() } @@ -407,6 +415,7 @@ watch( watch( () => store.cursor, cursor => { + if (!isActive) return if (cursor && !editing.value) { const a = document.querySelector(`#file-${cursor}`) as HTMLAnchorElement | null if (a) { @@ -419,6 +428,7 @@ watch( watch( () => [props.documents.length, store.cursor, store.query, editing.value] as const, ([len, cursor, query, editingDoc]) => { + if (!isActive) return if (!len && cursor && !query && !editingDoc) { store.cursor = '' focusBreadcrumb() @@ -449,12 +459,14 @@ onMounted(() => { attachGalleryObservers() }) onActivated(() => { + isActive = true nextTick(() => { updateColumns() attachGalleryObservers() }) }) onDeactivated(() => { + isActive = false detachGalleryObservers() if (editing.value) exit() }) diff --git a/frontend/src/components/GalleryFigure.vue b/frontend/src/components/GalleryFigure.vue index 98abbf5..77e3953 100644 --- a/frontend/src/components/GalleryFigure.vue +++ b/frontend/src/components/GalleryFigure.vue @@ -29,7 +29,7 @@ {{ snap.displayName }} .{{ snap.ext }} - +
@@ -64,6 +64,10 @@ const props = defineProps<{ doc: Doc editing?: EditingProp }>() +const emit = defineEmits<{ + (e: 'rename'): void + (e: 'menu', ev: MouseEvent): void +}>() const m = ref(null) const tooltip = ref | null>(null)