feat(14-07): extend StorageBrowser and CloudFolderView with analysis UX (Task 2)

StorageBrowser:
- Add analyze-file button per cloud file row (D-01); placed inside name cell
  to avoid interfering with capability-gated file-row-actions (Rule 1 compliance)
- Add file-select checkboxes for multi-select analysis (cloud mode only)
- Add analyze-selection toolbar button (D-01, ANALYZE-02)
- Add analyze-folder toolbar button when breadcrumb is non-empty (D-01)
- Add analyze-connection toolbar button when connectionRoot is set (D-01, ANALYZE-03)
- Add analysisEstimate prop + analysis-estimate-modal (D-03) with estimate-start button
- Add analysisQueue prop + analysis-aggregate-progress with simplified/detailed labels (D-05/D-06)
- Add expandable per-item list via analysis-queue-expand toggle (D-05)
- Add per-item cancel/retry/skip controls in expanded list (D-09, ANALYZE-05)
- Add Cancel all batch button (D-09, ANALYZE-05)
- Emit: analyze-file, analyze-selection, analyze-folder, analyze-connection,
  analysis-confirmed, analysis-cancelled, analysis-item-cancel/retry/skip, analysis-cancel-all

CloudFolderView:
- Pass analysis props to StorageBrowser from cloudStore
- Add analysis estimate, pending request state
- Handle all analysis emits: onAnalyzeFile, onAnalyzeSelection, onAnalyzeFolder,
  onAnalyzeConnection, onAnalysisConfirmed, onAnalysisItemCancel/Retry/Skip, onAnalysisCancelAll
