Optimize Media Manager sheet update trigger

- Update mediaHandlers.ts to accept an optional forcedThumbnailUrl in updateSpreadsheetThumbnail, enabling updates without re-fetching backend state.

- Update MediaManager.html execution plan to trigger the sheet update immediately (optimistically) using the predicted first item from the plan, running in parallel with other execution phases.

- Ensure the execution flow waits for both the sheet update and other operations to complete before finishing.
This commit is contained in:
Ben Miller
2026-01-01 08:22:21 -07:00
parent 2c01693271
commit ee5fd782fe
2 changed files with 95 additions and 47 deletions

View File

@ -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 // Phase 1: Deletions
if (plan.deletions.length === 0) { if (plan.deletions.length === 0) {
updateStatus('prog-delete', 'Skipped', '#aaa'); updateStatus('prog-delete', 'Skipped', '#aaa');
this.startParallelPhases(plan, activeItems, jobId); this.startParallelPhases(plan, activeItems, jobId, pSheet);
return; return;
} }
@ -1987,7 +2025,7 @@
google.script.run google.script.run
.withSuccessHandler(() => { .withSuccessHandler(() => {
updateStatus('prog-delete', 'Done', 'green'); updateStatus('prog-delete', 'Done', 'green');
this.startParallelPhases(plan, activeItems, jobId); this.startParallelPhases(plan, activeItems, jobId, pSheet);
}) })
.withFailureHandler(e => { .withFailureHandler(e => {
updateStatus('prog-delete', 'Failed', 'red'); updateStatus('prog-delete', 'Failed', 'red');
@ -1998,7 +2036,7 @@
.executeSavePhase(state.sku, 'deletions', plan.deletions, jobId); .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 // Helper function defined at the top to avoid hoisting issues
const updateStatus = (id, status, color) => { const updateStatus = (id, status, color) => {
const el = document.getElementById(id); const el = document.getElementById(id);
@ -2009,7 +2047,6 @@
}; };
// Phase 2 & 3: Adoptions & Uploads (Parallel) // Phase 2 & 3: Adoptions & Uploads (Parallel)
const pAdoption = new Promise((resolve, reject) => { const pAdoption = new Promise((resolve, reject) => {
if (plan.adoptions.length === 0) { if (plan.adoptions.length === 0) {
updateStatus('prog-adopt', 'Skipped', '#aaa'); updateStatus('prog-adopt', 'Skipped', '#aaa');
@ -2043,34 +2080,36 @@
Promise.all([pAdoption, pUpload]) Promise.all([pAdoption, pUpload])
.then(() => { .then(() => {
// Phase 4: Reorder // Phase 4: Reorder
if (plan.reorders.length === 0 && plan.deletions.length === 0 && plan.adoptions.length === 0 && plan.uploads.length === 0) { const pReorder = new Promise((resolve, reject) => {
// 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.
if (plan.reorders.length === 0) { if (plan.reorders.length === 0) {
updateStatus('prog-reorder', 'Skipped', '#aaa'); updateStatus('prog-reorder', 'Skipped', '#aaa');
updateStatus('prog-sheet', 'Running...', 'blue'); resolve();
// Still update sheet? Yes. return;
}
} }
if (plan.reorders.length > 0) {
updateStatus('prog-reorder', 'Running...', 'blue'); updateStatus('prog-reorder', 'Running...', 'blue');
google.script.run google.script.run
.withSuccessHandler(() => { .withSuccessHandler(() => {
updateStatus('prog-reorder', 'Done', 'green'); updateStatus('prog-reorder', 'Done', 'green');
updateStatus('prog-sheet', 'Running...', 'blue'); resolve();
this.runSheetUpdate();
}) })
.withFailureHandler(e => { .withFailureHandler(e => {
alert("Reorder Phase Failed: " + e.message); alert("Reorder Phase Failed: " + e.message);
this.finishSave(false); reject(e);
}) })
.executeSavePhase(state.sku, 'reorder', plan.reorders, jobId); .executeSavePhase(state.sku, 'reorder', plan.reorders, jobId);
} else { });
updateStatus('prog-sheet', 'Running...', 'blue');
this.runSheetUpdate(); // 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 => { .catch(e => {
alert("Parallel Execution Failed: " + e.message); alert("Parallel Execution Failed: " + e.message);
@ -2079,22 +2118,6 @@
}); });
}, },
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) { finishSave(success) {
this.stopLogPolling(); this.stopLogPolling();
if (success) { if (success) {

View File

@ -119,7 +119,7 @@ export function saveMediaChanges(sku: string, finalState: any[], jobId: string |
return logs return logs
} }
export function updateSpreadsheetThumbnail(sku: string) { export function updateSpreadsheetThumbnail(sku: string, forcedThumbnailUrl: string | null = null) {
const config = new Config() const config = new Config()
const driveService = new GASDriveService() const driveService = new GASDriveService()
const shop = new Shop() const shop = new Shop()
@ -128,6 +128,31 @@ export function updateSpreadsheetThumbnail(sku: string) {
const mediaService = new MediaService(driveService, shopifyMediaService, networkService, config) const mediaService = new MediaService(driveService, shopifyMediaService, networkService, config)
const ss = new GASSpreadsheetService(); 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); const product = new Product(sku);
// Need Shopify ID for accurate state logic? // Need Shopify ID for accurate state logic?