feat(12.1-03): align cloud browser contract — kind, provider_item_id, server freshness

- CloudFolderView: folders/files classified by kind=folder/file (remove is_dir)
- CloudFolderView: navigateTo uses item.provider_item_id for route param (not item.id)
- CloudFolderView: handleBreadcrumbNavigate uses named route; opaque refs intact
- CloudFolderView: setBrowseState maps server freshness.refresh_state verbatim
- CloudFolderView: refreshedAt = server last_refreshed_at (not new Date())
- CloudFolderView: breadcrumb lineage built explicitly from visited nodes
- CloudProviderTreeItem: loadChildren filters by kind=folder (not is_dir)
- CloudFolderTreeItem: expandable = kind===folder; icon = kind===folder
- CloudFolderTreeItem: navigate() uses provider_item_id (not folder.id)
- CloudFolderTreeItem: loadChildren uses provider_item_id as parent_ref
- CloudFolderTreeItem: isActive checks provider_item_id first
All 82 focused tests pass; full suite: 369/369
This commit is contained in:
curo1305
2026-06-22 08:49:19 +02:00
parent 9974fca2cb
commit dc3e1725da
4 changed files with 168 additions and 58 deletions
@@ -1,14 +1,14 @@
<template>
<TreeItem
:label="folder.name"
:expandable="folder.is_dir"
:expandable="folder.kind === 'folder'"
:load-children="loadChildren"
:depth="depth"
:is-active="isActive"
@select="navigate"
>
<template #icon>
<AppIcon v-if="folder.is_dir" name="folder" class="w-4 h-4 shrink-0 text-gray-400" />
<AppIcon v-if="folder.kind === 'folder'" name="folder" class="w-4 h-4 shrink-0 text-gray-400" />
<AppIcon v-else name="fileDoc" class="w-4 h-4 shrink-0 text-gray-400" />
</template>
<template #children="{ children }">
@@ -39,18 +39,40 @@ const props = defineProps({
const router = useRouter()
const route = useRoute()
/** Active when the current route points to this exact folder. */
/**
* Active when the current route points to this exact folder.
* Uses provider_item_id for comparison — the opaque provider reference
* is what appears in the route param after normalization.
*/
const isActive = computed(() => {
const provRef = props.folder.provider_item_id
if (provRef !== undefined && provRef !== null) {
return route.path === `/cloud/${props.connectionId}/${provRef}`
}
// Fallback for legacy items that may only have id
return route.path === `/cloud/${props.connectionId}/${props.folder.id}`
})
/**
* Load nested children using provider_item_id as the parent_ref.
* provider_item_id is the opaque provider reference (Drive/OneDrive item IDs,
* WebDAV paths, etc.) — never use the DocuVault stable id for provider navigation.
* Classify results by kind, not is_dir.
*/
async function loadChildren() {
// Browse by connection UUID + provider item/path reference — never provider slug
const data = await api.getCloudFoldersByConnectionId(props.connectionId, props.folder.id)
return (data.items ?? []).filter(i => i.is_dir)
// Use provider_item_id as the parent ref, falling back to id for legacy items
const parentRef = props.folder.provider_item_id ?? props.folder.id
const data = await api.getCloudFoldersByConnectionId(props.connectionId, parentRef)
return (data.items ?? []).filter(i => i.kind === 'folder')
}
/**
* Navigate using provider_item_id as the opaque route parameter.
* Reserved characters (/, ?, #, %, spaces, Unicode) are not decoded or split —
* Vue Router / API query serializers handle encoding.
*/
function navigate() {
router.push(`/cloud/${props.connectionId}/${props.folder.id}`)
const provRef = props.folder.provider_item_id ?? props.folder.id
router.push(`/cloud/${props.connectionId}/${provRef}`)
}
</script>
@@ -53,10 +53,15 @@ const isActive = computed(() => {
)
})
/**
* Load root-level folders using kind classification (not is_dir).
* Uses connection UUID, never provider slug.
* Root listing uses empty string as the sentinel for parent_ref=null.
*/
async function loadChildren() {
// Use connection UUID, never provider slug (UAT gap: 422 from UUID endpoint if slug passed)
const data = await api.getCloudFoldersByConnectionId(props.connection.id, '')
return (data.items ?? []).filter(i => i.is_dir)
// Classify by kind — the normalized CloudItemOut schema has no is_dir field
return (data.items ?? []).filter(i => i.kind === 'folder')
}
function navigateToRoot() {