- Routes all API calls through cloudStore (requestEstimate, enqueueAnalysis, etc.)
This commit is contained in:
curo1305
2026-06-23 19:42:53 +02:00
parent 1291040f9b
commit 04e6ea2ba2
2 changed files with 569 additions and 2 deletions
+196
View File
@@ -13,11 +13,24 @@
:folder-freshness="cloudStore.folderFreshness"
:last-refreshed-at="cloudStore.lastRefreshedAt"
:byte-availability="cloudStore.byteAvailability"
:analysis-estimate="analysisEstimate"
:analysis-queue="cloudStore.analysisQueue"
:detailed-analysis-progress="cloudStore.detailedAnalysisProgress"
@breadcrumb-navigate="handleBreadcrumbNavigate"
@upload="onFilesSelected"
@folder-navigate="item => navigateTo(item)"
@file-open="onFileOpen"
@upload-queue-resolve="onQueueResolve"
@analyze-file="onAnalyzeFile"
@analyze-selection="onAnalyzeSelection"
@analyze-folder="onAnalyzeFolder"
@analyze-connection="onAnalyzeConnection"
@analysis-confirmed="onAnalysisConfirmed"
@analysis-cancelled="analysisEstimate = null"
@analysis-item-cancel="onAnalysisItemCancel"
@analysis-item-retry="onAnalysisItemRetry"
@analysis-item-skip="onAnalysisItemSkip"
@analysis-cancel-all="onAnalysisCancelAll"
/>
</template>
@@ -40,6 +53,23 @@ const loading = ref(true)
const error = ref('')
const uploadQueue = ref([])
// ── Phase 14: Analysis state ──────────────────────────────────────────────────
/**
* Pending analysis estimate to show in the review modal.
* When set, StorageBrowser renders the estimate modal for confirmation.
* After confirmation/cancellation, set back to null.
*
* T-14-02: Never contains credentials or object_key.
*/
const analysisEstimate = ref(null)
/**
* Pending analysis request context — stored so onAnalysisConfirmed
* can enqueue the job with the right scope/items after estimate review.
*/
const analysisPending = ref(null)
/** Connection UUID from route — never uses provider slug */
const connectionId = computed(() => route.params.connectionId)
const folderId = computed(() => route.params.folderId)
@@ -408,6 +438,172 @@ async function onFileOpen(file) {
}
}
// ── Phase 14: Analysis handlers ───────────────────────────────────────────────
/**
* ANALYZE-01 / D-01: Handle analyze-file emission from StorageBrowser.
* Calls estimate for a single file; shows estimate modal when result is non-trivial.
*
* @param {object} file - CloudItemOut with provider_item_id
*/
async function onAnalyzeFile(file) {
if (!file?.provider_item_id) return
try {
const estimate = await cloudStore.requestEstimate(connectionId.value, {
scope: 'file',
provider_item_ids: [file.provider_item_id],
recursive: false,
})
analysisPending.value = {
scope: 'file',
provider_item_ids: [file.provider_item_id],
recursive: false,
}
// Show estimate modal — confirmed via onAnalysisConfirmed
analysisEstimate.value = estimate
} catch (e) {
toast.show(`Analysis estimate failed: ${e.message || 'Unknown error'}`, 'error')
}
}
/**
* ANALYZE-02 / D-01: Handle analyze-selection emission from StorageBrowser.
* Calls estimate for the selected files.
*
* @param {object[]} selectedFiles - Array of CloudItemOut items
*/
async function onAnalyzeSelection(selectedFiles) {
if (!selectedFiles?.length) return
try {
const providerItemIds = selectedFiles.map(f => f.provider_item_id).filter(Boolean)
const estimate = await cloudStore.requestEstimate(connectionId.value, {
scope: 'selection',
provider_item_ids: providerItemIds,
recursive: false,
})
analysisPending.value = {
scope: 'selection',
provider_item_ids: providerItemIds,
recursive: false,
}
analysisEstimate.value = estimate
} catch (e) {
toast.show(`Analysis estimate failed: ${e.message || 'Unknown error'}`, 'error')
}
}
/**
* ANALYZE-02 / D-01: Handle analyze-folder emission from StorageBrowser.
* Calls estimate for the current folder.
*/
async function onAnalyzeFolder() {
const folderRef = folderId.value && folderId.value !== 'root' ? folderId.value : null
if (!folderRef) return
try {
const estimate = await cloudStore.requestEstimate(connectionId.value, {
scope: 'folder',
provider_item_ids: [folderRef],
recursive: true,
})
analysisPending.value = {
scope: 'folder',
provider_item_ids: [folderRef],
recursive: true,
}
analysisEstimate.value = estimate
} catch (e) {
toast.show(`Analysis estimate failed: ${e.message || 'Unknown error'}`, 'error')
}
}
/**
* ANALYZE-03 / D-01: Handle analyze-connection emission from StorageBrowser.
* Calls estimate for the entire connection.
*/
async function onAnalyzeConnection() {
try {
const estimate = await cloudStore.requestEstimate(connectionId.value, {
scope: 'connection',
recursive: true,
})
analysisPending.value = {
scope: 'connection',
recursive: true,
}
analysisEstimate.value = estimate
} catch (e) {
toast.show(`Analysis estimate failed: ${e.message || 'Unknown error'}`, 'error')
}
}
/**
* D-03: Handle analysis-confirmed emission from StorageBrowser estimate modal.
* Enqueues the analysis job with the pending scope context.
*/
async function onAnalysisConfirmed() {
if (!analysisPending.value) return
const pending = analysisPending.value
analysisPending.value = null
analysisEstimate.value = null
try {
await cloudStore.enqueueAnalysis(connectionId.value, pending)
} catch (e) {
toast.show(`Failed to start analysis: ${e.message || 'Unknown error'}`, 'error')
}
}
/**
* ANALYZE-05 / D-09: Cancel a single analysis job item.
*
* @param {{ job_id: string, item_id: string }} payload
*/
async function onAnalysisItemCancel({ job_id, item_id }) {
try {
await cloudStore.cancelItem(job_id, item_id)
} catch (e) {
toast.show(`Failed to cancel item: ${e.message || 'Unknown error'}`, 'error')
}
}
/**
* ANALYZE-05 / D-09: Retry a failed analysis job item.
*
* @param {{ job_id: string, item_id: string }} payload
*/
async function onAnalysisItemRetry({ job_id, item_id }) {
try {
await cloudStore.retryItem(job_id, item_id)
} catch (e) {
toast.show(`Failed to retry item: ${e.message || 'Unknown error'}`, 'error')
}
}
/**
* ANALYZE-05 / D-09: Skip a failed analysis job item.
*
* @param {{ job_id: string, item_id: string }} payload
*/
async function onAnalysisItemSkip({ job_id, item_id }) {
try {
await cloudStore.skipItem(job_id, item_id)
} catch (e) {
toast.show(`Failed to skip item: ${e.message || 'Unknown error'}`, 'error')
}
}
/**
* ANALYZE-05 / D-09: Cancel the entire analysis job.
*
* @param {{ job_id: string }} payload
*/
async function onAnalysisCancelAll({ job_id }) {
try {
await cloudStore.cancelJob(job_id)
} catch (e) {
toast.show(`Failed to cancel analysis: ${e.message || 'Unknown error'}`, 'error')
}
}
onMounted(async () => {
// Ensure connections are loaded so connectionRoot resolves
if (cloudStore.connections.length === 0) {