class FieldBuilder {
constructor(updateButtonRef) {
this.UpdateButton = updateButtonRef;
}
// 🔧 Shared Utilities
_setStatus(element, type, message) {
const icon = element.querySelector(".file-status-icon, .choice-status-icon");
const text = element.querySelector(".file-status-text, .choice-status-text");
if (!icon || !text) return;
icon.style.display = "inline-block";
text.textContent = message;
switch (type) {
case "loading":
icon.className = "file-status-icon fa fa-spinner fa-spin";
break;
case "success":
icon.className = "file-status-icon fa fa-check text-success";
break;
case "error":
icon.className = "file-status-icon fa fa-exclamation-triangle text-danger";
break;
case "info":
icon.className = "file-status-icon fa fa-info-circle text-info";
break;
default:
icon.style.display = "none";
icon.className = "file-status-icon";
text.textContent = "";
}
}
_toggleUpdateButton(disabled) {
if (this.UpdateButton) {
this.UpdateButton.disabled = disabled;
}
}
// 📄 Virtual File Upload Field
createVirtualFileUploadField({
entitySetName,
logicalName,
recordId,
id,
label,
description = "",
currentFileName = "",
accepts = "application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.openxmlformats-officedocument.presentationml.presentation,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/plain,application/pdf",
postUploadCallback = null
} = {}) {
const template = `
${description}
| `;
const temp = document.createElement("tbody");
temp.innerHTML = template.trim();
const element = temp.firstChild;
const fileInput = element.querySelector(`#${logicalName}_input_file`);
let lastUploadedFile = null;
function attachEmptyFile(filename, fileInput) {
const emptyFile = new File([''], filename, {
type: 'application/octet-stream',
lastModified: Date.now()
});
const dt = new DataTransfer();
dt.items.add(emptyFile);
fileInput.files = dt.files;
return emptyFile;
}
if (currentFileName !== "") {
lastUploadedFile = attachEmptyFile(currentFileName, fileInput);
}
initField(fileInput);
if (fileInput) {
fileInput.addEventListener("click", e => {
if (fileInput.hasAttribute("readonly")) {
e.preventDefault(); // stop file picker
e.stopImmediatePropagation(); // block other handlers
}
});
fileInput.addEventListener("change", async () => {
const file = fileInput.files[0];
if (!file) {
if (lastUploadedFile) {
attachEmptyFile(lastUploadedFile.name, fileInput);
}
this._setStatus(element, "default", "");
return;
}
fileInput.disabled = true;
this._toggleUpdateButton(true);
this._setStatus(element, "loading", "Uploading file");
try {
await _api.upload(entitySetName, recordId, logicalName, file);
this._setStatus(element, "success", "File uploaded successfully");
lastUploadedFile = file;
if (typeof postUploadCallback === 'function') {
try { postUploadCallback(file); } catch (cbErr) { console.error(cbErr); }
}
} catch (err) {
console.error('Upload failed', err);
this._setStatus(element, "error", err?.message || "Unknown error");
} finally {
fileInput.disabled = false;
this._toggleUpdateButton(false);
}
}, { passive: true });
}
element.resetFileStatus = () => this._setStatus(element, "default", "");
return element;
}
// 📑 Dataverse Choice Field
createDataverseChoiceField({
entitySetName,
recordId,
logicalName,
dataType = "Choice", // "Choice" or "Lookup"
targetTableSetName = "", // Required if dataType is "Lookup"
relationshipName = "", // Required if dataType is "Lookup"
id,
label = "Select an option",
description = "",
options = [],
selectedValue = "",
postUpdateCallback = null,
showStatus = false,
allowClear = false
} = {}) {
const template = `
${description}
| `;
const statusTranslations = {
loading: {
en: "Saving...",
fr: "Enregistrement..."
},
saved: {
en: "Saved successfully",
fr: "Enregistré avec succès"
}
};
const temp = document.createElement("tbody");
temp.innerHTML = template.trim();
const element = temp.firstChild;
const select = element.querySelector(`#${id}`);
if (select) {
initField(select);
//let prevValue = selectedValue ?? "";
select.addEventListener("change", async (e) => {
if (select.hasAttribute("readonly")) {
this._setStatus(element, "default", "Field is read-only");
return;
}
const newValue = e.target.value;
// do not allow blank/empty removal for now
//if (!allowClear && (newValue === "" || newValue === null || newValue === undefined)) {
// // Restore previous value visually without network call
// select.value = prevValue;
// // Provide gentle feedback
// this._setStatus(
// element,
// "info",
// "Clearing this field is not allowed."
// );
// return;
//}
this._toggleUpdateButton(true);
if (showStatus) this._setStatus(element, "loading", statusTranslations["loading"][LANG]);
try {
if (dataType === "Lookup") {
if (!targetTableSetName || !relationshipName) {
throw new Error("Missing targetTableSetName or relationshipName for lookup field");
}
await _api.associate(entitySetName, recordId, relationshipName, targetTableSetName, newValue);
} else {
await _api.update(entitySetName, recordId, {
[logicalName]: newValue
});
}
if (showStatus) this._setStatus(element, "success", statusTranslations["saved"][LANG]);
if (typeof postUpdateCallback === "function") {
try { postUpdateCallback(newValue); } catch (cbErr) { console.error(cbErr); }
}
} catch (err) {
console.error("Update failed", err);
this._setStatus(element, "error", err?.message || "Unknown error");
} finally {
this._toggleUpdateButton(false);
}
}, { passive: true });
}
return element;
}
}