Product import

This commit is contained in:
2026-08-28 13:57:54 +02:00
parent 7442d10e68
commit 7833e1b668
21 changed files with 763 additions and 185 deletions

View File

@@ -79,7 +79,10 @@
</f:for>
</select>
</td>
<td><small>{row.oldPreview}</small></td>
<td data-vitec-inline-edit="1" data-uid="{product.uid}" data-field="{row.field}"
style="cursor:pointer;" title="Click to edit this field">
<small data-vitec-preview="1">{row.oldPreview}</small>
</td>
<td>
<f:if condition="{row.new}">
<f:then>
@@ -96,6 +99,10 @@
<td>
<input class="form-check-input" type="checkbox" name="apply[]" value="{row.field}"
{f:if(condition: row.changed, then: 'checked="checked"')} />
<button type="submit" class="btn btn-sm btn-default" style="margin-left:.5rem;"
name="applySingle" value="{row.field}"
formaction="{f:be.uri(route: 'web_vitecimport.product_apply')}"
title="Write only this field now">Save</button>
</td>
</tr>
</f:for>
@@ -152,6 +159,10 @@
<td>
<input class="form-check-input" type="checkbox" name="applyRelated[]" value="{rel.index}"
{f:if(condition: rel.changed, then: 'checked="checked"')} />
<button type="submit" class="btn btn-sm btn-default" style="margin-left:.5rem;"
name="applyRelatedSingle" value="{rel.index}"
formaction="{f:be.uri(route: 'web_vitecimport.product_apply')}"
title="Write only this card now">Save</button>
</td>
</tr>
</f:for>

View File

@@ -0,0 +1,148 @@
/**
* VITEC click-to-edit for the "Current value" column of the product text
* import (Edit Product view).
* EXT:vitec/Resources/Public/Javascript/product-inline-edit.js
*
* Delegated click handler on [data-vitec-inline-edit] cells (backend CSP
* forbids inline handlers). Clicking a cell fetches the FULL raw field value
* (the cell itself only shows a truncated preview), swaps in a textarea with
* Save/Cancel, and writes through the vitec_product_field_save AJAX route,
* which uses the same whitelist and DataHandler path as the form apply.
* RTE fields are edited as raw HTML on purpose - no inline WYSIWYG
* (decision 2026-08-28).
*/
import AjaxRequest from "@typo3/core/ajax/ajax-request.js";
import Notification from "@typo3/backend/notification.js";
const SELECTOR = "[data-vitec-inline-edit]";
function previewOf(cell) {
return cell.querySelector("[data-vitec-preview]");
}
function closeEditor(cell) {
const editor = cell.querySelector("[data-vitec-editor]");
if (editor) {
editor.remove();
}
const preview = previewOf(cell);
if (preview) {
preview.hidden = false;
}
delete cell.dataset.vitecEditing;
}
async function openEditor(cell) {
if (cell.dataset.vitecEditing === "1") {
return;
}
cell.dataset.vitecEditing = "1";
const getUrl = TYPO3.settings.ajaxUrls["vitec_product_field_get"];
const saveUrl = TYPO3.settings.ajaxUrls["vitec_product_field_save"];
if (!getUrl || !saveUrl) {
Notification.error("Inline edit", "AJAX routes are not registered.");
delete cell.dataset.vitecEditing;
return;
}
let value = "";
try {
const response = await new AjaxRequest(getUrl)
.withQueryArguments({ uid: cell.dataset.uid, field: cell.dataset.field })
.get();
const data = await response.resolve();
if (!data.success) {
Notification.error("Inline edit", data.message || "Could not load the field value.");
delete cell.dataset.vitecEditing;
return;
}
value = data.value;
} catch (e) {
Notification.error("Inline edit", "Loading the field value failed.");
delete cell.dataset.vitecEditing;
return;
}
const preview = previewOf(cell);
if (preview) {
preview.hidden = true;
}
const wrap = document.createElement("div");
wrap.setAttribute("data-vitec-editor", "1");
const textarea = document.createElement("textarea");
textarea.className = "form-control";
textarea.rows = Math.min(14, Math.max(3, Math.ceil(value.length / 80)));
textarea.style.fontFamily = "monospace";
textarea.style.fontSize = "11px";
textarea.value = value;
wrap.appendChild(textarea);
const bar = document.createElement("div");
bar.style.marginTop = ".25rem";
// type="button" is essential: the cells live inside the big mapping <form>,
// a bare <button> would submit it.
const saveBtn = document.createElement("button");
saveBtn.type = "button";
saveBtn.className = "btn btn-sm btn-primary";
saveBtn.textContent = "Save";
const cancelBtn = document.createElement("button");
cancelBtn.type = "button";
cancelBtn.className = "btn btn-sm btn-default";
cancelBtn.style.marginLeft = ".5rem";
cancelBtn.textContent = "Cancel";
bar.appendChild(saveBtn);
bar.appendChild(cancelBtn);
wrap.appendChild(bar);
cell.appendChild(wrap);
textarea.focus();
cancelBtn.addEventListener("click", () => closeEditor(cell));
textarea.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
closeEditor(cell);
}
});
saveBtn.addEventListener("click", async () => {
saveBtn.disabled = true;
try {
const response = await new AjaxRequest(saveUrl).post({
uid: cell.dataset.uid,
field: cell.dataset.field,
value: textarea.value,
});
const data = await response.resolve();
if (!data.success) {
Notification.error("Inline edit", data.message || "Saving failed.");
saveBtn.disabled = false;
return;
}
if (preview) {
preview.textContent = data.preview;
}
closeEditor(cell);
Notification.success("Inline edit", cell.dataset.field + " saved.");
} catch (e) {
Notification.error("Inline edit", "Saving failed.");
saveBtn.disabled = false;
}
});
}
document.addEventListener("click", (event) => {
const cell = event.target.closest(SELECTOR);
if (cell === null) {
return;
}
// Clicks on the editor's own controls must not re-open it.
if (event.target.closest("[data-vitec-editor]")) {
return;
}
openEditor(cell);
});