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