fix(frontend): new file and folder creation hang, empty folder UX

- Replace circular watchEffects in FileExplorer/Gallery with explicit watchers

  to stop recursive Vue updates when creating items in empty folders.

- Move EmptyFolder rendering inside FileExplorer/Gallery so empty/list swaps

  no longer trigger folder slide transitions.

- Keep EmptyFolder text size consistent across list and gallery views.
This commit is contained in:
LeoVasanko committed 2026-06-16 22:09:53 +00:00
1 parent bf8a049b92
commit 77e35cf0fc
5 files changed
+110 -63

No files matched your search

+22 -1
View File
@@ -1,5 +1,5 @@
<template>
<div v-if="!props.path || documents.length === 0" class="empty-container">
<div v-if="showEmpty" class="empty-container">
<component :is="cog" :class="['cog', { stopped: store.dialog === 'accessdenied' || store.authInProgress }]"/>
<p v-if="store.dialog === 'accessdenied'">Access Denied</p>
<p v-else-if="!store.connected">No Connection</p>
@@ -14,6 +14,7 @@
import { Cog } from '@/assets/svg'
import { useMainStore } from '@/stores/main'
import { exists } from '@/utils/fileutil'
import { computed } from 'vue'
const cog = Cog
const store = useMainStore()
@@ -21,9 +22,29 @@ const props = defineProps<{
path: string[]
documents: Document[]
}>()
const showEmpty = computed(() => {
const loc = props.path.join('/')
const hasVisibleGhost = store.ghosts.some(g => {
const full = g.loc ? `${g.loc}/${g.name}` : g.name
return g.loc === loc && !store.hiddenPaths.has(full)
})
return !props.path || (props.documents.length === 0 && !hasVisibleGhost)
})
</script>
<style scoped>
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
font-size: 2rem;
text-shadow: 0 0 .3rem #000, 0 0 2rem #0008;
color: var(--accent-color);
}
@keyframes rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
+41 -26
View File
@@ -15,7 +15,7 @@
<tr v-if="editing?.key === 'new'" :class="editing.dir ? 'folder' : 'file'">
<td class="selection"></td>
<td class="name">
<FileRenameInput :doc="editing" :rename="createItem" :exit="() => {editing = null}" />
<FileRenameInput :doc="editing" :rename="createItem" :exit="exitEditing" />
</td>
<FileModified :doc=editing :now=nowkey />
<FileSize :doc=editing />
@@ -46,7 +46,7 @@
</td>
<td class="name">
<template v-if="editing === doc">
<FileRenameInput :doc="doc" :rename="rename" :exit="() => {editing = null}" />
<FileRenameInput :doc="doc" :rename="rename" :exit="exitEditing" />
</template>
<template v-else>
<a :href="doc.text ? doc.editurl : doc.url" tabindex=-1 @contextmenu.stop @focus.stop="store.cursor = doc.key">
@@ -69,6 +69,7 @@
</tr>
</tbody>
</table>
<EmptyFolder v-else :documents="documents" :path="props.path" />
</template>
<script setup lang="ts">
@@ -81,11 +82,12 @@ import ContextMenu from '@imengyu/vue3-context-menu'
import {
computed,
nextTick,
onDeactivated,
onMounted,
onUnmounted,
ref,
shallowRef,
watchEffect
watch
} from 'vue'
import { useRouter } from 'vue-router'
import FileRenameInput from './FileRenameInput.vue'
@@ -189,6 +191,9 @@ const pageMove = (direction: 1 | -1, ev: KeyboardEvent) => {
// File rename
const editing = shallowRef<Doc | null>(null)
const exitEditing = () => {
editing.value = null
}
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
@@ -243,7 +248,7 @@ defineExpose({
const docs = props.documents
if (docs.length > 0) {
store.cursor = docs[0]!.key
// Also focus the element directly (watchEffect won't trigger if cursor unchanged)
// Also focus the element directly (post-flush watcher won't trigger if cursor unchanged)
nextTick(() => {
const a = document.querySelector(
`#file-${store.cursor} .name a`
@@ -329,22 +334,35 @@ const focusBreadcrumb = () => {
const keyboardFollowScroll = createKeyboardFollowScroll()
const markKeyboardFollow = keyboardFollowScroll.markKeyboardFollow
const keepCursorVisibleSmooth = keyboardFollowScroll.keepVisible
watchEffect(() => {
if (store.cursor && store.cursor !== editing.value?.key) editing.value = null
if (editing.value) store.cursor = editing.value?.key
if (store.cursor) {
const a = document.querySelector(
`#file-${store.cursor} .name a`
) as HTMLAnchorElement | null
if (a) a.focus({ preventScroll: true })
watch(
() => store.cursor,
cursor => {
if (cursor && editing.value && cursor !== editing.value.key) {
exitEditing()
}
}
})
watchEffect(() => {
if (!props.documents.length && store.cursor && !store.query) {
store.cursor = ''
focusBreadcrumb()
)
watch(
() => store.cursor,
cursor => {
if (cursor && !editing.value) {
const a = document.querySelector(
`#file-${cursor} .name a`
) as HTMLAnchorElement | null
if (a) a.focus({ preventScroll: true })
}
},
{ flush: 'post' }
)
watch(
() => [props.documents.length, store.cursor, store.query, editing.value] as const,
([len, cursor, query, editingDoc]) => {
if (!len && cursor && !query && !editingDoc) {
store.cursor = ''
focusBreadcrumb()
}
}
})
)
let nowkey = ref(0)
let modifiedTimer: any = null
const updateModified = () => {
@@ -358,6 +376,9 @@ onMounted(() => {
active.focus({ preventScroll: true })
}
})
onDeactivated(() => {
if (editing.value) exitEditing()
})
onUnmounted(() => {
keyboardFollowScroll.cancel()
clearInterval(modifiedTimer)
@@ -373,7 +394,8 @@ const createItem = async (doc: Doc, name: string) => {
doc.name = name
doc.key = crypto.randomUUID()
store.addGhost(doc)
editing.value = null
store.cursor = doc.key
exitEditing()
const path = doc.loc ? `${doc.loc}/${name}` : name
try {
const res = doc.dir
@@ -658,12 +680,6 @@ tbody .selection input {
content: '📁';
font-size: 1.5rem;
}
.empty-container {
padding-top: 3rem;
text-align: center;
font-size: 3rem;
color: var(--accent-color);
}
.folder-change {
margin-left: -.5rem;
}
@@ -674,4 +690,3 @@ tbody .selection input {
color: #888;
}
</style>
@/stores/main
+34 -21
View File
@@ -13,6 +13,7 @@
/>
</template>
</div>
<EmptyFolder v-else :documents="documents" :path="props.path" />
</template>
<script setup lang="ts">
@@ -23,16 +24,15 @@ import type { SortOrder } from '@/utils/docsort'
import { createKeyboardFollowScroll } from '@/utils/keyboardFollowScroll'
import ContextMenu from '@imengyu/vue3-context-menu'
import {
onActivated,
onDeactivated,
computed,
nextTick,
onActivated,
onDeactivated,
onMounted,
onUnmounted,
ref,
shallowRef,
watch,
watchEffect
watch
} from 'vue'
import { useRouter } from 'vue-router'
@@ -304,7 +304,7 @@ defineExpose({
const docs = props.documents
if (docs.length > 0) {
store.cursor = docs[0]!.key
// Also focus the element directly (watchEffect won't trigger if cursor unchanged)
// Also focus the element directly (post-flush watcher won't trigger if cursor unchanged)
nextTick(() => {
const a = document.querySelector(
`#file-${store.cursor}`
@@ -396,24 +396,35 @@ const focusBreadcrumb = () => {
const keyboardFollowScroll = createKeyboardFollowScroll()
const markKeyboardFollow = keyboardFollowScroll.markKeyboardFollow
const keepCursorVisibleSmooth = keyboardFollowScroll.keepVisible
watchEffect(() => {
if (store.cursor && store.cursor !== editing.value?.key) editing.value = null
if (editing.value) store.cursor = editing.value.key
if (store.cursor && !editing.value) {
const a = document.querySelector(
`#file-${store.cursor}`
) as HTMLAnchorElement | null
if (a) {
a.focus({ preventScroll: true })
watch(
() => store.cursor,
cursor => {
if (cursor && editing.value && cursor !== editing.value.key) {
exit()
}
}
})
watchEffect(() => {
if (!props.documents.length && store.cursor && !store.query) {
store.cursor = ''
focusBreadcrumb()
)
watch(
() => store.cursor,
cursor => {
if (cursor && !editing.value) {
const a = document.querySelector(`#file-${cursor}`) as HTMLAnchorElement | null
if (a) {
a.focus({ preventScroll: true })
}
}
},
{ flush: 'post' }
)
watch(
() => [props.documents.length, store.cursor, store.query, editing.value] as const,
([len, cursor, query, editingDoc]) => {
if (!len && cursor && !query && !editingDoc) {
store.cursor = ''
focusBreadcrumb()
}
}
})
)
let resizeObserver: ResizeObserver | null = null
const attachGalleryObservers = () => {
if (!gallery.value || resizeObserver) return
@@ -445,6 +456,7 @@ onActivated(() => {
})
onDeactivated(() => {
detachGalleryObservers()
if (editing.value) exit()
})
onUnmounted(() => {
keyboardFollowScroll.cancel()
@@ -464,7 +476,8 @@ const createItem = async (doc: Doc, name: string) => {
doc.name = name
doc.key = crypto.randomUUID()
store.addGhost(doc)
editing.value = null
store.cursor = doc.key
exit()
const path = doc.loc ? `${doc.loc}/${name}` : name
try {
const res = doc.dir