Implemented the functionality to create Service Requests

This commit is contained in:
2026-10-05 16:30:29 -03:00
parent e9c295be91
commit 7a7143b6e7
24 changed files with 1579 additions and 46 deletions

View File

@@ -0,0 +1,82 @@
function createSrRequestHeaders({ runtimeApi }) {
const origin = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com";
const appPath = "/ic/builder/rt/SeaaS_initiation/";
const storageKey = "opportunitiesExtension.srAppBuilderClientId.v1";
const maxAge = 60 * 60 * 1000;
let observed = null;
let started = false;
function valid(record) {
return record?.origin === origin && typeof record.value === "string"
&& /^[\x21-\x7e]{1,2048}$/.test(record.value)
&& Number.isFinite(record.observedAt) && record.observedAt <= Date.now()
&& Date.now() - record.observedAt < maxAge;
}
function observeHeaders(rawUrl, headers) {
let url;
try { url = new URL(rawUrl); } catch { return; }
if (url.origin !== origin || !/^\/ic\/builder\/rt\/SeaaS_initiation\/live(?:\/|;)/.test(url.pathname)) return;
// Read only the client ID, not cookies, bodies or authorization.
const value = typeof headers?.get === "function" ? headers.get("x-appbuilder-client-id")
: headers?.find(item => item.name.toLowerCase() === "x-appbuilder-client-id")?.value;
const record = { origin, value: value?.trim(), observedAt: Date.now() };
if (!valid(record)) return;
observed = record;
// Session storage survives service-worker suspension without persisting across browser sessions.
runtimeApi.storage?.session?.set({ [storageKey]: record }).catch(() => {});
}
function start() {
if (started || !runtimeApi?.webRequest) return;
started = true;
const filter = { urls: [`${origin}${appPath}*`], types: ["xmlhttprequest"] };
runtimeApi.webRequest.onBeforeSendHeaders?.addListener(details => {
if (details.tabId >= 0 && !details.incognito) observeHeaders(details.url, details.requestHeaders);
}, filter, ["requestHeaders"]);
runtimeApi.webRequest.onHeadersReceived?.addListener(details => {
if (!details.incognito) observeHeaders(details.url, details.responseHeaders);
}, filter, ["responseHeaders"]);
}
async function previewTokenHeaders(url) {
const target = new URL(url);
if (target.origin !== origin || !target.pathname.startsWith(appPath) || !/\/Catalogue_c(?:\/\d+)?$/.test(target.pathname)) {
throw new Error("Unexpected Service Request token URL.");
}
if (!valid(observed)) {
try {
const stored = await runtimeApi.storage?.session?.get(storageKey);
if (valid(stored?.[storageKey]) && !valid(observed)) observed = stored[storageKey];
} catch { /* A missing capture must not prevent trying the other required headers. */ }
}
return {
Accept: "application/json", "Content-Type": "application/vnd.oracle.adf.action+json",
Authorization: "Session", Origin: target.origin,
...(valid(observed) ? { "x-appbuilder-client-id": observed.value } : {})
};
}
async function tokenHeaders(url, serviceRequestUrl) {
const headers = await previewTokenHeaders(url);
if (!/\/Catalogue_c\/\d+$/.test(new URL(url).pathname)) throw new Error("Missing Catalogue Id.");
const serviceTarget = new URL(serviceRequestUrl);
if (serviceTarget.origin !== headers.Origin || !serviceTarget.pathname.startsWith(appPath)
|| !serviceTarget.pathname.endsWith("/set/seaas/mgmt/serviceRequests") || serviceTarget.search || serviceTarget.hash) {
throw new Error("Unexpected Service Request URL.");
}
if (!runtimeApi?.declarativeNetRequest?.updateSessionRules || !runtimeApi.runtime?.getURL) {
throw new Error("Reload the extension to enable the Service Request token header permissions.");
}
const extensionDomain = new URL(runtimeApi.runtime.getURL("/")).hostname;
const catalogUrl = url.slice(0, url.lastIndexOf("/") + 1);
const escapedCatalogUrl = catalogUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const escapedServiceUrl = serviceTarget.href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
await runtimeApi.declarativeNetRequest.updateSessionRules({
removeRuleIds: [7201, 7202],
addRules: [`^${escapedCatalogUrl}[0-9]+$`, `^${escapedServiceUrl}$`].map((regexFilter, index) => ({
id: 7201 + index, priority: 1,
action: { type: "modifyHeaders", requestHeaders: [{ header: "Origin", operation: "set", value: headers.Origin }] },
condition: { regexFilter, initiatorDomains: [extensionDomain],
requestMethods: ["post"], resourceTypes: ["xmlhttprequest"] }
}))
});
return headers;
}
return { start, tokenHeaders, previewTokenHeaders, observeResponseHeaders: observeHeaders };
}

