function createWorkloadActionForm({ MODAL_ID, allowsCrmActivities, isMissingAction, getOpportunity, getProducts, requestTeamMembers, requestLookup, getCurrentUserId, saveAction, onActionSaved, notify, formatDate, createCloseIcon, searchableSelect, environment }) { const { document } = environment; let active = null; function element(tag, className, text) { const node = document.createElement(tag); if (className) node.className = className; if (text !== undefined) node.textContent = text; return node; } function close(restoreFocus = true) { if (!active) return false; if (active.saving) return true; const previous = active; active = null; previous.modal.remove(); if (restoreFocus && previous.trigger?.isConnected) previous.trigger.focus(); return true; } function createButton(optyNumber) { const button = element("button", "opportunities-extension-add-action", "+"); button.type = "button"; button.title = "Add Action"; button.setAttribute("aria-label", `Add Action for ${optyNumber}`); button.setAttribute("aria-haspopup", "dialog"); button.addEventListener("click", event => { event.preventDefault(); event.stopPropagation(); open(optyNumber, button); }); return button; } function open(optyNumber, trigger) { if (!allowsCrmActivities() || !isMissingAction(optyNumber)) return; const host = document.getElementById(MODAL_ID); if (!host || host.hidden) return; close(false); const modal = element("section", "opportunities-extension-action-form-modal"); modal.setAttribute("role", "dialog"); modal.setAttribute("aria-modal", "true"); modal.setAttribute("aria-labelledby", "opportunities-extension-action-form-title"); const dialog = element("div", "opportunities-extension-action-form-dialog"); const header = element("header", "opportunities-extension-account-plans-header"); const heading = element("div", "opportunities-extension-action-form-heading"); const title = element("h2", "", "Add Action"); title.id = "opportunities-extension-action-form-title"; heading.append(title, element("p", "", String(optyNumber))); const opportunity = getOpportunity(optyNumber); const details = element("dl", "opportunities-extension-action-form-context"); for (const [label, value] of [ ["Customer", opportunity?.CustomerAccount?.PartyUniqueName], ["Name", opportunity?.Name], ["Close Date", formatDate(opportunity?.EffectiveDate)] ]) { details.append(element("dt", "", label), element("dd", "", value == null || value === "" ? "-" : String(value))); } heading.append(details); const dismiss = element("button", "opportunities-extension-icon-button"); dismiss.type = "button"; dismiss.title = "Close"; dismiss.setAttribute("aria-label", "Close Action form"); dismiss.append(createCloseIcon()); dismiss.addEventListener("click", () => close()); header.append(heading, dismiss); const form = element("form", "opportunities-extension-action-form"); form.noValidate = true; const workloads = [...new Map(getProducts(optyNumber).filter(product => /^\d+$/.test(String(product?.RevnId ?? ""))) .map(product => [String(product.RevnId), product])).entries()]; const workloadField = element("label", "opportunities-extension-action-form-field opportunities-extension-action-form-wide"); const workload = element("select"); workload.id = "opportunities-extension-action-form-workload"; workloadField.htmlFor = workload.id; function option(select, value, label) { const item = element("option", "", label); item.value = value; select.append(item); } option(workload, "", "Select workload"); for (const [id, product] of workloads) option(workload, id, `${product.WorkloadName_c || product.ProdGroupName || "Workload"} (${id})`); workload.value = workloads.length === 1 ? workloads[0][0] : ""; workloadField.hidden = workloads.length <= 1; workloadField.append(element("span", "", "Workload"), workload); const status = element("p", "opportunities-extension-action-form-status opportunities-extension-action-form-wide"); status.setAttribute("role", "status"); form.append(workloadField, status); const searchable = new Map(); searchable.set(workload, searchableSelect(workload, workloadField, () => loadOptions())); const fields = []; const selects = []; const lookupKeys = { role: "WWB_ACTION_ROLE", action: "WWB_ACTION_NAME", team: "WWB_ACTION_TEAM", status: "WWB_ACTION_STATUS", complexity: "WWB_ACTION_COMPLEXITY" }; for (const [name, label, type] of [ ["owner", "Owner", "select"], ["role", "Role", "select"], ["action", "Action", "select"], ["team", "Team", "select"], ["startDate", "Start date", "date"], ["endDate", "End Date", "date"], ["status", "Status", "select"], ["complexity", "Complexity", "select"], ["notes", "Notes", "textarea"] ]) { const field = element("label", "opportunities-extension-action-form-field"); const input = element(type === "select" || type === "textarea" ? type : "input"); if (type === "select") selects.push({ input, label, name }); else if (type === "textarea") input.rows = 5; else input.type = type; input.disabled = true; input.required = name !== "notes"; input.setAttribute("aria-required", String(input.required)); input.name = name; input.id = `opportunities-extension-action-form-${name}`; field.htmlFor = input.id; if (["notes", "team", "action"].includes(name)) field.className += " opportunities-extension-action-form-wide"; field.append(element("span", "", `${label} (${input.required ? "required" : "optional"})`), input); if (type === "select") searchable.set(input, searchableSelect(input, field)); form.append(field); fields.push(input); } const footer = element("footer", "opportunities-extension-action-form-footer"); const save = element("button", "opportunities-extension-action-form-save", "Save"); save.type = "submit"; save.disabled = true; save.title = "Save Action"; const retry = element("button", "", "Retry"); retry.type = "button"; retry.hidden = true; const cancel = element("button", "", "Cancel"); cancel.type = "button"; cancel.addEventListener("click", () => close()); footer.append(retry, save, cancel); form.append(footer); dialog.append(header, form); modal.append(dialog); host.append(modal); const view = { modal, trigger, generation: 0, lookup: null, loadingId: null }; active = view; const isOpen = () => active === view && allowsCrmActivities(); let ready = false; function validate() { const invalid = []; const value = name => String(fields.find(field => field.name === name)?.value || "").trim(); for (const field of fields) { let valid = !field.required || !!value(field.name); if (field.required && searchable.has(field)) valid = Array.from(field.children).some(option => option.value && option.value === field.value); if (field.type === "date") { const date = value(field.name); valid = /^\d{4}-\d{2}-\d{2}$/.test(date) && Number.isFinite(Date.parse(`${date}T00:00:00.000Z`)); if (valid) valid = new Date(`${date}T00:00:00.000Z`).toISOString().slice(0, 10) === date; if (field.name === "endDate" && value("startDate") > date) valid = false; } const control = searchable.get(field)?.input || field; control.setAttribute("aria-invalid", String(!valid)); if (!valid) invalid.push(control); } return { invalid, value }; } form.addEventListener("submit", async event => { event.preventDefault(); if (!ready || !isOpen() || view.saving) return; const { invalid, value } = validate(); if (invalid.length) { status.hidden = false; status.setAttribute("role", "alert"); status.textContent = "Complete the highlighted required fields. End date must be on or after start date."; invalid[0].focus(); return; } const userId = getCurrentUserId(); if (!userId) { notify("Unable to save: current user is unavailable.", true); return; } const payload = { parentType: "WORKLOAD", parentId: workload.value, action: { ownerEmail: value("owner"), role: value("role"), name: value("action"), team: value("team"), tags: "", startDate: `${value("startDate")}T00:00:00.000Z`, endDate: `${value("endDate")}T00:00:00.000Z`, status: value("status"), complexity: value("complexity"), notes: value("notes") ? [value("notes")] : [], objective: "", createdBy: userId, updatedBy: userId } }; view.saving = true; save.disabled = cancel.disabled = dismiss.disabled = workload.disabled = true; fields.forEach(field => { field.disabled = true; }); searchable.forEach(control => control.sync()); save.textContent = "Saving..."; form.setAttribute("aria-busy", "true"); status.hidden = true; try { const result = await saveAction(payload, isOpen); if (!result) return; view.saving = false; close(); try { await onActionSaved(optyNumber, payload.parentId, result.result, payload.action); } catch { notify("Action saved, but the local badge could not be updated.", true); return; } notify("Action saved successfully."); } catch (error) { status.hidden = false; status.setAttribute("role", "alert"); status.textContent = error.message || "Unable to save Action."; notify(status.textContent, true); } finally { view.saving = false; if (active === view) { save.disabled = cancel.disabled = dismiss.disabled = workload.disabled = false; fields.forEach(field => { field.disabled = false; }); searchable.forEach(control => control.sync()); save.textContent = "Save"; form.setAttribute("aria-busy", "false"); } } }); const settled = promise => promise.then(result => ({ result }), error => ({ error })); async function loadOptions(refresh = false) { const id = workload.value; if (!isOpen() || (view.loadingId === id && !refresh)) return; const generation = ++view.generation; const isCurrent = () => isOpen() && view.generation === generation; ready = false; save.disabled = true; retry.hidden = true; status.hidden = false; status.setAttribute("role", "status"); fields.forEach(input => { input.disabled = true; }); selects.forEach(({ input }) => { input.textContent = ""; option(input, "", "Loading..."); }); const syncSearch = () => selects.forEach(({ input }) => searchable.get(input).sync()); syncSearch(); if (!workloads.length) { form.setAttribute("data-state", "error"); form.setAttribute("aria-busy", "false"); status.setAttribute("role", "alert"); status.textContent = "No product with a valid RevnId is available for this opportunity."; selects.forEach(({ input }) => { input.textContent = ""; option(input, "", "Unavailable"); }); syncSearch(); return; } if (!view.lookup || refresh) view.lookup = settled(requestLookup(isOpen)); if (!workloads.some(([value]) => value === id)) { view.loadingId = null; form.setAttribute("data-state", "waiting"); form.setAttribute("aria-busy", "false"); status.textContent = "Select a workload to load Action options."; return; } view.loadingId = id; form.setAttribute("data-state", "loading"); form.setAttribute("aria-busy", "true"); status.textContent = "Loading Action options..."; try { const [lookup, team] = await Promise.all([view.lookup, settled(requestTeamMembers(id, isCurrent))]); if (!isCurrent()) return; if (lookup.error || team.error) throw lookup.error || team.error; const values = selects.map(({ name }) => name === "owner" ? team.result.members.map(({ email }) => ({ code: email, name: email })) : lookup.result.lookups[lookupKeys[name]]); const defaults = { owner: getCurrentUserId(), role: "Cloud Architect", action: "Success Plan & Consumption review", status: "Complete" }; const normalize = value => String(value ?? "").trim().toLowerCase(); for (const [index, { input, label, name }] of selects.entries()) { input.textContent = ""; option(input, "", values[index].length ? `Select ${label.toLowerCase()}` : "No options available"); for (const item of values[index]) option(input, item.code, item.name); const match = defaults[name] && values[index].find(item => normalize(name === "owner" ? item.code : item.name) === normalize(defaults[name])); input.value = match ? match.code : ""; } fields.forEach(input => { input.disabled = false; }); selects.forEach(({ input }, index) => { input.disabled = !values[index].length; }); form.setAttribute("data-state", "ready"); ready = true; save.disabled = false; status.hidden = true; syncSearch(); if (document.activeElement === dismiss) { const first = fields.find(field => !field.disabled); (searchable.get(first)?.input || first)?.focus(); } } catch (error) { if (!isCurrent()) return; form.setAttribute("data-state", "error"); status.setAttribute("role", "alert"); status.textContent = error.message || "Unable to load Action options."; selects.forEach(({ input }) => { input.textContent = ""; option(input, "", "Unavailable"); }); syncSearch(); retry.hidden = false; } finally { if (isCurrent()) { view.loadingId = null; form.setAttribute("aria-busy", "false"); } } } retry.addEventListener("click", () => { if (!view.loadingId) loadOptions(true); }); workload.addEventListener("change", () => loadOptions()); modal.addEventListener("click", event => { if (event.target === modal) close(); }); modal.addEventListener("keydown", event => { if (event.key !== "Tab") return; const controls = [dismiss, ...(!workloadField.hidden ? [workload] : []), ...fields, retry, save, cancel] .map(control => searchable.get(control)?.input || control) .filter(control => !control.disabled && !control.hidden); const index = controls.indexOf(document.activeElement); if (!controls.length) { event.preventDefault(); return; } if (index === -1 || (!event.shiftKey && index === controls.length - 1) || (event.shiftKey && index === 0)) { event.preventDefault(); controls[event.shiftKey ? controls.length - 1 : 0].focus(); } }); if (!workloadField.hidden) searchable.get(workload).input.focus(); else dismiss.focus(); loadOptions(); } return { createButton, open, close }; }