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