feat(media-manager): link media filenames to preview pages in match wizard

- Updates the 'Link Media' wizard and confirmation modal to make filenames clickable.
- Links Drive files to their view page.
- Links Shopify files to the Admin Content > Files page, derived from the product admin URL.
- Applies primary theme color to links for better visibility.
This commit is contained in:
Ben Miller
2026-01-01 05:37:53 -07:00
parent 09995d0d05
commit 8d780d2fcb

View File

@ -525,46 +525,54 @@
/* Combined Card Styles */
.combined-item {
display: flex;
flex-direction: column;
background: #f0f9ff; /* Light blue tint */
border: 2px solid var(--primary);
display: flex;
flex-direction: column;
background: #f0f9ff;
/* Light blue tint */
border: 2px solid var(--primary);
}
.combined-images {
display: flex;
flex: 1;
overflow: hidden;
border-bottom: 1px solid var(--border);
display: flex;
flex: 1;
overflow: hidden;
border-bottom: 1px solid var(--border);
}
.combined-part {
flex: 1;
position: relative;
border-right: 1px solid var(--border);
flex: 1;
position: relative;
border-right: 1px solid var(--border);
}
.combined-part:last-child {
border-right: none;
border-right: none;
}
.combined-part .media-content {
aspect-ratio: auto; /* Allow filling height */
height: 100px;
aspect-ratio: auto;
/* Allow filling height */
height: 100px;
}
.unlink-btn {
width: 100%;
background: white;
border: none;
padding: 6px;
color: var(--danger);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
font-size: 11px;
font-weight: 500;
transition: background 0.1s;
width: 100%;
background: white;
border: none;
padding: 6px;
color: var(--danger);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
font-size: 11px;
font-weight: 500;
transition: background 0.1s;
}
.unlink-btn:hover {
background: #fee2e2;
background: #fee2e2;
}
</style>
</head>
@ -830,54 +838,54 @@
this.checkDirty();
};
MediaState.prototype.toggleSelection = function (id) {
var item = this.items.find(function (i) { return i.id === id; });
if (!item) return;
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);
var affectedIds = [id];
var isSelected = this.selectedIds.has(id);
var affectedIds = [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 (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) {
// Simplified clearing logic
if (i.source === 'drive_only' && _this.selectedIds.has(i.id) && i.id !== id) {
_this.selectedIds.delete(i.id);
affectedIds.push(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) && i.id !== id) {
_this.selectedIds.delete(i.id);
affectedIds.push(i.id);
}
});
}
this.selectedIds.add(id);
}
if (isDrive) {
// Clear other Drive selections
var _this = this;
this.items.forEach(function (i) {
// Simplified clearing logic
if (i.source === 'drive_only' && _this.selectedIds.has(i.id) && i.id !== id) {
_this.selectedIds.delete(i.id);
affectedIds.push(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) && i.id !== id) {
_this.selectedIds.delete(i.id);
affectedIds.push(i.id);
}
});
}
this.selectedIds.add(id);
}
// Targeted updates
affectedIds.forEach(function (aid) { ui.updateCardState(aid); });
ui.updateLinkButtonState();
};
// Targeted updates
affectedIds.forEach(function (aid) { ui.updateCardState(aid); });
ui.updateLinkButtonState();
};
MediaState.prototype.clearSelection = function () {
var affectedIds = Array.from(this.selectedIds);
this.selectedIds.clear();
affectedIds.forEach(function (aid) { ui.updateCardState(aid); });
ui.updateLinkButtonState();
};
MediaState.prototype.clearSelection = function () {
var affectedIds = Array.from(this.selectedIds);
this.selectedIds.clear();
affectedIds.forEach(function (aid) { ui.updateCardState(aid); });
ui.updateLinkButtonState();
};
MediaState.prototype.addItem = function (item) {
this.items.push(item);
@ -885,12 +893,12 @@
this.checkDirty();
};
MediaState.prototype.deleteItem = function (id) {
var item = this.items.find(function (i) { return i.id === id; });
if (!item) {
console.warn("[MediaState] Item not found for deletion:", id);
return;
}
MediaState.prototype.deleteItem = function (id) {
var item = this.items.find(function (i) { return i.id === id; });
if (!item) {
console.warn("[MediaState] Item not found for deletion:", id);
return;
}
if (item.source === 'new') {
var index = this.items.indexOf(item);
if (index !== -1) this.items.splice(index, 1);
@ -906,13 +914,13 @@
// Handled by Sortable
};
MediaState.prototype.unlinkMedia = function (driveId, shopifyId) {
this.tentativeLinks = this.tentativeLinks.filter(function (l) {
return !(l.driveId === driveId && l.shopifyId === shopifyId);
});
ui.render(this.items);
this.checkDirty();
};
MediaState.prototype.unlinkMedia = function (driveId, shopifyId) {
this.tentativeLinks = this.tentativeLinks.filter(function (l) {
return !(l.driveId === driveId && l.shopifyId === shopifyId);
});
ui.render(this.items);
this.checkDirty();
};
MediaState.prototype.checkDirty = function () {
var plan = this.calculateDiff();
@ -1010,8 +1018,8 @@
};
var state = new MediaState();
state.sku = "<?!= initialSku ?>";
state.title = "<?!= initialTitle ?>";
state.sku = "<?!= initialSku ?>";
state.title = "<?!= initialTitle ?>";
window.state = state;
// --- ES5 Refactor: UI ---
@ -1131,21 +1139,21 @@
btn.disabled = false;
};
UI.prototype.updateLinkButtonState = function () {
var btnLink = document.getElementById('btn-link-selected');
if (btnLink) {
var items = state.items || [];
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'; });
UI.prototype.updateLinkButtonState = function () {
var btnLink = document.getElementById('btn-link-selected');
if (btnLink) {
var items = state.items || [];
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 (hasDrive && hasShopify) {
btnLink.style.display = 'block';
} else {
btnLink.style.display = 'none';
}
};
}
};
UI.prototype.render = function (items) {
this.grid.innerHTML = '';
@ -1297,23 +1305,23 @@
}
};
UI.prototype.setSavingState = function (isSaving) {
if (isSaving) {
this.grid.classList.add('grid-disabled');
if (this.sortable) this.sortable.option('disabled', true);
UI.prototype.setSavingState = function (isSaving) {
if (isSaving) {
this.grid.classList.add('grid-disabled');
if (this.sortable) this.sortable.option('disabled', true);
// Disable action buttons explicitly if needed (pointer-events handles most, but keyboard nav?)
var btns = this.grid.querySelectorAll('button');
btns.forEach(function (b) { b.disabled = true; });
// Disable action buttons explicitly if needed (pointer-events handles most, but keyboard nav?)
var btns = this.grid.querySelectorAll('button');
btns.forEach(function (b) { b.disabled = true; });
} else {
this.grid.classList.remove('grid-disabled');
if (this.sortable) this.sortable.option('disabled', false);
} else {
this.grid.classList.remove('grid-disabled');
if (this.sortable) this.sortable.option('disabled', false);
var btns = this.grid.querySelectorAll('button');
btns.forEach(function (b) { b.disabled = false; });
}
};
var btns = this.grid.querySelectorAll('button');
btns.forEach(function (b) { b.disabled = false; });
}
};
UI.prototype.logStatus = function (step, message, type) {
if (!type) type = 'info';
@ -1422,7 +1430,7 @@
centerIcon +
'<div class="media-overlay">' +
'<button class="icon-btn btn-view" onclick="ui.openPreview(\'' + item.id + '\')" title="View">👁️</button>' +
linkSelectionBtn +
linkSelectionBtn +
actionBtn +
'</div>';
@ -1477,84 +1485,84 @@
return div;
};
UI.prototype.updateCardState = function (id) {
var item = state.items.find(function (i) { return i.id === id; });
var el = this.grid.querySelector('[data-id="' + id + '"]');
if (!item || !el) return;
UI.prototype.updateCardState = function (id) {
var item = state.items.find(function (i) { return i.id === id; });
var el = this.grid.querySelector('[data-id="' + id + '"]');
if (!item || !el) return;
var isSelected = state.selectedIds.has(item.id);
var isSelected = state.selectedIds.has(item.id);
// 1. Update Container Class
// Combined Items don't use this update path (they are re-rendered if unlinked)
el.className = 'media-item ' + (item._deleted ? 'deleted-item' : '') + (isSelected ? ' selected' : '');
if (item.isProcessing) el.className += ' processing-card';
// 1. Update Container Class
// Combined Items don't use this update path (they are re-rendered if unlinked)
el.className = 'media-item ' + (item._deleted ? 'deleted-item' : '') + (isSelected ? ' selected' : '');
if (item.isProcessing) el.className += ' processing-card';
// 2. Update Badge
var badgeEl = el.querySelector('.badge');
if (badgeEl) {
if (!item._deleted) {
if (item.source === 'synced') {
badgeEl.innerText = 'Synced';
badgeEl.title = 'Synced';
badgeEl.style.background = '#dcfce7';
badgeEl.style.color = '#166534';
} else if (item.source === 'drive_only') {
badgeEl.innerText = 'Drive';
badgeEl.title = 'Drive Only';
badgeEl.style.background = '#dbeafe';
badgeEl.style.color = '#1e40af';
} else if (item.source === 'shopify_only') {
badgeEl.innerText = 'Shopify';
badgeEl.title = 'Shopify Only';
badgeEl.style.background = '#fce7f3';
badgeEl.style.color = '#9d174d';
}
} else {
badgeEl.innerText = 'Deleted';
badgeEl.title = '';
badgeEl.style.background = '#fee2e2';
badgeEl.style.color = '#991b1b';
}
}
// 2. Update Badge
var badgeEl = el.querySelector('.badge');
if (badgeEl) {
if (!item._deleted) {
if (item.source === 'synced') {
badgeEl.innerText = 'Synced';
badgeEl.title = 'Synced';
badgeEl.style.background = '#dcfce7';
badgeEl.style.color = '#166534';
} else if (item.source === 'drive_only') {
badgeEl.innerText = 'Drive';
badgeEl.title = 'Drive Only';
badgeEl.style.background = '#dbeafe';
badgeEl.style.color = '#1e40af';
} else if (item.source === 'shopify_only') {
badgeEl.innerText = 'Shopify';
badgeEl.title = 'Shopify Only';
badgeEl.style.background = '#fce7f3';
badgeEl.style.color = '#9d174d';
}
} else {
badgeEl.innerText = 'Deleted';
badgeEl.title = '';
badgeEl.style.background = '#fee2e2';
badgeEl.style.color = '#991b1b';
}
}
// 3. Update Overlay Buttons
var overlay = el.querySelector('.media-overlay');
if (overlay) {
// Remove existing link button if it exists
var oldLinkBtn = el.querySelector('[id="link-btn-' + id + '"]');
if (oldLinkBtn) oldLinkBtn.remove();
// 3. Update Overlay Buttons
var overlay = el.querySelector('.media-overlay');
if (overlay) {
// Remove existing link button if it exists
var oldLinkBtn = el.querySelector('[id="link-btn-' + id + '"]');
if (oldLinkBtn) oldLinkBtn.remove();
// Add link button back if NOT deleted
if (!item._deleted && (item.source === 'drive_only' || item.source === 'shopify_only')) {
var linkHtml = '<button id="link-btn-' + item.id + '" class="icon-btn' + (isSelected ? ' active' : '') + '" onclick="event.stopPropagation(); state.toggleSelection(\'' + item.id + '\')" title="Select for linking">🔗</button>';
var viewBtn = overlay.querySelector('.btn-view');
if (viewBtn) {
viewBtn.insertAdjacentHTML('afterend', linkHtml);
} else {
overlay.insertAdjacentHTML('afterbegin', linkHtml);
}
}
// Add link button back if NOT deleted
if (!item._deleted && (item.source === 'drive_only' || item.source === 'shopify_only')) {
var linkHtml = '<button id="link-btn-' + item.id + '" class="icon-btn' + (isSelected ? ' active' : '') + '" onclick="event.stopPropagation(); state.toggleSelection(\'' + item.id + '\')" title="Select for linking">🔗</button>';
var viewBtn = overlay.querySelector('.btn-view');
if (viewBtn) {
viewBtn.insertAdjacentHTML('afterend', linkHtml);
} else {
overlay.insertAdjacentHTML('afterbegin', linkHtml);
}
}
// Update Delete/Restore button
var actionBtn = overlay.querySelector('.btn-delete') || overlay.querySelector('[title="Restore"]');
if (actionBtn) {
if (item._deleted) {
actionBtn.className = 'icon-btn';
actionBtn.innerHTML = '↩️';
actionBtn.title = 'Restore';
} else {
actionBtn.className = 'icon-btn btn-delete';
actionBtn.innerHTML = '🗑️';
actionBtn.title = 'Delete';
}
}
}
// Update Delete/Restore button
var actionBtn = overlay.querySelector('.btn-delete') || overlay.querySelector('[title="Restore"]');
if (actionBtn) {
if (item._deleted) {
actionBtn.className = 'icon-btn';
actionBtn.innerHTML = '↩️';
actionBtn.title = 'Restore';
} else {
actionBtn.className = 'icon-btn btn-delete';
actionBtn.innerHTML = '🗑️';
actionBtn.title = 'Delete';
}
}
}
// 4. Update global item count
var activeCount = state.items.filter(function (i) { return !i._deleted; }).length;
var countEl = document.getElementById('item-count');
if (countEl) countEl.innerText = '(' + activeCount + ')';
};
// 4. Update global item count
var activeCount = state.items.filter(function (i) { return !i._deleted; }).length;
var countEl = document.getElementById('item-count');
if (countEl) countEl.innerText = '(' + activeCount + ')';
};
UI.prototype.openPreview = function (id) {
var item = state.items.find(function (i) { return i.id === id; });
@ -1790,15 +1798,15 @@
if (manualMatches.length > 0) {
this.matches = manualMatches;
this.currentMatchIndex = 0;
this.postMatchAction = 'save';
this.postMatchAction = 'save';
ui.logStatus('info', 'Processing ' + manualMatches.length + ' pending links before saving...', 'info');
ui.logStatus('info', 'Processing ' + manualMatches.length + ' pending links before saving...', 'info');
// Update Modal Text
document.getElementById('match-modal-title').innerText = "Confirm Manual Links";
document.getElementById('match-modal-text').innerText = "Please confirm the links you selected.";
// Update Modal Text
document.getElementById('match-modal-title').innerText = "Confirm Manual Links";
document.getElementById('match-modal-text').innerText = "Please confirm the links you selected.";
this.startMatching();
this.startMatching();
return;
}
}
@ -2199,8 +2207,20 @@
dImg.src = blank;
sImg.src = blank;
document.getElementById('match-drive-name').innerText = match.drive.filename;
document.getElementById('match-shopify-name').innerText = match.shopify.filename;
// Link to Drive Preview
var driveLink = "https://drive.google.com/file/d/" + match.drive.id + "/view";
document.getElementById('match-drive-name').innerHTML = '<a href="' + driveLink + '" target="_blank" style="color:var(--primary); text-decoration:underline;">' + match.drive.filename + '</a>';
// Link to Shopify Admin Media
var shopifyLink = match.shopify.contentUrl || "#";
if (ui.shopifyUrl) {
// Pattern: .../admin/content/files/{id}?selectedView=all
// We derive the base admin URL from the product URL
var adminBase = ui.shopifyUrl.split('/products/')[0];
var mediaId = match.shopify.id.split('/').pop();
shopifyLink = adminBase + "/content/files/" + mediaId + "?selectedView=all";
}
document.getElementById('match-shopify-name').innerHTML = '<a href="' + shopifyLink + '" target="_blank" style="color:var(--primary); text-decoration:underline;">' + match.shopify.filename + '</a>';
document.getElementById('match-index').innerText = this.currentMatchIndex + 1;
document.getElementById('match-total').innerText = this.matches.length;