diff --git a/src/MediaManager.html b/src/MediaManager.html index d798d3c..f30a94e 100644 --- a/src/MediaManager.html +++ b/src/MediaManager.html @@ -1975,10 +1975,48 @@ } }; + // Optimistic Sheet Update (Immediate Parallel Execution) + const pSheet = new Promise(resolve => { + const first = activeItems[0]; + let url = null; + if (first) { + // Determine best URL based on item type + // Note: MediaHandlers logic prioritizes Shopify URL if present. + // We mimic that logic here. + const isShopify = (first.source === 'shopify_only' || first.source === 'synced') && first.thumbnail; + + if (isShopify) { + url = first.thumbnail; + } else if (first.id) { + // Drive Item - Construct URL compatible with server logic + url = "https://drive.google.com/thumbnail?id=" + first.id + "&sz=w400"; + } + } + + if (!url) { + updateStatus('prog-sheet', 'Skipped', '#aaa'); + resolve(); + return; + } + + updateStatus('prog-sheet', 'Running...', 'blue'); + google.script.run + .withSuccessHandler(() => { + updateStatus('prog-sheet', 'Done', 'green'); + resolve(); + }) + .withFailureHandler(e => { + console.warn("Sheet update failed", e); + updateStatus('prog-sheet', 'Failed (Warn)', 'orange'); + resolve(); // Don't block completion + }) + .updateSpreadsheetThumbnail(state.sku, url); + }); + // Phase 1: Deletions if (plan.deletions.length === 0) { updateStatus('prog-delete', 'Skipped', '#aaa'); - this.startParallelPhases(plan, activeItems, jobId); + this.startParallelPhases(plan, activeItems, jobId, pSheet); return; } @@ -1987,7 +2025,7 @@ google.script.run .withSuccessHandler(() => { updateStatus('prog-delete', 'Done', 'green'); - this.startParallelPhases(plan, activeItems, jobId); + this.startParallelPhases(plan, activeItems, jobId, pSheet); }) .withFailureHandler(e => { updateStatus('prog-delete', 'Failed', 'red'); @@ -1998,7 +2036,7 @@ .executeSavePhase(state.sku, 'deletions', plan.deletions, jobId); }, - startParallelPhases(plan, activeItems, jobId) { + startParallelPhases(plan, activeItems, jobId, pSheet) { // Helper function defined at the top to avoid hoisting issues const updateStatus = (id, status, color) => { const el = document.getElementById(id); @@ -2009,7 +2047,6 @@ }; // Phase 2 & 3: Adoptions & Uploads (Parallel) - const pAdoption = new Promise((resolve, reject) => { if (plan.adoptions.length === 0) { updateStatus('prog-adopt', 'Skipped', '#aaa'); @@ -2043,58 +2080,44 @@ Promise.all([pAdoption, pUpload]) .then(() => { // Phase 4: Reorder - if (plan.reorders.length === 0 && plan.deletions.length === 0 && plan.adoptions.length === 0 && plan.uploads.length === 0) { - // Nothing to reorder usually means no changes, but if we just changed metadata? - // Reorder always runs to ensure "0001" suffixes are correct if we deleted something. - // But if plan.reorders is empty, likely no items left. + const pReorder = new Promise((resolve, reject) => { if (plan.reorders.length === 0) { updateStatus('prog-reorder', 'Skipped', '#aaa'); - updateStatus('prog-sheet', 'Running...', 'blue'); - // Still update sheet? Yes. - } - } + resolve(); + return; + } - if (plan.reorders.length > 0) { - updateStatus('prog-reorder', 'Running...', 'blue'); - google.script.run - .withSuccessHandler(() => { - updateStatus('prog-reorder', 'Done', 'green'); - updateStatus('prog-sheet', 'Running...', 'blue'); - this.runSheetUpdate(); - }) - .withFailureHandler(e => { - alert("Reorder Phase Failed: " + e.message); - this.finishSave(false); - }) - .executeSavePhase(state.sku, 'reorder', plan.reorders, jobId); - } else { - updateStatus('prog-sheet', 'Running...', 'blue'); - this.runSheetUpdate(); - } + updateStatus('prog-reorder', 'Running...', 'blue'); + google.script.run + .withSuccessHandler(() => { + updateStatus('prog-reorder', 'Done', 'green'); + resolve(); + }) + .withFailureHandler(e => { + alert("Reorder Phase Failed: " + e.message); + reject(e); + }) + .executeSavePhase(state.sku, 'reorder', plan.reorders, jobId); + }); + + // Wait for BOTH Reorder and Early Sheet Update + Promise.all([pReorder, pSheet]) + .then(() => { + this.finishSave(true); + }) + .catch(e => { + // If reorder failed, we already alerted. + console.error("Save completion failed", e); + this.finishSave(false); + }); }) .catch(e => { alert("Parallel Execution Failed: " + e.message); this.stopLogPolling(); - ui.setSavingState(false); + ui.setSavingState(false); }); }, - runSheetUpdate() { - google.script.run - .withSuccessHandler(() => { - const el = document.getElementById('prog-sheet'); - if (el) { el.style.color = 'green'; el.innerText = el.innerText.split(':')[0] + ': Done'; } - this.finishSave(true); - }) - .withFailureHandler(e => { - console.error("Sheet update failed", e); - const el = document.getElementById('prog-sheet'); - if (el) { el.style.color = 'orange'; el.innerText = el.innerText.split(':')[0] + ': Done (Sheet Warn)'; } - this.finishSave(true); - }) - .updateSpreadsheetThumbnail(state.sku); - }, - finishSave(success) { this.stopLogPolling(); if (success) { diff --git a/src/mediaHandlers.ts b/src/mediaHandlers.ts index 1f7aad8..564aacf 100644 --- a/src/mediaHandlers.ts +++ b/src/mediaHandlers.ts @@ -119,7 +119,7 @@ export function saveMediaChanges(sku: string, finalState: any[], jobId: string | return logs } -export function updateSpreadsheetThumbnail(sku: string) { +export function updateSpreadsheetThumbnail(sku: string, forcedThumbnailUrl: string | null = null) { const config = new Config() const driveService = new GASDriveService() const shop = new Shop() @@ -128,6 +128,31 @@ export function updateSpreadsheetThumbnail(sku: string) { const mediaService = new MediaService(driveService, shopifyMediaService, networkService, config) const ss = new GASSpreadsheetService(); + + // Optimization: If forced URL provided (optimistic update), skip state calculation + if (forcedThumbnailUrl) { + try { + const row = ss.getRowNumberByColumnValue("product_inventory", "sku", sku); + if (row) { + const thumbUrl = forcedThumbnailUrl; + try { + const image = SpreadsheetApp.newCellImage() + .setSourceUrl(thumbUrl) + .setAltTextTitle(sku) + .setAltTextDescription(`Thumbnail for ${sku}`) + .build(); + ss.setCellValueByColumnName("product_inventory", row, "thumbnail", image); + } catch (builderErr) { + ss.setCellValueByColumnName("product_inventory", row, "thumbnail", `=IMAGE("${thumbUrl}")`); + } + } + return; + } catch (e) { + console.warn("Failed to update sheet thumbnail (forced)", e); + throw new Error("Sheet Update Failed: " + e.message); + } + } + const product = new Product(sku); // Need Shopify ID for accurate state logic?