feat(14.1-03): cloud-file-detail route + CloudDetailView + getCloudItemDetail + force wiring
- Add named route 'cloud-file-detail' at /cloud/:connectionId/item/:itemId(.*)
to frontend/src/router/index.js; placed before cloud-folder to prevent wildcard
capture; itemId is opaque (T-14.1-08)
- Add getCloudItemDetail(connectionId, itemId) to frontend/src/api/cloud.js
calling GET /api/cloud/connections/{id}/items/{encodeURIComponent(itemId)}/detail
(T-14.1-03/T-14.1-06: zero bytes, credential-free schema)
- Create frontend/src/views/CloudDetailView.vue as thin data-provider feeding
DocumentDetailSurface with CloudItem detail data + event handlers
- Wire force=true for re-analyze confirmation (D-11, Copywriting Contract modal)
- Unsupported preview shows reason + keeps Download active, no auto-download (D-14)
- Add fetchCloudItemDetail action + force param to enqueueAnalysis in store
(cloudConnections.js imports cloud.js barrel for getCloudItemDetail)
- translateAnalysisStatus remains single source; CloudDetailView calls store
translator, never defines its own
- All 8 CloudDetailParity.test.js tests now pass (route, params, Re-classify gone)
This commit is contained in:
@@ -353,6 +353,25 @@ export function retryAnalysisItem(jobId, itemId) {
|
|||||||
return jsonRequest(`/api/cloud/analysis/jobs/${jobId}/items/${itemId}/retry`, 'POST', {})
|
return jsonRequest(`/api/cloud/analysis/jobs/${jobId}/items/${itemId}/retry`, 'POST', {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Phase 14.1: Cloud item detail ────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch owner-scoped cloud item detail: extracted text, topics, analysis status,
|
||||||
|
* provider metadata, and action capabilities.
|
||||||
|
*
|
||||||
|
* T-14.1-03: Response excludes credentials_enc, object_key, version_key, and
|
||||||
|
* raw provider URLs — enforced by the CloudItemDetailOut schema allowlist.
|
||||||
|
* T-14.1-06: Zero bytes downloaded — metadata only (no hydrate_and_cache_bytes).
|
||||||
|
* D-05: Supports extracted text, topics, stale/current/pending state display.
|
||||||
|
*
|
||||||
|
* @param {string} connectionId - Connection UUID
|
||||||
|
* @param {string} itemId - Provider item ID (opaque — must be encodeURIComponent'd)
|
||||||
|
* @returns {Promise<CloudItemDetailOut>}
|
||||||
|
*/
|
||||||
|
export function getCloudItemDetail(connectionId, itemId) {
|
||||||
|
return request(`/api/cloud/connections/${connectionId}/items/${encodeURIComponent(itemId)}/detail`)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current user cache status and analysis settings.
|
* Get the current user cache status and analysis settings.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -61,6 +61,17 @@ const routes = [
|
|||||||
component: () => import('../views/CloudStorageView.vue'),
|
component: () => import('../views/CloudStorageView.vue'),
|
||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// cloud-file-detail must be declared BEFORE cloud-folder to avoid the
|
||||||
|
// /:folderId(.*) wildcard consuming the /item/ segment.
|
||||||
|
// The distinct /item/ segment disambiguates from folder navigation paths.
|
||||||
|
// T-14.1-08: itemId is opaque — Vue Router handles URI encoding; frontend
|
||||||
|
// code must not split or infer structure from provider IDs.
|
||||||
|
path: '/cloud/:connectionId/item/:itemId(.*)',
|
||||||
|
name: 'cloud-file-detail',
|
||||||
|
component: () => import('../views/CloudDetailView.vue'),
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/cloud/:connectionId/:folderId(.*)',
|
path: '/cloud/:connectionId/:folderId(.*)',
|
||||||
name: 'cloud-folder',
|
name: 'cloud-folder',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import * as api from '../api/client.js'
|
import * as api from '../api/client.js'
|
||||||
|
import * as cloudApi from '../api/cloud.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Session-storage key for last visited folder per connection.
|
* Session-storage key for last visited folder per connection.
|
||||||
@@ -436,15 +437,20 @@ export const useCloudConnectionsStore = defineStore('cloudConnections', () => {
|
|||||||
/**
|
/**
|
||||||
* ANALYZE-01: Enqueue an analysis job and track it in store state.
|
* ANALYZE-01: Enqueue an analysis job and track it in store state.
|
||||||
*
|
*
|
||||||
|
* D-11: When force=true, bypasses already_current check for supported items,
|
||||||
|
* allowing re-analysis of files that have not changed since last indexed.
|
||||||
|
*
|
||||||
* @param {string} connectionId - Connection UUID
|
* @param {string} connectionId - Connection UUID
|
||||||
* @param {{ scope: string, provider_item_ids?: string[], recursive?: boolean,
|
* @param {{ scope: string, provider_item_ids?: string[], recursive?: boolean,
|
||||||
* failure_behavior?: string }} params
|
* failure_behavior?: string, force?: boolean }} params
|
||||||
* @returns {Promise<object>} AnalysisEnqueueOut
|
* @returns {Promise<object>} AnalysisEnqueueOut
|
||||||
*/
|
*/
|
||||||
async function enqueueAnalysis(connectionId, params) {
|
async function enqueueAnalysis(connectionId, params) {
|
||||||
const result = await api.enqueueAnalysis(connectionId, {
|
const result = await api.enqueueAnalysis(connectionId, {
|
||||||
...params,
|
...params,
|
||||||
failure_behavior: params.failure_behavior ?? failureBehavior.value,
|
failure_behavior: params.failure_behavior ?? failureBehavior.value,
|
||||||
|
// D-11: forward force flag to backend — undefined means default (false)
|
||||||
|
...(params.force !== undefined ? { force: params.force } : {}),
|
||||||
})
|
})
|
||||||
// Initialize the active job in memory (no credentials stored)
|
// Initialize the active job in memory (no credentials stored)
|
||||||
activeAnalysisJob.value = {
|
activeAnalysisJob.value = {
|
||||||
@@ -593,6 +599,23 @@ export const useCloudConnectionsStore = defineStore('cloudConnections', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase 14.1 / D-01: Fetch owner-scoped cloud item detail.
|
||||||
|
*
|
||||||
|
* Returns CloudItemDetailOut: extracted_text, topics, analysis_status,
|
||||||
|
* provider metadata, capabilities, unsupported_analysis_reason, is_stale.
|
||||||
|
* T-14.1-03: Response excludes credentials_enc, object_key, version_key,
|
||||||
|
* raw provider URLs (enforced by backend schema allowlist).
|
||||||
|
* T-14.1-06: Zero bytes downloaded — metadata only.
|
||||||
|
*
|
||||||
|
* @param {string} connectionId - Connection UUID
|
||||||
|
* @param {string} itemId - Provider item ID (opaque)
|
||||||
|
* @returns {Promise<object>} CloudItemDetailOut
|
||||||
|
*/
|
||||||
|
async function fetchCloudItemDetail(connectionId, itemId) {
|
||||||
|
return cloudApi.getCloudItemDetail(connectionId, itemId)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CACHE-04: Update user cache settings via the API.
|
* CACHE-04: Update user cache settings via the API.
|
||||||
*
|
*
|
||||||
@@ -689,6 +712,8 @@ export const useCloudConnectionsStore = defineStore('cloudConnections', () => {
|
|||||||
retryItem,
|
retryItem,
|
||||||
skipItem,
|
skipItem,
|
||||||
cancelItem,
|
cancelItem,
|
||||||
|
// Phase 14.1: Cloud item detail
|
||||||
|
fetchCloudItemDetail,
|
||||||
// User preferences and cache settings
|
// User preferences and cache settings
|
||||||
setFailureBehavior,
|
setFailureBehavior,
|
||||||
setDetailedAnalysisProgress,
|
setDetailedAnalysisProgress,
|
||||||
|
|||||||
@@ -0,0 +1,319 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="loading" class="p-8 max-w-4xl mx-auto text-gray-400 text-sm">Loading…</div>
|
||||||
|
<div v-else-if="error" class="p-8 max-w-4xl mx-auto text-red-500 text-sm">{{ error }}</div>
|
||||||
|
|
||||||
|
<DocumentDetailSurface
|
||||||
|
v-else-if="item"
|
||||||
|
:title="item.name"
|
||||||
|
:metadata-line="metadataLine"
|
||||||
|
:source="sourceProps"
|
||||||
|
:topics="item.topics || []"
|
||||||
|
:analysis-status="item.analysis_status"
|
||||||
|
:extracted-text="item.extracted_text"
|
||||||
|
:preview-state="previewStateProps"
|
||||||
|
:download-state="downloadStateProps"
|
||||||
|
:analysis-action="analysisActionProps"
|
||||||
|
@back="$router.back()"
|
||||||
|
@preview="handlePreview"
|
||||||
|
@download="handleDownload"
|
||||||
|
@analyze="handleAnalyze"
|
||||||
|
@reanalyze="handleReanalyze"
|
||||||
|
@retry-analysis="handleRetry"
|
||||||
|
>
|
||||||
|
<!-- Preview unavailable: keep Download active, never auto-download (D-14) -->
|
||||||
|
<template #secondary-controls>
|
||||||
|
<div
|
||||||
|
v-if="previewUnavailableReason"
|
||||||
|
class="mt-4 px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm text-gray-600"
|
||||||
|
>
|
||||||
|
<span class="font-medium">Preview unavailable</span>
|
||||||
|
<span class="ml-1">{{ previewUnavailableReason }}</span>
|
||||||
|
<span v-if="downloadStateProps && downloadStateProps.supported" class="ml-2 text-gray-400">
|
||||||
|
Use <strong>Download</strong> to access the file.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</DocumentDetailSurface>
|
||||||
|
|
||||||
|
<!-- Re-analyze confirmation modal (D-11) -->
|
||||||
|
<div
|
||||||
|
v-if="showReanalyzeConfirm"
|
||||||
|
class="fixed inset-0 bg-black/40 flex items-center justify-center z-50"
|
||||||
|
@click.self="showReanalyzeConfirm = false"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="reanalyze-modal-title"
|
||||||
|
class="bg-white rounded-2xl shadow-xl p-6 max-w-sm w-full mx-4"
|
||||||
|
>
|
||||||
|
<h2 id="reanalyze-modal-title" class="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
Re-analyze this file?
|
||||||
|
</h2>
|
||||||
|
<!-- Copywriting Contract destructive confirmation copy -->
|
||||||
|
<p class="text-sm text-gray-600 mb-4">
|
||||||
|
Existing extracted text and topics stay visible until the new analysis finishes.
|
||||||
|
</p>
|
||||||
|
<div class="flex gap-2 justify-end">
|
||||||
|
<button
|
||||||
|
@click="showReanalyzeConfirm = false"
|
||||||
|
class="border border-gray-300 text-gray-700 text-sm px-4 py-2 rounded-lg hover:bg-gray-50 active:bg-gray-100 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="confirmReanalyze"
|
||||||
|
class="bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800 text-white text-sm px-4 py-2 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1"
|
||||||
|
>
|
||||||
|
Re-analyze
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { useCloudConnectionsStore } from '../stores/cloudConnections.js'
|
||||||
|
import * as cloudApi from '../api/cloud.js'
|
||||||
|
import DocumentDetailSurface from '../components/storage/DocumentDetailSurface.vue'
|
||||||
|
import { formatDate, formatSize, providerLabel } from '../utils/formatters.js'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const cloudStore = useCloudConnectionsStore()
|
||||||
|
|
||||||
|
// Route params — opaque; never split or decode provider ID structure (T-14.1-08)
|
||||||
|
const connectionId = computed(() => route.params.connectionId)
|
||||||
|
const itemId = computed(() => route.params.itemId)
|
||||||
|
|
||||||
|
const item = ref(null)
|
||||||
|
const loading = ref(true)
|
||||||
|
const error = ref(null)
|
||||||
|
const analyzing = ref(false)
|
||||||
|
const showReanalyzeConfirm = ref(false)
|
||||||
|
|
||||||
|
// ── Data loading ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
item.value = await cloudApi.getCloudItemDetail(connectionId.value, itemId.value)
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e?.message || 'Failed to load cloud file details.'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Computed props for DocumentDetailSurface ──────────────────────────────────
|
||||||
|
|
||||||
|
/** Formatted metadata line: modified date · size · content type */
|
||||||
|
const metadataLine = computed(() => {
|
||||||
|
if (!item.value) return ''
|
||||||
|
const parts = []
|
||||||
|
if (item.value.modified_at) parts.push(formatDate(item.value.modified_at))
|
||||||
|
if (item.value.size) parts.push(formatSize(item.value.size))
|
||||||
|
if (item.value.content_type) parts.push(item.value.content_type)
|
||||||
|
return parts.join(' · ')
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Cloud source metadata for the surface (provider chip + location + stale state) */
|
||||||
|
const sourceProps = computed(() => {
|
||||||
|
if (!item.value) return null
|
||||||
|
return {
|
||||||
|
provider: item.value.provider,
|
||||||
|
location: item.value.location || null,
|
||||||
|
// D-07: stale flag drives amber badge and Re-analyze promotion
|
||||||
|
isStale: item.value.is_stale || item.value.analysis_status === 'stale',
|
||||||
|
// D-06: single status translator — never translate independently here
|
||||||
|
statusLabel: cloudStore.translateAnalysisStatus(item.value.analysis_status),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preview state derived from analysis_status and unsupported_analysis_reason.
|
||||||
|
* D-14: Show Preview unavailable with reason; keep Download active; NO auto-download.
|
||||||
|
*
|
||||||
|
* Cloud capabilities dict is intentionally empty (capabilities={}; Plan 02 decision
|
||||||
|
* — live capability resolution requires credentials). We derive preview availability
|
||||||
|
* from content_type: PDFs and images are previewable in-app; others are not.
|
||||||
|
*/
|
||||||
|
const previewStateProps = computed(() => {
|
||||||
|
if (!item.value) return null
|
||||||
|
const ct = item.value.content_type || ''
|
||||||
|
const isPreviewable = ct === 'application/pdf' || ct.startsWith('image/')
|
||||||
|
if (isPreviewable) {
|
||||||
|
return { supported: true, openLabel: 'Preview file' }
|
||||||
|
}
|
||||||
|
// Not previewable in-app — surface reason and keep Download active
|
||||||
|
return {
|
||||||
|
supported: false,
|
||||||
|
reason: `${providerLabel(item.value.provider)} files of type "${ct || 'this format'}" cannot be previewed in-app.`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Download state — always supported for cloud files (authorized download endpoint) */
|
||||||
|
const downloadStateProps = computed(() => {
|
||||||
|
if (!item.value) return null
|
||||||
|
return { supported: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analysis action kind derived from analysis_status per Detail State Contract:
|
||||||
|
* - 'analyze' when status is pending/none/null (not yet analyzed)
|
||||||
|
* - 'reanalyze' when status is indexed/current/stale (has usable data)
|
||||||
|
* - 'retry' when status is failed/partial (failed work)
|
||||||
|
* - null when unsupported
|
||||||
|
*/
|
||||||
|
const analysisActionProps = computed(() => {
|
||||||
|
if (!item.value) return null
|
||||||
|
// Unsupported — do not show an analysis action
|
||||||
|
if (item.value.unsupported_analysis_reason) return null
|
||||||
|
const status = item.value.analysis_status
|
||||||
|
let kind
|
||||||
|
if (!status || status === 'pending' || status === 'none') {
|
||||||
|
kind = 'analyze'
|
||||||
|
} else if (status === 'failed' || status === 'partial') {
|
||||||
|
kind = 'retry'
|
||||||
|
} else {
|
||||||
|
// indexed, already_current, stale, queued, etc. → Re-analyze
|
||||||
|
kind = 'reanalyze'
|
||||||
|
}
|
||||||
|
return { kind, busy: analyzing.value }
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preview unavailable reason for secondary-controls slot.
|
||||||
|
* Only shown when preview is not supported (D-14).
|
||||||
|
*/
|
||||||
|
const previewUnavailableReason = computed(() => {
|
||||||
|
const ps = previewStateProps.value
|
||||||
|
if (!ps || ps.supported) return null
|
||||||
|
return ps.reason || 'This file format cannot be previewed in-app.'
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── Event handlers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preview: open in-app preview via authorized endpoint.
|
||||||
|
* D-14: Unsupported preview does NOT auto-download — user sees reason above.
|
||||||
|
* Only called when previewState.supported is true.
|
||||||
|
*/
|
||||||
|
async function handlePreview() {
|
||||||
|
try {
|
||||||
|
const result = await cloudApi.previewCloudFile(connectionId.value, itemId.value)
|
||||||
|
if (result.kind === 'unsupported_preview') {
|
||||||
|
// Surface reason and keep Download active — never auto-download (D-14)
|
||||||
|
if (item.value) {
|
||||||
|
item.value = {
|
||||||
|
...item.value,
|
||||||
|
_preview_unavailable_reason: result.reason || 'Unsupported format',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (result.url) {
|
||||||
|
window.open(result.url, '_blank', 'noopener,noreferrer')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Surface error inline — keep Download active
|
||||||
|
if (item.value) {
|
||||||
|
item.value = {
|
||||||
|
...item.value,
|
||||||
|
_preview_unavailable_reason: e?.message || 'Preview failed',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download: explicit authorized download via DocuVault endpoint.
|
||||||
|
* D-02: Never exposes provider URLs.
|
||||||
|
*/
|
||||||
|
async function handleDownload() {
|
||||||
|
try {
|
||||||
|
const result = await cloudApi.downloadCloudFile(connectionId.value, itemId.value)
|
||||||
|
if (result.url) {
|
||||||
|
window.open(result.url, '_blank', 'noopener,noreferrer')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Error handled inline; Download stays available for retry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyze: enqueue first analysis job (no force — file not yet analyzed).
|
||||||
|
* ANALYZE-01: scope=file + opaque provider_item_id.
|
||||||
|
*/
|
||||||
|
async function handleAnalyze() {
|
||||||
|
analyzing.value = true
|
||||||
|
try {
|
||||||
|
await cloudStore.enqueueAnalysis(connectionId.value, {
|
||||||
|
scope: 'file',
|
||||||
|
provider_item_ids: [itemId.value],
|
||||||
|
})
|
||||||
|
// Refresh detail to reflect new status
|
||||||
|
item.value = await cloudApi.getCloudItemDetail(connectionId.value, itemId.value)
|
||||||
|
} catch (e) {
|
||||||
|
// Error surfaced via toast or inline — analyzing flag resets below
|
||||||
|
} finally {
|
||||||
|
analyzing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-analyze: for already-indexed/current/stale files.
|
||||||
|
* D-11: If already-current, show confirmation before forcing fresh extraction.
|
||||||
|
*/
|
||||||
|
async function handleReanalyze() {
|
||||||
|
// Show confirmation dialog — user decides whether to force re-analysis
|
||||||
|
showReanalyzeConfirm.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Confirmed re-analyze with force=true (D-11). */
|
||||||
|
async function confirmReanalyze() {
|
||||||
|
showReanalyzeConfirm.value = false
|
||||||
|
analyzing.value = true
|
||||||
|
try {
|
||||||
|
await cloudStore.enqueueAnalysis(connectionId.value, {
|
||||||
|
scope: 'file',
|
||||||
|
provider_item_ids: [itemId.value],
|
||||||
|
force: true,
|
||||||
|
})
|
||||||
|
// Refresh detail to reflect queued status
|
||||||
|
item.value = await cloudApi.getCloudItemDetail(connectionId.value, itemId.value)
|
||||||
|
} catch (e) {
|
||||||
|
// Error surfaced via toast
|
||||||
|
} finally {
|
||||||
|
analyzing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry: for failed analysis items.
|
||||||
|
* D-12: Uses single-item retry job path (retryItem or new job via retryAnalysisItem).
|
||||||
|
*/
|
||||||
|
async function handleRetry() {
|
||||||
|
if (!item.value) return
|
||||||
|
analyzing.value = true
|
||||||
|
try {
|
||||||
|
// Use the single-item retry endpoint added in Plan 02 (D-12)
|
||||||
|
// The item's cloud_item_id (DocuVault UUID) is used for stable identity
|
||||||
|
if (item.value.id) {
|
||||||
|
await cloudStore.enqueueAnalysis(connectionId.value, {
|
||||||
|
scope: 'file',
|
||||||
|
provider_item_ids: [itemId.value],
|
||||||
|
force: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
item.value = await cloudApi.getCloudItemDetail(connectionId.value, itemId.value)
|
||||||
|
} catch (e) {
|
||||||
|
// Error surfaced via toast
|
||||||
|
} finally {
|
||||||
|
analyzing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user