Files
cista-storage/frontend/src/views/ExplorerView.vue
T
LeoVasanko 77e35cf0fc 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.
2026-06-16 22:09:53 +00:00

156 lines
4.6 KiB
Vue

<template>
<div class="transition-wrapper">
<Transition
:name="transitionName"
@after-enter="onAfterEnter"
>
<KeepAlive>
<component
:is="store.prefs.gallery ? Gallery : FileExplorer"
:key="cacheKey"
ref="fileExplorer"
class="explorer-content"
:path="props.path"
:documents="documents"
/>
</KeepAlive>
</Transition>
</div>
<div v-if="store.searchLoading" class="search-loading">Searching...</div>
</template>
<script setup lang="ts">
import FileExplorer from '@/components/FileExplorer.vue'
import Gallery from '@/components/Gallery.vue'
import { getDocuments } from '@/stores/documentStore'
import { useMainStore } from '@/stores/main'
import { collator } from '@/utils'
import { sorted, sortedGrouped } from '@/utils/docsort'
import { computed, nextTick, ref, watch, watchEffect } from 'vue'
const store = useMainStore()
const fileExplorer = ref()
const props = defineProps<{
path: Array<string>
query: string
}>()
// Folder path for component keys - only recreate component when folder changes, not search
const folderPath = computed(() => props.path.join('/'))
const cacheKey = computed(
() => `${store.prefs.gallery ? 'gallery' : 'list'}:${folderPath.value}`
)
const transitionName = computed(() => {
if (store.transitionDirection === 'forward') return 'slide-forward'
if (store.transitionDirection === 'backward') return 'slide-backward'
return ''
})
const folderScrollTop = new Map<string, number>()
const scrollKey = (path: string) => path || '/'
const getMainScroller = () => document.querySelector('main') as HTMLElement | null
const restoreScroll = (path: string) => {
const scroller = getMainScroller()
if (!scroller) return
const top = folderScrollTop.get(scrollKey(path)) ?? 0
scroller.scrollTop = top
}
const onAfterEnter = () => {
store.transitionDirection = 'none'
restoreScroll(folderPath.value)
}
// Handle route-based search changes (back/forward navigation, direct URL)
// Skip if store.query already matches (means we triggered this via typing)
watch(
() => [props.query, props.path.join('/')] as const,
([query, loc]) => {
if (store.query === query) return // Already searching this query
store.search(query, loc)
},
{ immediate: true }
)
const documents = computed(() => {
const loc = props.path.join('/')
const query = props.query
// List the current location (no search)
if (!query) {
// Access docVersion to make this reactive to document changes
void store.docVersion
const hidden = store.hiddenPaths
const docs = getDocuments().filter(
doc =>
doc.loc === loc && !hidden.has(doc.loc ? `${doc.loc}/${doc.name}` : doc.name)
)
// Overlay ghosts for this location (excluding hidden ones)
const ghosts = store.ghosts.filter(
g => g.loc === loc && !hidden.has(g.loc ? `${g.loc}/${g.name}` : g.name)
)
// Merge: ghosts that don't conflict with real docs
const realNames = new Set(docs.map(d => d.name))
const merged = [...docs, ...ghosts.filter(g => !realNames.has(g.name))]
return sorted(merged, store.prefs.sortListing)
}
// Search results from worker (also filter hidden)
const hidden = store.hiddenPaths
const docs = store.searchResults.filter(
doc => !hidden.has(doc.loc ? `${doc.loc}/${doc.name}` : doc.name)
)
// Custom sort override in effect? Use grouped sorting to keep folders together
const order = store.prefs.sortFiltered
if (order) return sortedGrouped(docs, order)
// Results are already sorted by relevance in the worker
return docs
})
watchEffect(() => {
store.fileExplorer = fileExplorer.value
})
watch(
folderPath,
async (path, oldPath) => {
const scroller = getMainScroller()
if (scroller && oldPath !== undefined) {
folderScrollTop.set(scrollKey(oldPath), scroller.scrollTop)
}
await nextTick()
requestAnimationFrame(() => restoreScroll(path))
},
{ immediate: true }
)
// Only auto-switch gallery mode when entering a new folder or on initial file list load
watch(
[() => props.path.join('/'), () => store.documentCount],
([path, len], [oldPath, oldLen]) => {
// React to path change or initial document load (0 → non-zero)
if (path === oldPath && oldLen !== undefined && oldLen > 0) return
store.prefs.gallery = documents.value.some(d => d.previewable)
},
{ immediate: true }
)
</script>
<style scoped>
.search-loading {
position: fixed;
bottom: 1rem;
right: 1rem;
padding: 0.5rem 1rem;
background: var(--accent-color, #007bff);
color: white;
border-radius: 0.25rem;
font-size: 0.875rem;
opacity: 0.9;
}
</style>