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