Compare commits

...

2 Commits

Author SHA1 Message Date
8b1da56820 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
2025-12-31 10:29:45 -07:00
05d459d58f chore: remove temporary test output file 2025-12-31 09:52:52 -07:00
2 changed files with 126 additions and 9 deletions

View File

@ -502,6 +502,26 @@
opacity: 0.7; opacity: 0.7;
filter: grayscale(0.5); 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> </style>
</head> </head>
@ -527,10 +547,14 @@
<div class="card" style="padding-bottom: 0;"> <div class="card" style="padding-bottom: 0;">
<div class="header" style="margin-bottom:8px; display:flex; justify-content:space-between; align-items:center;"> <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" <h2 style="margin:0;">Gallery <span id="item-count"
style="font-weight:400; color:var(--text-secondary); font-size:12px;">(0)</span></h2> 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> <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>
<div style="display: flex; gap: 8px;"> <div style="display: flex; gap: 8px;">
<button id="toggle-log-btn" onclick="ui.toggleLog()" <button id="toggle-log-btn" onclick="ui.toggleLog()"
@ -679,8 +703,8 @@
<div id="matching-modal" class="modal-overlay" style="z-index: 150;"> <div id="matching-modal" class="modal-overlay" style="z-index: 150;">
<div class="card" <div class="card"
style="width: 600px; max-width: 90%; text-align: center; padding: 24px; position: relative; background: #fff;"> style="width: 600px; max-width: 90%; text-align: center; padding: 24px; position: relative; background: #fff;">
<h3 style="margin-top:0;">Link Media?</h3> <h3 id="match-modal-title" style="margin-top:0;">Link Media?</h3>
<p style="color:var(--text-secondary); margin-bottom: 24px;"> <p id="match-modal-text" style="color:var(--text-secondary); margin-bottom: 24px;">
We found a matching file in Shopify. Should these be linked? We found a matching file in Shopify. Should these be linked?
</p> </p>
@ -741,6 +765,7 @@
this.token = null; this.token = null;
this.items = []; this.items = [];
this.initialState = []; this.initialState = [];
this.selectedIds = new Set();
} }
MediaState.prototype.setSku = function (info) { MediaState.prototype.setSku = function (info) {
@ -754,10 +779,51 @@
MediaState.prototype.setItems = function (items) { MediaState.prototype.setItems = function (items) {
this.items = items || []; this.items = items || [];
this.initialState = JSON.parse(JSON.stringify(this.items)); this.initialState = JSON.parse(JSON.stringify(this.items));
this.selectedIds.clear();
ui.render(this.items); ui.render(this.items);
this.checkDirty(); 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) { MediaState.prototype.addItem = function (item) {
this.items.push(item); this.items.push(item);
ui.render(this.items); ui.render(this.items);
@ -982,6 +1048,20 @@
banner.style.display = hasProcessing ? 'flex' : 'none'; 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) { if (items.length === 0) {
this.grid.innerHTML = '<div class="empty-state">No media found. Upload something!</div>'; this.grid.innerHTML = '<div class="empty-state">No media found. Upload something!</div>';
return; return;
@ -995,7 +1075,7 @@
if (this.sortable) this.sortable.destroy(); if (this.sortable) this.sortable.destroy();
this.sortable = new Sortable(this.grid, { this.sortable = new Sortable(this.grid, {
animation: 150, animation: 150,
filter: '.deleted-item', filter: '.deleted-item', // Simplified filter
ghostClass: 'sortable-ghost', ghostClass: 'sortable-ghost',
dragClass: 'sortable-drag', dragClass: 'sortable-drag',
onEnd: function () { onEnd: function () {
@ -1084,7 +1164,8 @@
UI.prototype.createCard = function (item, index) { UI.prototype.createCard = function (item, index) {
var div = document.createElement('div'); 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; div.dataset.id = item.id;
// Processing Class // Processing Class
@ -1092,6 +1173,8 @@
div.className += ' processing-card'; div.className += ' processing-card';
} }
div.onmouseenter = function () { div.onmouseenter = function () {
var v = div.querySelector('video'); var v = div.querySelector('video');
if (v) v.play(); if (v) v.play();
@ -1103,9 +1186,9 @@
var badge = ''; var badge = '';
if (!item._deleted) { if (!item._deleted) {
if (item.status === 'synced') badge = '<span class="badge" title="Synced" style="background:#dcfce7; color:#166534;">Synced</span>'; if (item.source === '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.source === '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>'; else if (item.source === 'shopify_only') badge = '<span class="badge" title="Shopify Only" style="background:#fce7f3; color:#9d174d;">Shopify</span>';
} else { } else {
badge = '<span class="badge" style="background:#fee2e2; color:#991b1b;">Deleted</span>'; 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) // content URL logic (Only relevant for Shopify where we have a direct public link)
var contentUrl = item.contentUrl || ""; 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 var actionBtn = item._deleted
? '<button class="icon-btn" onclick="state.deleteItem(' + index + ')" title="Restore">↩️</button>' ? '<button class="icon-btn" onclick="state.deleteItem(' + index + ')" title="Restore">↩️</button>'
: '<button class="icon-btn btn-delete" onclick="state.deleteItem(' + index + ')" title="Delete">🗑️</button>'; : '<button class="icon-btn btn-delete" onclick="state.deleteItem(' + index + ')" title="Delete">🗑️</button>';
div.innerHTML = div.innerHTML +=
badge + badge +
videoBadgeIcon + videoBadgeIcon +
centerIcon + centerIcon +
'<div class="media-overlay">' + '<div class="media-overlay">' +
'<button class="icon-btn btn-view" onclick="ui.openPreview(\'' + item.id + '\')" title="View">👁️</button>' + '<button class="icon-btn btn-view" onclick="ui.openPreview(\'' + item.id + '\')" title="View">👁️</button>' +
linkSelectionBtn +
actionBtn + actionBtn +
'</div>'; '</div>';
@ -1385,6 +1475,29 @@
.getMediaManagerInitialState(sku, title); .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() { saveChanges() {
ui.toggleSave(false); ui.toggleSave(false);
@ -1730,6 +1843,10 @@
this.currentMatchIndex = 0; this.currentMatchIndex = 0;
ui.logStatus('info', 'Found ' + newMatches.length + ' potential matches. Starting matching wizard...', 'info'); 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 // Preload Images
newMatches.forEach(function (m) { newMatches.forEach(function (m) {
if (m.drive && m.drive.thumbnail) new Image().src = m.drive.thumbnail; if (m.drive && m.drive.thumbnail) new Image().src = m.drive.thumbnail;

Binary file not shown.