View File

@@ -56,6 +56,7 @@ function createBackgroundApplication(overrides = {}) {
});
const spaResponseShapes = createSpaResponseShapes();
const httpBackgroundJson = createHttpBackgroundJson({
observeResponseHeaders: (...args) => srRequestHeaders.observeResponseHeaders(...args),
previewDebugBody: createDebugBodyPreview,
DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT,
environment
@@ -125,6 +126,12 @@ function createBackgroundApplication(overrides = {}) {
const requestRatecardByPlan = (...args) => spaConsumption.requestRatecardByPlan(...args);
const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args);
const runtimeApi = environment.browser || environment.chrome;
const srRequestHeaders = createSrRequestHeaders({ runtimeApi });
const fusionSrCreate = createFusionSrCreate({
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args),
tokenHeaders: (...args) => srRequestHeaders.tokenHeaders(...args),
previewTokenHeaders: url => srRequestHeaders.previewTokenHeaders(url)
});
const fusionSrDetails = createFusionSrDetails({
cookieChanges: runtimeApi?.cookies?.onChanged,
cookiesGetAll: (...args) => platformBackgroundApi.cookiesGetAll(...args),
@@ -135,7 +142,8 @@ function createBackgroundApplication(overrides = {}) {
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args)
});
function start() {
startBackgroundApplication({ prepareSrSession: sender => fusionSrDetails.prepareSession(sender), requestSrLookupWithSession: (...args) => fusionSrDetails.requestLookupWithSession(...args), messageTypes, requestSrLookupLanguage: () => fusionSrDetails.requestSrLookupLanguage(), requestSrLookupTechPlatform: () => fusionSrDetails.requestSrLookupTechPlatform(), requestSrLookupOpportunityBusiness: message => fusionSrDetails.requestSrLookupOpportunityBusiness(message), requestSrLookupPrimaryPillar: () => fusionSrDetails.requestSrLookupPrimaryPillar(), requestInternalServiceRequest: (message, sender) => fusionSrDetails.requestInternalServiceRequest(message, sender), requestMgmtAction: message => spaActionOptions.requestMgmtAction(message), invalidateProductsCache: () => productsCache.invalidate(), requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestActionTeamMembers, requestActionFindLookup, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi });
srRequestHeaders.start();
startBackgroundApplication({ requestPostServiceRequest: message => fusionSrCreate.requestPostServiceRequest(message), prepareSrSession: sender => fusionSrDetails.prepareSession(sender), requestSrLookupWithSession: (...args) => fusionSrDetails.requestLookupWithSession(...args), messageTypes, requestSrLookupLanguage: () => fusionSrDetails.requestSrLookupLanguage(), requestSrLookupTechPlatform: () => fusionSrDetails.requestSrLookupTechPlatform(), requestSrLookupOpportunityBusiness: message => fusionSrDetails.requestSrLookupOpportunityBusiness(message), requestSrLookupPrimaryPillar: () => fusionSrDetails.requestSrLookupPrimaryPillar(), requestInternalServiceRequest: (message, sender) => fusionSrDetails.requestInternalServiceRequest(message, sender), requestMgmtAction: message => spaActionOptions.requestMgmtAction(message), invalidateProductsCache: () => productsCache.invalidate(), requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestActionTeamMembers, requestActionFindLookup, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi });
}
return {
start, contracts,

View File

@@ -11,6 +11,7 @@ function registerBackgroundMessages({
requestActionTeamMembers,
requestActionFindLookup,
requestMgmtAction,
requestPostServiceRequest,
requestInternalServiceRequest,
requestSrLookupPrimaryPillar,
requestSrLookupWithSession,
@@ -30,6 +31,11 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (!message || !message.type) {
return false;
}
if (message.type === messageTypes.requestPostServiceRequest) {
requestPostServiceRequest(message).then(sendResponse).catch(error => sendResponse({ ok: false,
error: error.message, debugRequests: error.debugRequests || [] }));
return true;
}
if (message.type === messageTypes.prepareSrSession) {
prepareSrSession(sender).then(sendResponse).catch(error => sendResponse({ ok: false,
error: error.message, debugRequests: error.debugRequests || [] }));

View File

@@ -240,6 +240,13 @@ function createContentApplication(overrides = {}) {
return result.items;
}
const crmSrForm = createCrmSrForm({
requestSave: async fields => {
const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestPostServiceRequest, ...fields, emailAddress: currentUser.getUserId() });
debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []);
if (!result?.ok) throw new Error(result?.error || "Unable to save Service Request.");
return result;
},
notify: (...args) => opportunitiesLinks.showCopyConfirmation(...args),
getOpportunity: number => opportunitiesTableState.opportunitiesTableState.items.find(item => String(item.OptyNumber) === String(number)),
formatDate: value => opportunitiesFormatters.formatOracleResponseDate(value),
requestLanguage: () => requestSrFormLookup(messageTypes.requestSrLookupLanguage),

View File

@@ -26,7 +26,7 @@ function createDebugContentView({
if (["requestWlActions", "requestActionTeamMembers", "requestActionFindLookup", "requestMgmtAction"].includes(entry.name)) return "actions";
if (entry.name === "requestGenerateAiSummary") return "ai";
if (entry.name === "requestInstallBase") return "installBase";
if (["requestCustomerServiceRequest", "resquestCrmServiceRequest", "requestInternalServiceRequest", "requestSrLookupPrimaryPillar", "requestSrLookupLanguage", "requestSrLookupTechPlatform", "requestSrLookupOpportunityBusiness"].includes(entry.name)) return "serviceRequests";
if (["requestCustomerServiceRequest", "resquestCrmServiceRequest", "requestInternalServiceRequest", "requestSrLookupPrimaryPillar", "requestSrLookupLanguage", "requestSrLookupTechPlatform", "requestSrLookupOpportunityBusiness", "requestResourceUser", "requestSrCatalog", "requestSrTokenCatalog", "requestPostServiceRequest"].includes(entry.name)) return "serviceRequests";
if (/^request(?:AccountSummary|AccountPlans|ConsumptionService|ConsumptionPlanDetail|RatecardByPlan)/.test(entry.name)) return "consumption";
return "other";
}

View File

@@ -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();

View File

@@ -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; }

View File

@@ -150,7 +150,8 @@ function createFusionOpportunities({
"OptyId",
"CustomerAccount.PartyUniqueName",
"CustomerAccount.PartyId",
"CustomerAccount.PartyNumber"
"CustomerAccount.PartyNumber",
"CustomerAccount.Region_c"
],
sort: [],
language: "en",

View File

@@ -0,0 +1,88 @@
function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders }) {
const base = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_initiation/live;profile=PROD/services/auth/1.1/proxy/";
const catalog = base + "crmRestApiResources/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/11.13.18.05/Catalogue_c";
const resourceUsers = base + "oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/resourceUsers";
const endpoint = base + "oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/mgmt/serviceRequests";
function numericId(value, name) {
if (!/^\d+$/.test(String(value ?? "")) || !Number.isSafeInteger(Number(value)) || Number(value) <= 0) throw new Error(`Missing or invalid ${name}.`);
return Number(value);
}
function text(value, name) {
if (typeof value !== "string" || !value.trim()) throw new Error(`Missing ${name}.`);
return value.trim();
}
function payloadFor(input) {
const payload = {};
for (const name of ["AccountPartyId", "OpportunityName_Id_c", "ServiceNew_Id_c"]) payload[name] = numericId(input?.[name], name);
for (const name of ["Title", "RegionText_c", "ProblemDescription", "Language_c", "CustomerDeadline_c", "TechPlatformLookUp_c", "CountryText_c", "PrimaryPillarNew_c"]) payload[name] = text(input?.[name], name);
const date = payload.CustomerDeadline_c;
if (!/^\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) throw new Error("Invalid Customer deadline.");
return { ...payload, SvcQuestionsCollection_c: [], SourceCd: "SVC_OPPORTUNITY_SCREEN", SeverityCd: "ORA_SVC_SEV2", ChannelTypeCd: "ORA_SVC_WEB" };
}
async function requestPostServiceRequest(message) {
const debugRequests = [];
try {
if (message.phase !== "prepare" && message.phase !== "confirm") throw new Error("Service Request confirmation is required.");
const payload = payloadFor(message.payload);
if (message.phase === "prepare") {
const emailAddress = text(message.emailAddress, "current user email");
const resource = await fetchJson(`${resourceUsers}?emailAddress=${encodeURIComponent(emailAddress)}`, {
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
}, "requestResourceUser", debugRequests);
payload.ReportedByPartyId = numericId(resource?.resourcePartyId, "resourcePartyId");
const sharedHeaders = await previewTokenHeaders(catalog);
return { ok: true, payload, debugRequests, headers: {
requestSrTokenCatalog: sharedHeaders,
requestPostServiceRequest: { ...sharedHeaders, "vb-proxy-header-x-oracle-ectoken": "(generated after confirmation)" }
} };
}
payload.ReportedByPartyId = numericId(message.payload?.ReportedByPartyId, "ReportedByPartyId");
let id = message.catalogId;
if (id == null) {
const result = await fetchJson(`${catalog}?onlyData=true&fields=Id&limit=1`, {
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
}, "requestSrCatalog", debugRequests);
id = result?.items?.[0]?.Id;
}
id = numericId(id, "Catalogue Id");
const tokenUrl = `${catalog}/${id}`;
let catalogHeaders = await tokenHeaders(tokenUrl, endpoint);
const requestToken = () => fetchJson(tokenUrl, {
method: "POST", credentials: "include", headers: catalogHeaders, body: JSON.stringify({ name: "getJwtToken" })
}, "requestSrTokenCatalog", debugRequests);
let token;
try {
token = await requestToken();
} catch (error) {
if (error.httpStatus !== 403) throw error;
const refreshed = await previewTokenHeaders(tokenUrl);
if (!refreshed["x-appbuilder-client-id"] || refreshed["x-appbuilder-client-id"] === catalogHeaders["x-appbuilder-client-id"]) throw error;
catalogHeaders = refreshed;
token = await requestToken();
}
if (debugRequests.at(-1)?.httpStatus !== 200 || typeof token?.result !== "string" || !token.result.trim()) {
throw new Error("requestSrTokenCatalog did not return HTTP 200 with a token.");
}
const result = await fetchJson(endpoint, {
method: "POST", credentials: "include", headers: { ...catalogHeaders, "vb-proxy-header-x-oracle-ectoken": token.result.trim() },
body: JSON.stringify(payload)
}, "requestPostServiceRequest", debugRequests, { allowEmpty: true });
return { ok: true, result, debugRequests };
} catch (error) {
error.debugRequests = debugRequests;
throw error;
} finally {
// Never forward the signing token to content scripts, even with Debug secrets enabled.
for (const entry of debugRequests) {
if (entry.name === "requestSrTokenCatalog") {
entry.metadata.appBuilderClientId = entry.requestHeaders?.["x-appbuilder-client-id"] ? "Observed in SeaaS requests" : "Not observed in SeaaS requests";
entry.responseBody = JSON.stringify({ result: "(redacted)" });
entry.responseBodyTruncated = false;
}
if (entry.requestHeaders?.["vb-proxy-header-x-oracle-ectoken"]) entry.requestHeaders["vb-proxy-header-x-oracle-ectoken"] = "(redacted)";
}
}
}
return { requestPostServiceRequest };
}

View File

@@ -1,6 +1,7 @@
function createHttpBackgroundJson({
previewDebugBody,
DEBUG_BODY_LIMIT,
observeResponseHeaders = () => {},
environment
}) {
const { fetch } = environment;
@@ -17,6 +18,7 @@ function createHttpBackgroundJson({
throw error;
}
observeResponseHeaders(response.url || url, response.headers);
const responsePayload = await readResponsePayload(response);
const responseBody = createDebugBody(responsePayload.text);
completeBackgroundDebugEntry(debugEntry, response, responseBody, "");

View File

@@ -16,6 +16,7 @@ function createMessageTypes() {
requestMgmtAction: "opportunitiesExtension.requestMgmtAction",
requestInternalServiceRequest: "opportunitiesExtension.requestInternalServiceRequest",
requestSrLookupPrimaryPillar: "opportunitiesExtension.requestSrLookupPrimaryPillar",
requestPostServiceRequest: "opportunitiesExtension.requestPostServiceRequest",
prepareSrSession: "opportunitiesExtension.prepareSrSession",
requestSrLookupLanguage: "opportunitiesExtension.requestSrLookupLanguage",
requestSrLookupTechPlatform: "opportunitiesExtension.requestSrLookupTechPlatform",