feat: implement manual media matching in Media Manager
- Added selection logic to MediaState to track Drive-only and Shopify-only items - Refined UI to include a link button () in media card action overlays - Reordered action buttons to: Preview, Link, Delete - Replaced JS confirm with a visual side-by-side matching modal for linking - Added adaptive 'Link Selected' button to the gallery header - Fixed TypeError by restoring the quick-links element
This commit is contained in:
@ -502,6 +502,26 @@
|
||||
opacity: 0.7;
|
||||
filter: grayscale(0.5);
|
||||
}
|
||||
|
||||
/* Selection Styles */
|
||||
.media-item.selected {
|
||||
border: 3px solid var(--primary);
|
||||
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
|
||||
}
|
||||
|
||||
.icon-btn.active {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-link:hover {
|
||||
background-color: var(--primary-hover);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@ -527,10 +547,14 @@
|
||||
|
||||
<div class="card" style="padding-bottom: 0;">
|
||||
<div class="header" style="margin-bottom:8px; display:flex; justify-content:space-between; align-items:center;">
|
||||
<div style="display:flex; align-items:baseline; gap:12px;">
|
||||
<div style="display:flex; align-items:center; gap:12px;">
|
||||
<h2 style="margin:0;">Gallery <span id="item-count"
|
||||
style="font-weight:400; color:var(--text-secondary); font-size:12px;">(0)</span></h2>
|
||||
<div id="quick-links" style="font-size:12px; display:flex; gap:8px;"></div>
|
||||
<button id="btn-link-selected" onclick="controller.linkSelectedMedia()" class="btn btn-link"
|
||||
style="display:none; width:auto; padding: 4px 12px; font-size:12px; height:28px;">
|
||||
Link Selected
|
||||
</button>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button id="toggle-log-btn" onclick="ui.toggleLog()"
|
||||
@ -679,8 +703,8 @@
|
||||
<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;">
|
||||
<h3 id="match-modal-title" style="margin-top:0;">Link Media?</h3>
|
||||
<p id="match-modal-text" style="color:var(--text-secondary); margin-bottom: 24px;">
|
||||
We found a matching file in Shopify. Should these be linked?
|
||||
</p>
|
||||
|
||||
@ -741,6 +765,7 @@
|
||||
this.token = null;
|
||||
this.items = [];
|
||||
this.initialState = [];
|
||||
this.selectedIds = new Set();
|
||||
}
|
||||
|
||||
MediaState.prototype.setSku = function (info) {
|
||||
@ -754,10 +779,51 @@
|
||||
MediaState.prototype.setItems = function (items) {
|
||||
this.items = items || [];
|
||||
this.initialState = JSON.parse(JSON.stringify(this.items));
|
||||
this.selectedIds.clear();
|
||||
ui.render(this.items);
|
||||
this.checkDirty();
|
||||
};
|
||||
|
||||
MediaState.prototype.toggleSelection = function (id) {
|
||||
var item = this.items.find(function (i) { return i.id === id; });
|
||||
if (!item) return;
|
||||
|
||||
var isSelected = this.selectedIds.has(id);
|
||||
|
||||
if (isSelected) {
|
||||
this.selectedIds.delete(id);
|
||||
} else {
|
||||
// Enforce one-pair rule: Max one Drive, one Shopify
|
||||
var isDrive = (item.source === 'drive_only');
|
||||
var isShopify = (item.source === 'shopify_only');
|
||||
|
||||
if (isDrive) {
|
||||
// Clear other Drive selections
|
||||
var _this = this;
|
||||
this.items.forEach(function (i) {
|
||||
if (i.source === 'drive_only' && _this.selectedIds.has(i.id)) {
|
||||
_this.selectedIds.delete(i.id);
|
||||
}
|
||||
});
|
||||
} else if (isShopify) {
|
||||
// Clear other Shopify selections
|
||||
var _this = this;
|
||||
this.items.forEach(function (i) {
|
||||
if (i.source === 'shopify_only' && _this.selectedIds.has(i.id)) {
|
||||
_this.selectedIds.delete(i.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.selectedIds.add(id);
|
||||
}
|
||||
ui.render(this.items);
|
||||
};
|
||||
|
||||
MediaState.prototype.clearSelection = function () {
|
||||
this.selectedIds.clear();
|
||||
ui.render(this.items);
|
||||
};
|
||||
|
||||
MediaState.prototype.addItem = function (item) {
|
||||
this.items.push(item);
|
||||
ui.render(this.items);
|
||||
@ -982,6 +1048,20 @@
|
||||
banner.style.display = hasProcessing ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
// Link Selected button logic
|
||||
var btnLink = document.getElementById('btn-link-selected');
|
||||
if (btnLink) {
|
||||
var selectedItems = items.filter(function (i) { return state.selectedIds.has(i.id); });
|
||||
var hasDrive = selectedItems.some(function (i) { return i.source === 'drive_only'; });
|
||||
var hasShopify = selectedItems.some(function (i) { return i.source === 'shopify_only'; });
|
||||
|
||||
if (hasDrive && hasShopify) {
|
||||
btnLink.style.display = 'block';
|
||||
} else {
|
||||
btnLink.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
this.grid.innerHTML = '<div class="empty-state">No media found. Upload something!</div>';
|
||||
return;
|
||||
@ -995,7 +1075,7 @@
|
||||
if (this.sortable) this.sortable.destroy();
|
||||
this.sortable = new Sortable(this.grid, {
|
||||
animation: 150,
|
||||
filter: '.deleted-item',
|
||||
filter: '.deleted-item', // Simplified filter
|
||||
ghostClass: 'sortable-ghost',
|
||||
dragClass: 'sortable-drag',
|
||||
onEnd: function () {
|
||||
@ -1084,7 +1164,8 @@
|
||||
|
||||
UI.prototype.createCard = function (item, index) {
|
||||
var div = document.createElement('div');
|
||||
div.className = 'media-item ' + (item._deleted ? 'deleted-item' : '');
|
||||
var isSelected = state.selectedIds.has(item.id);
|
||||
div.className = 'media-item ' + (item._deleted ? 'deleted-item' : '') + (isSelected ? ' selected' : '');
|
||||
div.dataset.id = item.id;
|
||||
|
||||
// Processing Class
|
||||
@ -1092,6 +1173,8 @@
|
||||
div.className += ' processing-card';
|
||||
}
|
||||
|
||||
|
||||
|
||||
div.onmouseenter = function () {
|
||||
var v = div.querySelector('video');
|
||||
if (v) v.play();
|
||||
@ -1103,9 +1186,9 @@
|
||||
|
||||
var badge = '';
|
||||
if (!item._deleted) {
|
||||
if (item.status === 'synced') badge = '<span class="badge" title="Synced" style="background:#dcfce7; color:#166534;">Synced</span>';
|
||||
else if (item.status === 'drive_only') badge = '<span class="badge" title="Drive Only" style="background:#dbeafe; color:#1e40af;">Drive</span>';
|
||||
else if (item.status === 'shopify_only') badge = '<span class="badge" title="Shopify Only" style="background:#fce7f3; color:#9d174d;">Shopify</span>';
|
||||
if (item.source === 'synced') badge = '<span class="badge" title="Synced" style="background:#dcfce7; color:#166534;">Synced</span>';
|
||||
else if (item.source === 'drive_only') badge = '<span class="badge" title="Drive Only" style="background:#dbeafe; color:#1e40af;">Drive</span>';
|
||||
else if (item.source === 'shopify_only') badge = '<span class="badge" title="Shopify Only" style="background:#fce7f3; color:#9d174d;">Shopify</span>';
|
||||
} else {
|
||||
badge = '<span class="badge" style="background:#fee2e2; color:#991b1b;">Deleted</span>';
|
||||
}
|
||||
@ -1124,16 +1207,23 @@
|
||||
// content URL logic (Only relevant for Shopify where we have a direct public link)
|
||||
var contentUrl = item.contentUrl || "";
|
||||
|
||||
// Link selection button
|
||||
var linkSelectionBtn = '';
|
||||
if (!item._deleted && (item.source === 'drive_only' || item.source === 'shopify_only')) {
|
||||
linkSelectionBtn = '<button class="icon-btn' + (isSelected ? ' active' : '') + '" onclick="event.stopPropagation(); state.toggleSelection(\'' + item.id + '\')" title="Select for linking">🔗</button>';
|
||||
}
|
||||
|
||||
var actionBtn = item._deleted
|
||||
? '<button class="icon-btn" onclick="state.deleteItem(' + index + ')" title="Restore">↩️</button>'
|
||||
: '<button class="icon-btn btn-delete" onclick="state.deleteItem(' + index + ')" title="Delete">🗑️</button>';
|
||||
|
||||
div.innerHTML =
|
||||
div.innerHTML +=
|
||||
badge +
|
||||
videoBadgeIcon +
|
||||
centerIcon +
|
||||
'<div class="media-overlay">' +
|
||||
'<button class="icon-btn btn-view" onclick="ui.openPreview(\'' + item.id + '\')" title="View">👁️</button>' +
|
||||
linkSelectionBtn +
|
||||
actionBtn +
|
||||
'</div>';
|
||||
|
||||
@ -1385,6 +1475,29 @@
|
||||
.getMediaManagerInitialState(sku, title);
|
||||
},
|
||||
|
||||
linkSelectedMedia() {
|
||||
var selectedItems = state.items.filter(function (i) { return state.selectedIds.has(i.id); });
|
||||
var driveItem = selectedItems.find(function (i) { return i.source === 'drive_only'; });
|
||||
var shopifyItem = selectedItems.find(function (i) { return i.source === 'shopify_only'; });
|
||||
|
||||
if (!driveItem || !shopifyItem) {
|
||||
alert("Please select exactly one Drive item and one Shopify item.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare a single match for the modal
|
||||
this.matches = [{
|
||||
drive: driveItem,
|
||||
shopify: shopifyItem
|
||||
}];
|
||||
this.currentMatchIndex = 0;
|
||||
|
||||
// Custom text for manual link
|
||||
document.getElementById('match-modal-title').innerText = "Confirm Manual Link";
|
||||
document.getElementById('match-modal-text').innerText = "Are you sure you want to link these two items?";
|
||||
|
||||
this.startMatching();
|
||||
},
|
||||
|
||||
saveChanges() {
|
||||
ui.toggleSave(false);
|
||||
@ -1730,6 +1843,10 @@
|
||||
this.currentMatchIndex = 0;
|
||||
ui.logStatus('info', 'Found ' + newMatches.length + ' potential matches. Starting matching wizard...', 'info');
|
||||
|
||||
// Default text for automatic matching
|
||||
document.getElementById('match-modal-title').innerText = "Link Media?";
|
||||
document.getElementById('match-modal-text').innerText = "We found a matching file in Shopify. Should these be linked?";
|
||||
|
||||
// Preload Images
|
||||
newMatches.forEach(function (m) {
|
||||
if (m.drive && m.drive.thumbnail) new Image().src = m.drive.thumbnail;
|
||||
|
||||
Reference in New Issue
Block a user