feat(12-03): connection-ID routing, rename, session folder memory, thin views

- Route /cloud/:connectionId/:folderId replaces /cloud/:provider/:folderId
- api/cloud.js: getCloudFoldersByConnectionId + renameCloudConnection
- cloudConnections store: rename, selectConnection, setBrowseState, defaultDisplayName, session storage helpers
- CloudStorageView: thin — passes connectionRoots to StorageBrowser
- CloudFolderView: thin — uses connectionId param, never provider slug
- SettingsCloudTab: inline rename input for each active connection
- 27 tests pass: store, view, settings
This commit is contained in:
curo1305
2026-06-18 23:25:29 +02:00
parent 6f0ecfa39b
commit bf9af11274
10 changed files with 594 additions and 91 deletions
+70 -17
View File
@@ -7,7 +7,12 @@
:upload-queue="uploadQueue"
:loading="loading"
:empty-message="error || 'This folder is empty'"
:empty-hint="error ? '' : 'Files stored here are managed by ' + providerLabel(provider)"
:empty-hint="error ? '' : 'Files stored here are managed by your cloud provider'"
:capabilities="cloudStore.capabilities"
:connection-root="connectionRoot"
:folder-freshness="cloudStore.folderFreshness"
:last-refreshed-at="cloudStore.lastRefreshedAt"
:byte-availability="cloudStore.byteAvailability"
@breadcrumb-navigate="handleBreadcrumbNavigate"
@upload="onFilesSelected"
@folder-navigate="item => navigateTo(item)"
@@ -20,25 +25,37 @@ import { ref, computed, watch, onMounted, reactive } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import * as api from '../api/client.js'
import StorageBrowser from '../components/storage/StorageBrowser.vue'
import { providerLabel } from '../utils/formatters.js'
import { useToastStore } from '../stores/toast.js'
import { useCloudConnectionsStore } from '../stores/cloudConnections.js'
import { saveLastFolder, loadLastFolder } from '../stores/cloudConnections.js'
const route = useRoute()
const router = useRouter()
const toast = useToastStore()
const cloudStore = useCloudConnectionsStore()
const items = ref([])
const loading = ref(true)
const error = ref('')
const uploadQueue = ref([])
const provider = computed(() => route.params.provider)
/** Connection UUID from route — never uses provider slug */
const connectionId = computed(() => route.params.connectionId)
const folderId = computed(() => route.params.folderId)
const folders = computed(() => items.value.filter(i => i.is_dir))
const files = computed(() => items.value.filter(i => !i.is_dir))
/** Breadcrumb built from the folder path segments. */
const connectionRoot = computed(() => {
const conn = cloudStore.connections.find(c => c.id === connectionId.value)
if (!conn) return null
return {
id: conn.id,
name: conn.display_name || cloudStore.defaultDisplayName(conn),
provider: conn.provider,
}
})
const breadcrumb = computed(() => {
if (!folderId.value || folderId.value === 'root') return []
const parts = folderId.value.replace(/\/$/, '').split('/')
@@ -55,39 +72,54 @@ const mappedBreadcrumb = computed(() =>
async function load() {
loading.value = true
error.value = ''
cloudStore.setBrowseState({ freshness: 'refreshing' })
try {
const data = await api.getCloudFolders(provider.value, folderId.value ?? 'root')
const data = await api.getCloudFoldersByConnectionId(
connectionId.value,
folderId.value ?? 'root'
)
items.value = data.items ?? []
cloudStore.setBrowseState({
items: items.value,
caps: data.capabilities ?? null,
freshness: 'fresh',
refreshedAt: new Date().toISOString(),
byteAvail: data.byte_availability ?? null,
err: null,
})
} catch (e) {
const msg = e.message || ''
if (msg.toLowerCase().includes('no active connection') || msg.includes('404') || msg.toLowerCase().includes('not found')) {
if (
msg.toLowerCase().includes('no active connection') ||
msg.includes('404') ||
msg.toLowerCase().includes('not found')
) {
error.value = 'No cloud provider connected. Go to Settings to connect a cloud storage account.'
} else {
error.value = msg || 'Failed to load folder contents.'
}
cloudStore.setBrowseState({ freshness: 'stale', err: error.value })
} finally {
loading.value = false
}
// Save last folder in sessionStorage (folder refs only — no tokens/credentials)
saveLastFolder(connectionId.value, folderId.value)
}
function navigateTo(item) {
router.push(`/cloud/${provider.value}/${item.id}`)
router.push(`/cloud/${connectionId.value}/${item.id}`)
}
function handleBreadcrumbNavigate(id) {
if (id == null) router.push(`/cloud/${provider.value}/root`)
else router.push(`/cloud/${provider.value}/${id}`)
if (id == null) router.push(`/cloud/${connectionId.value}/root`)
else router.push(`/cloud/${connectionId.value}/${id}`)
}
// ── Upload ────────────────────────────────────────────────────────────────────
const uploadQueue = ref([])
async function onFilesSelected({ files: selectedFiles }) {
const promises = selectedFiles.map(file => {
const item = reactive({ name: file.name, done: false, error: null, status: 'Uploading…' })
uploadQueue.value.unshift(item)
return api.uploadToCloud(file, provider.value, folderId.value || null)
return api.uploadToCloud(file, connectionId.value, folderId.value || null)
.then(() => { item.done = true; item.status = null })
.catch(e => { item.error = e.message || 'Upload failed' })
})
@@ -99,6 +131,27 @@ function onFileOpen(file) {
toast.show(`"${file.name}" can't be opened here — open it directly in your cloud storage provider.`, 'info')
}
onMounted(load)
watch([provider, folderId], load)
onMounted(async () => {
// Ensure connections are loaded so connectionRoot resolves
if (cloudStore.connections.length === 0) {
await cloudStore.fetchConnections()
}
cloudStore.selectConnection(connectionId.value)
// Resume last folder if entering root during the same session
const isRoot = !folderId.value || folderId.value === 'root'
if (isRoot) {
const last = loadLastFolder(connectionId.value)
if (last) {
router.replace(`/cloud/${connectionId.value}/${last}`)
return
}
}
load()
})
watch([connectionId, folderId], () => {
cloudStore.selectConnection(connectionId.value)
load()
})
</script>