Implemented the functionality to create Service Requests
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportunity, formatDate, requestPillars, requestLanguage, requestPlatform, requestServices, createCloseIcon, searchableSelect, environment }) {
|
||||
function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportunity, requestSave, notify, formatDate, requestPillars, requestLanguage, requestPlatform, requestServices, createCloseIcon, searchableSelect, environment }) {
|
||||
const { document } = environment;
|
||||
let active = null;
|
||||
function node(tag, className, text) {
|
||||
@@ -9,6 +9,7 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
||||
}
|
||||
function close(restoreFocus = true) {
|
||||
if (!active) return false;
|
||||
if (active.saving) return true;
|
||||
const previous = active;
|
||||
active = null;
|
||||
previous.modal.remove();
|
||||
@@ -25,6 +26,7 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
||||
return button;
|
||||
}
|
||||
function open(number, trigger) {
|
||||
if (active?.saving) return;
|
||||
const host = document.getElementById(MODAL_ID);
|
||||
if (!host || host.hidden || !allowsCrmActivities() || !isMissing(number)) return;
|
||||
close(false);
|
||||
@@ -55,23 +57,36 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
||||
dismiss.addEventListener("click", () => close());
|
||||
header.append(heading, dismiss);
|
||||
const form = node("form", "opportunities-extension-action-form");
|
||||
form.addEventListener("submit", event => event.preventDefault());
|
||||
form.noValidate = true;
|
||||
const fields = new Map();
|
||||
const combos = new Map();
|
||||
const loadingFields = new Set(["primaryPillar", "language", "platform", "services"]);
|
||||
const availableFields = new Set();
|
||||
let serviceItems = [];
|
||||
const serviceRadios = [];
|
||||
let serviceGroup;
|
||||
const field = node("label", "opportunities-extension-action-form-field");
|
||||
const select = node("select");
|
||||
select.id = "opportunities-extension-sr-primary-pillar";
|
||||
select.name = "primaryPillar";
|
||||
select.required = true;
|
||||
field.htmlFor = select.id;
|
||||
field.append(node("span", "", "Primary pillar"), select);
|
||||
field.append(node("span", "", "Primary pillar (required)"), select);
|
||||
const autocomplete = searchableSelect(select, field);
|
||||
fields.set("primaryPillar", select);
|
||||
combos.set(select, autocomplete);
|
||||
const status = node("p", "opportunities-extension-action-form-status opportunities-extension-action-form-wide");
|
||||
status.setAttribute("role", "status");
|
||||
const footer = node("footer", "opportunities-extension-action-form-footer");
|
||||
const retry = node("button", "", "Retry");
|
||||
retry.type = "button";
|
||||
const cancel = node("button", "", "Cancel");
|
||||
const save = node("button", "opportunities-extension-action-form-save", "Save");
|
||||
save.type = "submit";
|
||||
save.disabled = true;
|
||||
cancel.type = "button";
|
||||
cancel.addEventListener("click", () => close());
|
||||
footer.append(retry, cancel);
|
||||
footer.append(retry, save, cancel);
|
||||
form.append(field);
|
||||
const extraControls = [];
|
||||
const extraLoads = [];
|
||||
@@ -82,8 +97,10 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
||||
else input.type = type;
|
||||
input.id = `opportunities-extension-sr-${name}`;
|
||||
input.name = name;
|
||||
input.required = true;
|
||||
fields.set(name, input);
|
||||
wrapper.htmlFor = input.id;
|
||||
wrapper.append(node("span", "", label), input);
|
||||
wrapper.append(node("span", "", `${label} (required)`), input);
|
||||
form.append(wrapper);
|
||||
extraControls.push(input);
|
||||
}
|
||||
@@ -98,15 +115,18 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
||||
retryOption.hidden = true;
|
||||
extraControls.push(retryOption);
|
||||
let input, combo;
|
||||
if (radios) wrapper.append(node("legend", "", label));
|
||||
if (radios) { wrapper.append(node("legend", "", `${label} (required)`)); serviceGroup = wrapper; }
|
||||
else {
|
||||
const field = node("label", "opportunities-extension-action-form-field");
|
||||
input = node("select");
|
||||
input.id = `opportunities-extension-sr-${name}`;
|
||||
input.name = name;
|
||||
input.required = true;
|
||||
fields.set(name, input);
|
||||
field.htmlFor = input.id;
|
||||
field.append(node("span", "", label), input);
|
||||
field.append(node("span", "", `${label} (required)`), input);
|
||||
combo = searchableSelect(input, field);
|
||||
combos.set(input, combo);
|
||||
content.append(field);
|
||||
extraControls.push(combo.input);
|
||||
}
|
||||
@@ -116,6 +136,9 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
||||
async function loadOptions() {
|
||||
if (loading || !current()) return;
|
||||
loading = true;
|
||||
loadingFields.add(name);
|
||||
availableFields.delete(name);
|
||||
updateSave();
|
||||
retryOption.hidden = true;
|
||||
message.hidden = false;
|
||||
message.setAttribute("role", "status");
|
||||
@@ -125,6 +148,7 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
||||
try {
|
||||
const items = await request();
|
||||
if (!current()) return;
|
||||
if (items.length) availableFields.add(name);
|
||||
if (input) {
|
||||
const blank = node("option", "", items.length ? `Select ${label.toLowerCase()}` : "No options available");
|
||||
blank.value = "";
|
||||
@@ -134,6 +158,8 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
||||
input.disabled = !items.length;
|
||||
} else {
|
||||
content.textContent = "";
|
||||
serviceItems = items;
|
||||
serviceRadios.length = 0;
|
||||
const seen = new Set();
|
||||
items.forEach((item, index) => {
|
||||
if (seen.has(String(item.Id))) return;
|
||||
@@ -142,6 +168,8 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
||||
const radio = node("input");
|
||||
radio.type = "radio";
|
||||
radio.name = "service";
|
||||
radio.required = true;
|
||||
serviceRadios.push(radio);
|
||||
radio.value = String(item.Id);
|
||||
radio.id = `opportunities-extension-sr-service-${index}`;
|
||||
const copy = node("span");
|
||||
@@ -164,6 +192,8 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
||||
retryOption.hidden = false;
|
||||
} finally {
|
||||
loading = false;
|
||||
loadingFields.delete(name);
|
||||
if (current()) updateSave();
|
||||
if (current()) { combo?.sync(); wrapper.setAttribute("aria-busy", "false"); }
|
||||
}
|
||||
}
|
||||
@@ -181,10 +211,156 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
||||
const view = { modal, trigger, loading: false };
|
||||
active = view;
|
||||
const current = () => active === view && allowsCrmActivities();
|
||||
function updateSave() { save.disabled = loadingFields.size > 0 || availableFields.size < 4 || Boolean(view.saving); }
|
||||
function confirmPayload(payload, requestHeaders = {}) {
|
||||
return new Promise(resolve => {
|
||||
const overlay = node("section", "opportunities-extension-action-form-modal opportunities-extension-sr-confirmation");
|
||||
overlay.setAttribute("role", "dialog");
|
||||
overlay.setAttribute("aria-modal", "true");
|
||||
overlay.setAttribute("aria-labelledby", "opportunities-extension-sr-confirm-title");
|
||||
const panel = node("div", "opportunities-extension-action-form-dialog opportunities-extension-sr-confirm-dialog");
|
||||
const heading = node("header", "opportunities-extension-account-plans-header");
|
||||
const title = node("h2", "", "Confirm Service Request");
|
||||
title.id = "opportunities-extension-sr-confirm-title";
|
||||
heading.append(title);
|
||||
const body = node("div", "opportunities-extension-sr-confirm-body");
|
||||
const label = node("p", "", "requestPostServiceRequest");
|
||||
label.id = "opportunities-extension-sr-confirm-payload-label";
|
||||
const preview = node("pre", "opportunities-extension-sr-confirm-payload", JSON.stringify(payload, null, 2));
|
||||
preview.tabIndex = 0;
|
||||
preview.setAttribute("aria-labelledby", label.id);
|
||||
body.append(label, preview);
|
||||
const headerPreviews = [];
|
||||
for (const name of ["requestSrTokenCatalog", "requestPostServiceRequest"]) {
|
||||
if (!requestHeaders[name]) continue;
|
||||
const headerLabel = node("p", "", `${name} - Headers`);
|
||||
headerLabel.id = `opportunities-extension-sr-confirm-${name}-headers`;
|
||||
const headerPreview = node("pre", "opportunities-extension-sr-confirm-payload", JSON.stringify(requestHeaders[name], null, 2));
|
||||
headerPreview.tabIndex = 0;
|
||||
headerPreview.setAttribute("aria-labelledby", headerLabel.id);
|
||||
headerPreviews.push(headerPreview);
|
||||
body.append(headerLabel, headerPreview);
|
||||
if (name === "requestSrTokenCatalog" && !requestHeaders[name]["x-appbuilder-client-id"]) {
|
||||
body.append(node("p", "opportunities-extension-action-form-status", "x-appbuilder-client-id: not yet observed in SeaaS requests."));
|
||||
}
|
||||
}
|
||||
const footer = node("footer", "opportunities-extension-action-form-footer");
|
||||
const cancel = node("button", "", "Cancel");
|
||||
const confirm = node("button", "", "Confirm Save");
|
||||
cancel.type = confirm.type = "button";
|
||||
footer.append(cancel, confirm);
|
||||
panel.append(heading, body, footer);
|
||||
overlay.append(panel);
|
||||
dialog.inert = true;
|
||||
dialog.setAttribute("aria-hidden", "true");
|
||||
modal.setAttribute("aria-modal", "false");
|
||||
modal.append(overlay);
|
||||
let settled = false;
|
||||
function finish(confirmed) {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
overlay.remove();
|
||||
dialog.inert = false;
|
||||
dialog.setAttribute("aria-hidden", "false");
|
||||
modal.setAttribute("aria-modal", "true");
|
||||
resolve(confirmed);
|
||||
}
|
||||
cancel.addEventListener("click", () => finish(false));
|
||||
confirm.addEventListener("click", () => finish(true));
|
||||
overlay.addEventListener("keydown", event => {
|
||||
event.stopPropagation();
|
||||
if (event.key === "Escape") { event.preventDefault(); finish(false); }
|
||||
if (event.key === "Tab") {
|
||||
const controls = [preview, ...headerPreviews, cancel, confirm];
|
||||
const index = controls.indexOf(document.activeElement);
|
||||
if (index < 0 || (event.shiftKey ? index === 0 : index === controls.length - 1)) {
|
||||
event.preventDefault(); controls[event.shiftKey ? controls.length - 1 : 0].focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
cancel.focus();
|
||||
});
|
||||
}
|
||||
form.addEventListener("submit", async event => {
|
||||
event.preventDefault();
|
||||
if (!current() || view.saving || save.disabled) return;
|
||||
const invalid = [];
|
||||
const value = name => String(fields.get(name)?.value || "").trim();
|
||||
for (const [name, input] of fields) {
|
||||
let valid = !!value(name);
|
||||
if (combos.has(input)) valid = valid && Array.from(input.children).some(option => option.value === value(name));
|
||||
if (name === "customerDeadline") {
|
||||
const date = value(name);
|
||||
valid = /^\d{4}-\d{2}-\d{2}$/.test(date) && Number.isFinite(Date.parse(`${date}T00:00:00Z`))
|
||||
&& new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) === date;
|
||||
}
|
||||
const control = combos.get(input)?.input || input;
|
||||
control.setAttribute("aria-invalid", String(!valid));
|
||||
if (!valid) invalid.push(control);
|
||||
}
|
||||
const selected = serviceRadios.find(radio => radio.checked);
|
||||
const service = serviceItems.find(item => String(item.Id) === selected?.value);
|
||||
serviceGroup.setAttribute("aria-invalid", String(!service));
|
||||
if (!service) invalid.push(serviceRadios[0]);
|
||||
if (invalid.length) {
|
||||
status.hidden = false;
|
||||
status.setAttribute("role", "alert");
|
||||
status.textContent = "Complete the highlighted required fields with valid values.";
|
||||
invalid[0]?.focus();
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
AccountPartyId: opportunity?.CustomerAccount?.PartyId,
|
||||
OpportunityName_Id_c: opportunity?.OptyId,
|
||||
Title: `${service.RecordName}-${number}-${opportunity?.CustomerAccount?.PartyUniqueName || ""}`,
|
||||
RegionText_c: opportunity?.CustomerAccount?.Region_c,
|
||||
CountryText_c: opportunity?.CustomerAccount?.PrimaryAddress?.CountryCode,
|
||||
ServiceNew_Id_c: service.Id,
|
||||
ProblemDescription: value("description"), Language_c: value("language"),
|
||||
CustomerDeadline_c: value("customerDeadline"), TechPlatformLookUp_c: value("platform"),
|
||||
PrimaryPillarNew_c: value("primaryPillar")
|
||||
};
|
||||
view.saving = true;
|
||||
const controls = [dismiss, retry, cancel, ...fields.values(), ...serviceRadios, ...extraControls];
|
||||
const disabled = controls.map(control => [control, control.disabled]);
|
||||
controls.forEach(control => { control.disabled = true; });
|
||||
combos.forEach(combo => combo.sync());
|
||||
updateSave();
|
||||
save.textContent = "Preparing...";
|
||||
form.setAttribute("aria-busy", "true");
|
||||
status.hidden = true;
|
||||
try {
|
||||
const prepared = await requestSave({ phase: "prepare", payload });
|
||||
if (!current() || host.hidden || !await confirmPayload(prepared.payload, prepared.headers)) return;
|
||||
save.textContent = "Saving...";
|
||||
await requestSave({ phase: "confirm", payload: prepared.payload });
|
||||
view.saving = false;
|
||||
close();
|
||||
notify("Service Request saved successfully.");
|
||||
} catch (error) {
|
||||
status.hidden = false;
|
||||
status.setAttribute("role", "alert");
|
||||
status.textContent = error.message || "Unable to save Service Request.";
|
||||
notify(status.textContent, true);
|
||||
} finally {
|
||||
view.saving = false;
|
||||
if (current()) {
|
||||
disabled.forEach(([control, wasDisabled]) => { control.disabled = wasDisabled; });
|
||||
combos.forEach(combo => combo.sync());
|
||||
updateSave();
|
||||
save.textContent = "Save";
|
||||
form.setAttribute("aria-busy", "false");
|
||||
save.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
function option(value, text) { const item = node("option", "", text); item.value = value; select.append(item); }
|
||||
async function load() {
|
||||
if (view.loading || !current()) return;
|
||||
view.loading = true;
|
||||
loadingFields.add("primaryPillar");
|
||||
availableFields.delete("primaryPillar");
|
||||
updateSave();
|
||||
retry.hidden = true;
|
||||
select.disabled = true;
|
||||
select.textContent = "";
|
||||
@@ -203,6 +379,7 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
||||
items.forEach(item => option(item.code, item.name));
|
||||
select.value = "";
|
||||
select.disabled = !items.length;
|
||||
if (items.length) availableFields.add("primaryPillar");
|
||||
form.setAttribute("data-state", "ready");
|
||||
status.hidden = true;
|
||||
} catch (error) {
|
||||
@@ -215,6 +392,8 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
||||
retry.hidden = false;
|
||||
} finally {
|
||||
view.loading = false;
|
||||
loadingFields.delete("primaryPillar");
|
||||
if (current()) updateSave();
|
||||
if (current()) { autocomplete.sync(); form.setAttribute("aria-busy", "false"); }
|
||||
}
|
||||
}
|
||||
@@ -223,7 +402,7 @@ function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportun
|
||||
modal.addEventListener("keydown", event => {
|
||||
if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(); }
|
||||
if (event.key !== "Tab") return;
|
||||
const controls = [dismiss, autocomplete.input, ...extraControls, retry, cancel].filter(control => !control.disabled && !control.hidden);
|
||||
const controls = [dismiss, autocomplete.input, ...extraControls, retry, save, cancel].filter(control => !control.disabled && !control.hidden);
|
||||
const index = controls.indexOf(document.activeElement);
|
||||
if (index < 0 || (event.shiftKey ? index === 0 : index === controls.length - 1)) {
|
||||
event.preventDefault(); controls[event.shiftKey ? controls.length - 1 : 0].focus();
|
||||
|
||||
@@ -310,6 +310,17 @@
|
||||
.opportunities-extension-action-form-field select { height: 38px; }
|
||||
.opportunities-extension-action-form-field select[hidden] { display: none; }
|
||||
.opportunities-extension-sr-form-dialog { width: min(900px, 100%); }
|
||||
.opportunities-extension-sr-confirmation { z-index: 6; }
|
||||
.opportunities-extension-sr-confirm-dialog { width: min(800px, 100%); display: flex; flex-direction: column; overflow: hidden; }
|
||||
.opportunities-extension-sr-confirm-dialog > header,
|
||||
.opportunities-extension-sr-confirm-dialog > footer { flex-shrink: 0; }
|
||||
.opportunities-extension-sr-confirm-dialog > footer { padding: 16px 20px; }
|
||||
.opportunities-extension-sr-confirm-body { min-height: 0; overflow: auto; padding: 0 20px; }
|
||||
.opportunities-extension-sr-confirm-body > p { font-size: 13px; font-weight: 600; }
|
||||
.opportunities-extension-sr-confirm-payload { margin: 0; padding: 16px; border: 1px solid #b9b4ae; border-radius: 4px; background: #f6f5f4; color: #312d2a; font: 13px/1.6 Consolas, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.opportunities-extension-sr-confirmation :focus-visible { outline: 2px solid #00758f; outline-offset: 2px; }
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-sr-confirm-payload { background: #211f1d; color: #eeeae6; border-color: #756f68; }
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-sr-confirmation :focus-visible { outline-color: #80d6e8; }
|
||||
.opportunities-extension-sr-form-dialog > .opportunities-extension-account-plans-header { padding: 20px 24px; }
|
||||
.opportunities-extension-sr-form-dialog > .opportunities-extension-action-form { gap: 20px 24px; padding: 24px; align-items: start; }
|
||||
.opportunities-extension-sr-form-dialog .opportunities-extension-action-form-field { gap: 8px; }
|
||||
|
||||
Reference in New Issue
Block a user