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
+124 -2
View File
@@ -1,12 +1,57 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { ref, computed } from 'vue'
import * as api from '../api/client.js'
/**
* Session-storage key for last visited folder per connection.
* Only folder navigation state is stored — never tokens or credentials.
*/
function folderSessionKey(connectionId) {
return `docuvault:cloud:folder:${connectionId}`
}
export function saveLastFolder(connectionId, folderId) {
try {
if (folderId && folderId !== 'root') {
sessionStorage.setItem(folderSessionKey(connectionId), folderId)
} else {
sessionStorage.removeItem(folderSessionKey(connectionId))
}
} catch {
// sessionStorage unavailable — ignore
}
}
export function loadLastFolder(connectionId) {
try {
return sessionStorage.getItem(folderSessionKey(connectionId)) || null
} catch {
return null
}
}
export const useCloudConnectionsStore = defineStore('cloudConnections', () => {
const connections = ref([])
const loading = ref(false)
const error = ref(null)
// Active browse state (populated by CloudFolderView)
const selectedConnectionId = ref(null)
const browseItems = ref([])
const browseLoading = ref(false)
const browseError = ref(null)
/** Capabilities for the currently selected connection */
const capabilities = ref(null)
/** 'refreshing' | 'fresh' | 'stale' | null */
const folderFreshness = ref(null)
const lastRefreshedAt = ref(null)
/** 'cached' | 'cloud_only' | null */
const byteAvailability = ref(null)
const selectedConnection = computed(() =>
connections.value.find(c => c.id === selectedConnectionId.value) ?? null
)
async function fetchConnections() {
loading.value = true
error.value = null
@@ -24,6 +69,9 @@ export const useCloudConnectionsStore = defineStore('cloudConnections', () => {
try {
await api.disconnectCloud(id)
connections.value = connections.value.filter(c => c.id !== id)
if (selectedConnectionId.value === id) {
selectedConnectionId.value = null
}
} catch (e) {
throw e
}
@@ -35,5 +83,79 @@ export const useCloudConnectionsStore = defineStore('cloudConnections', () => {
connections.value = []
}
return { connections, loading, error, fetchConnections, disconnect, disconnectAll }
/**
* Rename a connection's display_name via the API.
* Only updates the single returned connection in state.
*/
async function rename(id, displayName) {
const trimmed = displayName?.trim()
if (!trimmed) throw new Error('Display name cannot be empty')
const updated = await api.renameCloudConnection(id, trimmed)
const idx = connections.value.findIndex(c => c.id === id)
if (idx !== -1) {
connections.value[idx] = { ...connections.value[idx], ...updated }
}
return updated
}
/**
* Default display_name for a connection.
* If another connection shares the same provider, append a 4-char UUID suffix.
*/
function defaultDisplayName(connection) {
const providerLabel = {
google_drive: 'Google Drive',
onedrive: 'OneDrive',
nextcloud: 'Nextcloud',
webdav: 'WebDAV',
}[connection.provider] ?? connection.provider
const siblings = connections.value.filter(
c => c.provider === connection.provider && c.id !== connection.id
)
if (siblings.length > 0) {
return `${providerLabel} (${connection.id.slice(0, 4)})`
}
return providerLabel
}
function selectConnection(id) {
selectedConnectionId.value = id
browseItems.value = []
browseError.value = null
capabilities.value = null
folderFreshness.value = null
lastRefreshedAt.value = null
byteAvailability.value = null
}
function setBrowseState({ items, caps, freshness, refreshedAt, byteAvail, err }) {
if (err !== undefined) browseError.value = err
if (items !== undefined) browseItems.value = items
if (caps !== undefined) capabilities.value = caps
if (freshness !== undefined) folderFreshness.value = freshness
if (refreshedAt !== undefined) lastRefreshedAt.value = refreshedAt
if (byteAvail !== undefined) byteAvailability.value = byteAvail
}
return {
connections,
loading,
error,
selectedConnectionId,
selectedConnection,
browseItems,
browseLoading,
browseError,
capabilities,
folderFreshness,
lastRefreshedAt,
byteAvailability,
fetchConnections,
disconnect,
disconnectAll,
rename,
defaultDisplayName,
selectConnection,
setBrowseState,
}
})