feat: Implement Media Matching Workflow
- Added matching wizard to MediaManager.html for linking Drive files to orphaned Shopify media on load. - Updated MediaService.ts to extract filenames from Shopify URLs for better matching. - Added linkDriveFileToShopifyMedia method to MediaService and exposed it via mediaHandlers and global.ts. - Improved UX in MediaManager with image transition clearing and button state feedback.
This commit is contained in:
@ -484,6 +484,52 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Matching Modal -->
|
||||
<div id="matching-modal" class="modal-overlay" style="z-index: 150;">
|
||||
<div class="card"
|
||||
style="width: 600px; max-width: 90%; text-align: center; padding: 24px; position: relative; background: #fff;">
|
||||
<h3 style="margin-top:0;">Link Media?</h3>
|
||||
<p style="color:var(--text-secondary); margin-bottom: 24px;">
|
||||
We found a matching file in Shopify. Should these be linked?
|
||||
</p>
|
||||
|
||||
<div style="display: flex; justify-content: center; gap: 24px; margin-bottom: 24px;">
|
||||
<!-- Drive Side -->
|
||||
<div style="flex: 1;">
|
||||
<div style="font-size: 12px; font-weight: 600; margin-bottom: 8px;">Drive File</div>
|
||||
<img id="match-drive-img"
|
||||
style="width: 100%; height: 200px; object-fit: contain; border: 1px solid var(--border); border-radius: 8px; background: #f8fafc;">
|
||||
<div id="match-drive-name"
|
||||
style="font-size: 11px; margin-top: 4px; color: var(--text-secondary); word-break: break-all;">filename.jpg
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Icon -->
|
||||
<div style="display: flex; align-items: center; font-size: 24px; color: var(--text-secondary);">🔗</div>
|
||||
|
||||
<!-- Shopify Side -->
|
||||
<div style="flex: 1;">
|
||||
<div style="font-size: 12px; font-weight: 600; margin-bottom: 8px;">Shopify Media</div>
|
||||
<img id="match-shopify-img"
|
||||
style="width: 100%; height: 200px; object-fit: contain; border: 1px solid var(--border); border-radius: 8px; background: #f8fafc;">
|
||||
<div id="match-shopify-name"
|
||||
style="font-size: 11px; margin-top: 4px; color: var(--text-secondary); word-break: break-all;">filename.jpg
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 12px; justify-content: center;">
|
||||
<button id="btn-match-skip" onclick="controller.skipLink()" class="btn btn-secondary" style="width: 100px;">No,
|
||||
Skip</button>
|
||||
<button id="btn-match-confirm" onclick="controller.confirmLink()" class="btn" style="width: 100px;">Yes,
|
||||
Link</button>
|
||||
</div>
|
||||
<div style="margin-top: 12px; font-size: 12px; color: var(--text-secondary);">
|
||||
Match <span id="match-index">1</span> of <span id="match-total">1</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="drop-overlay"
|
||||
style="position: fixed; top:0; left:0; right:0; bottom:0; background: rgba(37, 99, 235, 0.9); z-index: 200; display: none; flex-direction: column; align-items: center; justify-content: center; color: white;">
|
||||
<div style="font-size: 48px; margin-bottom: 16px;">☁️</div>
|
||||
@ -986,11 +1032,13 @@
|
||||
}));
|
||||
|
||||
state.setItems(normalized);
|
||||
document.getElementById('loading-ui').style.display = 'none';
|
||||
document.getElementById('main-ui').style.display = 'block';
|
||||
|
||||
ui.logStatus('done', 'Finished loading.', 'success');
|
||||
setTimeout(() => ui.toggleLog(false), 1000); // Auto hide after 1s
|
||||
if (!controller.hasRunMatching) {
|
||||
controller.hasRunMatching = true;
|
||||
controller.checkMatches(normalized);
|
||||
} else {
|
||||
controller.showGallery();
|
||||
}
|
||||
|
||||
})
|
||||
.withFailureHandler(function (err) {
|
||||
@ -1114,6 +1162,150 @@
|
||||
})
|
||||
.importFromPicker(state.sku, null, item.mimeType, item.filename, url);
|
||||
});
|
||||
},
|
||||
|
||||
// --- Compatibility / Matching Logic ---
|
||||
matches: [],
|
||||
currentMatchIndex: 0,
|
||||
hasRunMatching: false,
|
||||
|
||||
checkMatches(items) {
|
||||
// Filter candidates
|
||||
var driveOnly = items.filter(function (i) { return i.status === 'drive_only'; });
|
||||
var shopifyOnly = items.filter(function (i) { return i.source === 'shopify_only'; }); // source check is safer for shopify items
|
||||
|
||||
var newMatches = [];
|
||||
|
||||
driveOnly.forEach(function (d) {
|
||||
// Find match by filename
|
||||
// Note: Backend might return "Orphaned Media" if extraction failed, ignore those.
|
||||
if (!d.filename || d.filename === 'Orphaned Media') return;
|
||||
|
||||
var match = shopifyOnly.find(function (s) {
|
||||
return s.filename === d.filename; // Exact match
|
||||
});
|
||||
|
||||
if (match) {
|
||||
newMatches.push({ drive: d, shopify: match });
|
||||
}
|
||||
});
|
||||
|
||||
if (newMatches.length > 0) {
|
||||
this.matches = newMatches;
|
||||
this.currentMatchIndex = 0;
|
||||
ui.logStatus('info', 'Found ' + newMatches.length + ' potential matches. Starting matching wizard...', 'info');
|
||||
this.startMatching();
|
||||
} else {
|
||||
// No matches, show UI
|
||||
this.showGallery();
|
||||
}
|
||||
},
|
||||
|
||||
startMatching() {
|
||||
document.getElementById('loading-ui').style.display = 'none';
|
||||
document.getElementById('main-ui').style.display = 'none';
|
||||
document.getElementById('matching-modal').style.display = 'flex';
|
||||
this.renderMatch();
|
||||
},
|
||||
|
||||
renderMatch() {
|
||||
var match = this.matches[this.currentMatchIndex];
|
||||
|
||||
// Reset Buttons
|
||||
var btnConfirm = document.getElementById('btn-match-confirm');
|
||||
var btnSkip = document.getElementById('btn-match-skip');
|
||||
if (btnConfirm) {
|
||||
btnConfirm.disabled = false;
|
||||
btnConfirm.innerText = "Yes, Link";
|
||||
}
|
||||
if (btnSkip) {
|
||||
btnSkip.disabled = false;
|
||||
btnSkip.innerText = "No, Skip";
|
||||
}
|
||||
|
||||
var dImg = document.getElementById('match-drive-img');
|
||||
var sImg = document.getElementById('match-shopify-img');
|
||||
|
||||
// Reset visual state safely
|
||||
dImg.style.transition = 'none';
|
||||
dImg.style.opacity = '0';
|
||||
sImg.style.transition = 'none';
|
||||
sImg.style.opacity = '0';
|
||||
|
||||
// Clear source to blank pixel to ensure old image is gone
|
||||
var blank = "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";
|
||||
dImg.src = blank;
|
||||
sImg.src = blank;
|
||||
|
||||
document.getElementById('match-drive-name').innerText = match.drive.filename;
|
||||
document.getElementById('match-shopify-name').innerText = match.shopify.filename;
|
||||
|
||||
document.getElementById('match-index').innerText = this.currentMatchIndex + 1;
|
||||
document.getElementById('match-total').innerText = this.matches.length;
|
||||
|
||||
// Load new images
|
||||
setTimeout(function () {
|
||||
dImg.style.transition = 'opacity 0.3s ease';
|
||||
sImg.style.transition = 'opacity 0.3s ease';
|
||||
|
||||
dImg.onload = function () { dImg.style.opacity = '1'; };
|
||||
sImg.onload = function () { sImg.style.opacity = '1'; };
|
||||
|
||||
dImg.src = match.drive.thumbnail;
|
||||
sImg.src = match.shopify.thumbnail;
|
||||
}, 50);
|
||||
},
|
||||
|
||||
confirmLink() {
|
||||
var match = this.matches[this.currentMatchIndex];
|
||||
var _this = this;
|
||||
|
||||
document.getElementById('btn-match-confirm').disabled = true;
|
||||
document.getElementById('btn-match-confirm').innerText = "Linking...";
|
||||
document.getElementById('btn-match-skip').disabled = true;
|
||||
|
||||
// ui.logStatus('link', 'Linking ' + match.drive.filename + '...', 'info');
|
||||
|
||||
google.script.run
|
||||
.withSuccessHandler(function () {
|
||||
// ui.logStatus('link', 'Linked ' + match.drive.filename, 'success');
|
||||
_this.nextMatch();
|
||||
})
|
||||
.withFailureHandler(function (e) {
|
||||
alert("Failed to link: " + e.message);
|
||||
document.getElementById('btn-match-confirm').disabled = false;
|
||||
document.getElementById('btn-match-confirm').innerText = "Yes, Link";
|
||||
document.getElementById('btn-match-skip').disabled = false;
|
||||
})
|
||||
.linkDriveFileToShopifyMedia(state.sku, match.drive.id, match.shopify.id);
|
||||
},
|
||||
|
||||
skipLink() {
|
||||
document.getElementById('btn-match-skip').innerText = "Skipping...";
|
||||
document.getElementById('btn-match-skip').disabled = true;
|
||||
document.getElementById('btn-match-confirm').disabled = true;
|
||||
setTimeout(() => this.nextMatch(), 200);
|
||||
},
|
||||
|
||||
nextMatch() {
|
||||
this.currentMatchIndex++;
|
||||
if (this.currentMatchIndex < this.matches.length) {
|
||||
this.renderMatch();
|
||||
} else {
|
||||
// Done
|
||||
document.getElementById('matching-modal').style.display = 'none';
|
||||
ui.logStatus('info', 'Matching complete. Reloading...', 'info');
|
||||
document.getElementById('loading-ui').style.display = 'block';
|
||||
// Reload to get fresh state. Since hasRunMatching is true, it shouldn't trigger again.
|
||||
this.loadMedia(true);
|
||||
}
|
||||
},
|
||||
|
||||
showGallery() {
|
||||
document.getElementById('loading-ui').style.display = 'none';
|
||||
document.getElementById('main-ui').style.display = 'block';
|
||||
ui.logStatus('done', 'Finished loading.', 'success');
|
||||
setTimeout(function () { ui.toggleLog(false); }, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ import { fillProductFromTemplate } from "./fillProductFromTemplate"
|
||||
import { showSidebar, getQueueStatus, setQueueEnabled, deleteEdit, pushEdit } from "./sidebar"
|
||||
import { checkRecentSales, reconcileSalesHandler } from "./salesSync"
|
||||
import { installSalesSyncTrigger } from "./triggers"
|
||||
import { showMediaManager, getSelectedProductInfo, getMediaForSku, saveFileToDrive, saveMediaChanges, getMediaDiagnostics, getPickerConfig, importFromPicker, debugScopes, createPhotoSession, checkPhotoSession, debugFolderAccess } from "./mediaHandlers"
|
||||
import { showMediaManager, getSelectedProductInfo, getMediaForSku, saveFileToDrive, saveMediaChanges, getMediaDiagnostics, getPickerConfig, importFromPicker, debugScopes, createPhotoSession, checkPhotoSession, debugFolderAccess, linkDriveFileToShopifyMedia } from "./mediaHandlers"
|
||||
import { runSystemDiagnostics } from "./verificationSuite"
|
||||
|
||||
// prettier-ignore
|
||||
@ -64,3 +64,4 @@ import { runSystemDiagnostics } from "./verificationSuite"
|
||||
;(global as any).createPhotoSession = createPhotoSession
|
||||
;(global as any).checkPhotoSession = checkPhotoSession
|
||||
;(global as any).debugFolderAccess = debugFolderAccess
|
||||
;(global as any).linkDriveFileToShopifyMedia = linkDriveFileToShopifyMedia
|
||||
|
||||
@ -116,6 +116,17 @@ export function getMediaDiagnostics(sku: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function linkDriveFileToShopifyMedia(sku: string, driveId: string, shopifyId: string) {
|
||||
const config = new Config()
|
||||
const driveService = new GASDriveService()
|
||||
const shop = new Shop()
|
||||
const shopifyMediaService = new ShopifyMediaService(shop)
|
||||
const networkService = new GASNetworkService()
|
||||
const mediaService = new MediaService(driveService, shopifyMediaService, networkService, config)
|
||||
|
||||
return mediaService.linkDriveFileToShopifyMedia(sku, driveId, shopifyId)
|
||||
}
|
||||
|
||||
export function saveFileToDrive(sku: string, filename: string, mimeType: string, base64Data: string) {
|
||||
const config = new Config()
|
||||
const driveService = new GASDriveService()
|
||||
|
||||
@ -153,12 +153,26 @@ export class MediaService {
|
||||
contentUrl = m.image.url
|
||||
}
|
||||
|
||||
// Extract filename from URL (Shopify URLs usually contain the filename)
|
||||
let filename = "Orphaned Media";
|
||||
try {
|
||||
if (contentUrl) {
|
||||
// Clean query params and get last segment
|
||||
const cleanUrl = contentUrl.split('?')[0];
|
||||
const parts = cleanUrl.split('/');
|
||||
const candidate = parts.pop();
|
||||
if (candidate) filename = candidate;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to extract filename from URL", e);
|
||||
}
|
||||
|
||||
|
||||
unifiedState.push({
|
||||
id: m.id, // Use Shopify ID keys for orphans
|
||||
driveId: null,
|
||||
shopifyId: m.id,
|
||||
filename: "Orphaned Media", // Shopify doesn't always expose filename cleanly in same way
|
||||
// Try to get filename if possible or fallback
|
||||
filename: filename,
|
||||
source: 'shopify_only',
|
||||
thumbnail: m.preview?.image?.originalSrc || "",
|
||||
status: 'active',
|
||||
@ -172,6 +186,13 @@ export class MediaService {
|
||||
return unifiedState
|
||||
}
|
||||
|
||||
linkDriveFileToShopifyMedia(sku: string, driveId: string, shopifyId: string) {
|
||||
console.log(`MediaService: Linking Drive File ${driveId} to Shopify Media ${shopifyId}`);
|
||||
// Verify ownership? Maybe later. For now, trust the ID.
|
||||
this.driveService.updateFileProperties(driveId, { shopify_media_id: shopifyId });
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
processMediaChanges(sku: string, finalState: any[], shopifyProductId: string): string[] {
|
||||
const logs: string[] = []
|
||||
logs.push(`Starting processing for SKU ${sku}`)
|
||||
|
||||
Reference in New Issue
Block a user