Add batch picture candidate workflow

This commit is contained in:
2026-08-06 00:16:25 +02:00
parent a8e3e62dbd
commit 0987edd112
5 changed files with 289 additions and 14 deletions
+66 -4
View File
@@ -34,6 +34,7 @@ const sidebarOverlay = document.querySelector("#sidebarOverlay");
const sourceModal = document.querySelector("#sourceModal");
const sourceTitle = document.querySelector("#sourceTitle");
const sourceList = document.querySelector("#sourceList");
const sourceBatchActionButton = document.querySelector("#sourceBatchActionButton");
const sourceReloadButton = document.querySelector("#sourceReloadButton");
const sourceCloseButton = document.querySelector("#sourceCloseButton");
const settingsButton = document.querySelector("#settingsButton");
@@ -200,6 +201,7 @@ loadProductButton.title = "Load existing product attribute info.";
loadProductButton.addEventListener("click", loadProductInfoForCurrentProduct);
findSourcesButton.addEventListener("click", findSourcesForCurrentProduct);
sourceReloadButton.addEventListener("click", () => findSourcesForCurrentProduct(true));
sourceBatchActionButton.addEventListener("click", handleSourceBatchAction);
getPicturesButton.addEventListener("click", getPicturesForCurrentProduct);
secondaryPanelToggle.addEventListener("click", toggleSecondaryPanel);
mainWorkspaceToggle.addEventListener("click", toggleMainWorkspace);
@@ -404,6 +406,23 @@ function setButtonLabel(button, label) {
button.textContent = label;
}
}
function setButtonIcon(button, iconName) {
const useElement = button.querySelector(":scope > svg use");
if (useElement)
useElement.setAttribute("href", `/icons.svg#${iconName}`);
}
function setSourceBatchAction(mode) {
sourceBatchActionButton.dataset.mode = mode;
sourceBatchActionButton.hidden = false;
sourceBatchActionButton.disabled = false;
if (mode === "get-pictures") {
setButtonIcon(sourceBatchActionButton, "image");
setButtonLabel(sourceBatchActionButton, "Get all pictures");
return;
}
setButtonIcon(sourceBatchActionButton, "save");
setButtonLabel(sourceBatchActionButton, "Save all");
}
async function loadLanguages() {
languageSelect.replaceChildren(createOption("", "Select language"));
try {
@@ -821,6 +840,7 @@ function getFirstSupplierColorGroup() {
return [...groups.values()].find((group) => group.supplierRows.length > 0) || null;
}
function renderSourceCandidates(items) {
sourceBatchActionButton.hidden = true;
if (!items.length) {
const empty = document.createElement("div");
empty.className = "source-row";
@@ -828,6 +848,7 @@ function renderSourceCandidates(items) {
sourceList.replaceChildren(empty);
return;
}
setSourceBatchAction("get-pictures");
sourceList.replaceChildren(...items.map((item) => {
const section = document.createElement("section");
section.className = "source-section";
@@ -855,12 +876,14 @@ function renderSourceCandidates(items) {
}));
}
function renderPictureCandidates(items) {
sourceBatchActionButton.hidden = true;
const rows = [];
let position = 0;
for (const item of items) {
for (const candidate of item.candidates || []) {
for (const picture of candidate.pictures || []) {
position += 1;
const sourcePosition = position;
const target = buildPictureTargetInfo();
const row = document.createElement("div");
row.className = "picture-candidate";
@@ -878,7 +901,7 @@ function renderPictureCandidates(items) {
details.className = "picture-candidate-meta";
const label = document.createElement("span");
label.className = "picture-candidate-title";
label.textContent = `${item.source?.name || item.source?.key || "Source"} picture #${position}`;
label.textContent = `${item.source?.name || item.source?.key || "Source"} picture #${sourcePosition}`;
const dimensions = document.createElement("span");
dimensions.className = "picture-candidate-dimensions";
dimensions.textContent = "Size: loading...";
@@ -901,16 +924,17 @@ function renderPictureCandidates(items) {
action.type = "button";
action.className = "source-picture-action picture-candidate-save";
action.textContent = "Save to DB";
action.addEventListener("click", () => savePictureCandidateToDb({
action.savePictureCandidate = () => savePictureCandidateToDb({
picture,
item,
candidate,
image,
position,
position: sourcePosition,
target,
action,
duplicateHint,
}));
});
action.addEventListener("click", () => action.savePictureCandidate());
row.append(preview, details, action);
rows.push(row);
}
@@ -923,6 +947,7 @@ function renderPictureCandidates(items) {
sourceList.replaceChildren(empty);
return;
}
setSourceBatchAction("save-pictures");
sourceList.replaceChildren(...rows);
}
function buildPictureTargetInfo() {
@@ -990,16 +1015,53 @@ async function savePictureCandidateToDb({ picture, item, candidate, image, posit
if (!response.ok)
throw new Error(data.message || "Picture save failed.");
action.textContent = "Saved";
action.dataset.saved = "1";
duplicateHint.textContent = data.duplicateStatus === "exact_duplicate"
? `DB: duplicate of #${data.duplicateOfCandidateId || "known"}`
: `DB: saved #${data.candidateId}`;
return true;
}
catch (error) {
action.disabled = false;
action.textContent = "Save to DB";
duplicateHint.textContent = `DB: ${error.message || "save failed"}`;
return false;
}
}
async function saveAllVisiblePictureCandidates() {
const buttons = [...sourceList.querySelectorAll(".picture-candidate-save")]
.filter((button) => button.savePictureCandidate && button.dataset.saved !== "1");
if (!buttons.length)
return;
sourceBatchActionButton.disabled = true;
setButtonLabel(sourceBatchActionButton, `Saving 0/${buttons.length}`);
let savedCount = 0;
for (const button of buttons) {
const saved = await button.savePictureCandidate();
if (saved)
savedCount += 1;
setButtonLabel(sourceBatchActionButton, `Saving ${savedCount}/${buttons.length}`);
}
sourceBatchActionButton.disabled = false;
setButtonLabel(sourceBatchActionButton, savedCount === buttons.length ? "Saved all" : `Saved ${savedCount}/${buttons.length}`);
}
async function handleSourceBatchAction() {
if (sourceBatchActionButton.dataset.mode === "get-pictures") {
sourceBatchActionButton.disabled = true;
setButtonLabel(sourceBatchActionButton, "Loading pictures...");
try {
await getPicturesForCurrentProduct("");
}
finally {
sourceBatchActionButton.disabled = false;
if (sourceBatchActionButton.dataset.mode === "get-pictures") {
setButtonLabel(sourceBatchActionButton, "Get all pictures");
}
}
return;
}
await saveAllVisiblePictureCandidates();
}
function collectPictureUrls(items) {
return items.flatMap((item) => (item.candidates || []).flatMap((candidate) => candidate.pictures || [])).filter((url, index, all) => all.indexOf(url) === index);
}