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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user