Implemented the Create Internal Service Request functionality
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
function createSrRequestHeaders({ runtimeApi }) {
|
||||
const origin = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com";
|
||||
const appPath = "/ic/builder/rt/SeaaS_initiation/";
|
||||
const internalPath = "/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPICloudAccount/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests";
|
||||
const storageKey = "opportunitiesExtension.srAppBuilderClientId.v1";
|
||||
const maxAge = 60 * 60 * 1000;
|
||||
let observed = null;
|
||||
@@ -14,7 +15,7 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
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;
|
||||
if (url.origin !== origin || !/^\/ic\/builder\/rt\/(?:SeaaS_initiation|SeaaS_SR_management)\/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;
|
||||
@@ -27,7 +28,7 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
function start() {
|
||||
if (started || !runtimeApi?.webRequest) return;
|
||||
started = true;
|
||||
const filter = { urls: [`${origin}${appPath}*`], types: ["xmlhttprequest"] };
|
||||
const filter = { urls: [`${origin}${appPath}*`, `${origin}/ic/builder/rt/SeaaS_SR_management/*`], types: ["xmlhttprequest"] };
|
||||
runtimeApi.webRequest.onBeforeSendHeaders?.addListener(details => {
|
||||
if (details.tabId >= 0 && !details.incognito) observeHeaders(details.url, details.requestHeaders);
|
||||
}, filter, ["requestHeaders"]);
|
||||
@@ -40,6 +41,9 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
if (target.origin !== origin || !target.pathname.startsWith(appPath) || !/\/Catalogue_c(?:\/\d+)?$/.test(target.pathname)) {
|
||||
throw new Error("Unexpected Service Request token URL.");
|
||||
}
|
||||
return sessionHeaders(target.origin, "application/vnd.oracle.adf.action+json");
|
||||
}
|
||||
async function sessionHeaders(targetOrigin, contentType) {
|
||||
if (!valid(observed)) {
|
||||
try {
|
||||
const stored = await runtimeApi.storage?.session?.get(storageKey);
|
||||
@@ -47,8 +51,8 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
} 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,
|
||||
Accept: "application/json", "Content-Type": contentType,
|
||||
Authorization: "Session", Origin: targetOrigin,
|
||||
...(valid(observed) ? { "x-appbuilder-client-id": observed.value } : {})
|
||||
};
|
||||
}
|
||||
@@ -56,8 +60,8 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
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) {
|
||||
const isServiceRequest = serviceTarget.pathname.startsWith(appPath) && serviceTarget.pathname.endsWith("/set/seaas/mgmt/serviceRequests");
|
||||
if (serviceTarget.origin !== headers.Origin || !isServiceRequest || serviceTarget.search || serviceTarget.hash) {
|
||||
throw new Error("Unexpected Service Request URL.");
|
||||
}
|
||||
if (!runtimeApi?.declarativeNetRequest?.updateSessionRules || !runtimeApi.runtime?.getURL) {
|
||||
@@ -67,10 +71,11 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
const catalogUrl = url.slice(0, url.lastIndexOf("/") + 1);
|
||||
const escapedCatalogUrl = catalogUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const escapedServiceUrl = serviceTarget.href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const ruleIds = [7201, 7202];
|
||||
await runtimeApi.declarativeNetRequest.updateSessionRules({
|
||||
removeRuleIds: [7201, 7202],
|
||||
removeRuleIds: ruleIds,
|
||||
addRules: [`^${escapedCatalogUrl}[0-9]+$`, `^${escapedServiceUrl}$`].map((regexFilter, index) => ({
|
||||
id: 7201 + index, priority: 1,
|
||||
id: ruleIds[index], priority: 1,
|
||||
action: { type: "modifyHeaders", requestHeaders: [{ header: "Origin", operation: "set", value: headers.Origin }] },
|
||||
condition: { regexFilter, initiatorDomains: [extensionDomain],
|
||||
requestMethods: ["post"], resourceTypes: ["xmlhttprequest"] }
|
||||
@@ -78,5 +83,27 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
});
|
||||
return headers;
|
||||
}
|
||||
return { start, tokenHeaders, previewTokenHeaders, observeResponseHeaders: observeHeaders };
|
||||
async function internalServiceRequestHeaders(url) {
|
||||
const target = new URL(url);
|
||||
if (target.origin !== origin || target.pathname !== internalPath || target.search || target.hash) {
|
||||
throw new Error("Unexpected Internal Service Request URL.");
|
||||
}
|
||||
if (!runtimeApi?.declarativeNetRequest?.updateSessionRules || !runtimeApi.runtime?.getURL) {
|
||||
throw new Error("Reload the extension to enable the Internal Service Request header permissions.");
|
||||
}
|
||||
const headers = await sessionHeaders(target.origin, "application/json");
|
||||
const extensionDomain = new URL(runtimeApi.runtime.getURL("/")).hostname;
|
||||
const escapedUrl = target.href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
await runtimeApi.declarativeNetRequest.updateSessionRules({
|
||||
removeRuleIds: [7203],
|
||||
addRules: [{
|
||||
id: 7203, priority: 1,
|
||||
action: { type: "modifyHeaders", requestHeaders: [{ header: "Origin", operation: "set", value: headers.Origin }] },
|
||||
condition: { regexFilter: `^${escapedUrl}$`, initiatorDomains: [extensionDomain],
|
||||
requestMethods: ["post"], resourceTypes: ["xmlhttprequest"] }
|
||||
}]
|
||||
});
|
||||
return headers;
|
||||
}
|
||||
return { start, tokenHeaders, previewTokenHeaders, internalServiceRequestHeaders, observeResponseHeaders: observeHeaders };
|
||||
}
|
||||
|
||||
@@ -92,10 +92,7 @@ function createAuthTokenRelay({
|
||||
throw new Error("Token relay response does not contain access_token.");
|
||||
}
|
||||
|
||||
logRequestSuccess(requestEntry, response, createDebugBody(JSON.stringify({
|
||||
...responseData,
|
||||
access_token: maskToken(responseData.access_token)
|
||||
}, null, 2)));
|
||||
logRequestSuccess(requestEntry, response, responseBody);
|
||||
|
||||
if (url === TOKEN_RELAY_URL) {
|
||||
authSessionState.principal = typeof responseData.principal === "string" ? responseData.principal.trim() : "";
|
||||
@@ -122,7 +119,7 @@ function createAuthTokenRelay({
|
||||
method: "GET",
|
||||
url,
|
||||
headers: {
|
||||
"x-xsrf-token": xsrfTokenSource.token ? maskToken(xsrfTokenSource.token) : "(missing)"
|
||||
"x-xsrf-token": xsrfTokenSource.token || "(missing)"
|
||||
},
|
||||
metadata: {
|
||||
xsrfCookieName: xsrfTokenSource.cookieName || "(missing)",
|
||||
|
||||
@@ -58,7 +58,6 @@ function createBackgroundApplication(overrides = {}) {
|
||||
const httpBackgroundJson = createHttpBackgroundJson({
|
||||
observeResponseHeaders: (...args) => srRequestHeaders.observeResponseHeaders(...args),
|
||||
previewDebugBody: createDebugBodyPreview,
|
||||
DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT,
|
||||
environment
|
||||
});
|
||||
const productsBackgroundJobs = createProductsBackgroundJobs({
|
||||
@@ -130,6 +129,7 @@ function createBackgroundApplication(overrides = {}) {
|
||||
const fusionSrCreate = createFusionSrCreate({
|
||||
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args),
|
||||
tokenHeaders: (...args) => srRequestHeaders.tokenHeaders(...args),
|
||||
internalServiceRequestHeaders: url => srRequestHeaders.internalServiceRequestHeaders(url),
|
||||
previewTokenHeaders: url => srRequestHeaders.previewTokenHeaders(url)
|
||||
});
|
||||
const fusionSrDetails = createFusionSrDetails({
|
||||
@@ -143,7 +143,11 @@ function createBackgroundApplication(overrides = {}) {
|
||||
});
|
||||
function start() {
|
||||
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 });
|
||||
startBackgroundApplication({
|
||||
requestLookupTypePlatform: () => fusionSrDetails.requestLookupTypePlatform(),
|
||||
requestPostInternalServiceRequest: message => fusionSrCreate.requestPostInternalServiceRequest(message),
|
||||
requestInternalSrServices: message => fusionSrDetails.requestInternalSrServices(message),
|
||||
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,
|
||||
|
||||
@@ -12,12 +12,15 @@ function registerBackgroundMessages({
|
||||
requestActionFindLookup,
|
||||
requestMgmtAction,
|
||||
requestPostServiceRequest,
|
||||
requestPostInternalServiceRequest,
|
||||
requestInternalServiceRequest,
|
||||
requestSrLookupPrimaryPillar,
|
||||
requestSrLookupWithSession,
|
||||
prepareSrSession,
|
||||
requestSrLookupLanguage,
|
||||
requestSrLookupTechPlatform,
|
||||
requestLookupTypePlatform,
|
||||
requestInternalSrServices,
|
||||
requestSrLookupOpportunityBusiness,
|
||||
requestCurrentUser,
|
||||
requestConsumerTokenService,
|
||||
@@ -31,6 +34,11 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (!message || !message.type) {
|
||||
return false;
|
||||
}
|
||||
if (message.type === messageTypes.requestPostInternalServiceRequest) {
|
||||
requestPostInternalServiceRequest(message).then(sendResponse).catch(error => sendResponse({ ok: false,
|
||||
error: error.message, debugRequests: error.debugRequests || [] }));
|
||||
return true;
|
||||
}
|
||||
if (message.type === messageTypes.requestPostServiceRequest) {
|
||||
requestPostServiceRequest(message).then(sendResponse).catch(error => sendResponse({ ok: false,
|
||||
error: error.message, debugRequests: error.debugRequests || [] }));
|
||||
@@ -42,6 +50,8 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
return true;
|
||||
}
|
||||
const srLookups = {
|
||||
[messageTypes.requestLookupTypePlatform]: requestLookupTypePlatform,
|
||||
[messageTypes.requestInternalSrServices]: requestInternalSrServices,
|
||||
[messageTypes.requestSrLookupPrimaryPillar]: requestSrLookupPrimaryPillar,
|
||||
[messageTypes.requestSrLookupLanguage]: requestSrLookupLanguage,
|
||||
[messageTypes.requestSrLookupTechPlatform]: requestSrLookupTechPlatform,
|
||||
|
||||
@@ -57,7 +57,7 @@ function createContentApplication(overrides = {}) {
|
||||
});
|
||||
const contentModal = createContentModal({
|
||||
prepareSrSession: () => srSessionPreparation.ensure(),
|
||||
closeActionForm: (...args) => opportunityCalendar.closeDetails(...args) || crmSrForm.close(...args) || workloadActionForm.close(...args),
|
||||
closeActionForm: (...args) => internalSrForm.close(...args) || opportunityCalendar.closeDetails(...args) || crmSrForm.close(...args) || workloadActionForm.close(...args),
|
||||
mountOpportunityViews: (...args) => opportunityCalendar.mount(...args),
|
||||
createActivityFilterControls: (...args) => activityFilters.createControls(...args),
|
||||
closeCrmSrPopover: () => crmSrPopover.close(),
|
||||
@@ -228,6 +228,8 @@ function createContentApplication(overrides = {}) {
|
||||
}
|
||||
});
|
||||
const crmSrPopover = createCrmSrPopover({
|
||||
openInternalSrForm: (parent, trigger) => internalSrForm.open(parent, trigger),
|
||||
formatDate: value => opportunitiesFormatters.formatOracleResponseDate(value),
|
||||
requestDetails: srId => internalSrSession.request(srId),
|
||||
MODAL_ID: contracts.MODAL_ID,
|
||||
createCopyButton: (...args) => opportunitiesLinks.createCopyOpportunityButton(...args),
|
||||
@@ -240,6 +242,20 @@ function createContentApplication(overrides = {}) {
|
||||
if (!result?.ok) throw new Error(result?.error || "Unable to load Service Request options.");
|
||||
return result.items;
|
||||
}
|
||||
const internalSrForm = createInternalSrForm({
|
||||
notify: (...args) => opportunitiesLinks.showCopyConfirmation(...args),
|
||||
requestSave: async payload => {
|
||||
const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestPostInternalServiceRequest, payload, emailAddress: currentUser.getUserId() });
|
||||
debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []);
|
||||
if (!result?.ok) throw new Error(result?.error || "Unable to create Internal Service Request.");
|
||||
return result.result;
|
||||
},
|
||||
getOpportunity: number => number == null ? null : opportunitiesTableState.opportunitiesTableState.items.find(item => String(item.OptyNumber) === String(number)),
|
||||
MODAL_ID: contracts.MODAL_ID, environment, searchableSelect,
|
||||
formatDate: value => opportunitiesFormatters.formatOracleResponseDate(value),
|
||||
requestPlatform: () => requestSrFormLookup(messageTypes.requestLookupTypePlatform),
|
||||
requestServices: parentServiceId => requestSrFormLookup(messageTypes.requestInternalSrServices, { parentServiceId, emailAddress: currentUser.getUserId() })
|
||||
});
|
||||
const crmSrForm = createCrmSrForm({
|
||||
requestSave: async fields => {
|
||||
const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestPostServiceRequest, ...fields, emailAddress: currentUser.getUserId() });
|
||||
@@ -261,8 +277,9 @@ function createContentApplication(overrides = {}) {
|
||||
requestPillars: () => requestSrFormLookup(messageTypes.requestSrLookupPrimaryPillar)
|
||||
});
|
||||
const opportunityCrmServiceRequests = createOpportunityCrmServiceRequests({
|
||||
formatDate: value => opportunitiesFormatters.formatOracleResponseDate(value),
|
||||
createSrButton: number => crmSrForm.createButton(number),
|
||||
closeSrForm: () => crmSrForm.close(false),
|
||||
closeSrForm: () => { internalSrForm.close(false); crmSrForm.close(false); },
|
||||
setBadgeContent: (...args) => opportunityBadgeContent.setContent(...args),
|
||||
activityCache,
|
||||
reportProgress: (...args) => activityProgress.update("sr", ...args),
|
||||
@@ -639,7 +656,6 @@ function createContentApplication(overrides = {}) {
|
||||
});
|
||||
const debugContentLog = createDebugContentLog({
|
||||
previewDebugBody: createDebugBodyPreview,
|
||||
DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT,
|
||||
debugState,
|
||||
renderDebugPanel: (...args) => debugContentView.renderDebugPanel(...args)
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
function createDebugContentLog({
|
||||
previewDebugBody,
|
||||
DEBUG_BODY_LIMIT,
|
||||
debugState,
|
||||
renderDebugPanel
|
||||
}) {
|
||||
@@ -99,7 +98,7 @@ function createDebugContentLog({
|
||||
}
|
||||
|
||||
function createDebugBody(text) {
|
||||
return previewDebugBody(text, DEBUG_BODY_LIMIT);
|
||||
return previewDebugBody(text);
|
||||
}
|
||||
|
||||
function headersToObject(headers) {
|
||||
@@ -126,7 +125,7 @@ function createDebugContentLog({
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function formatDebugBody(body) {
|
||||
function formatDebugBody(body, requestName = "") {
|
||||
const text = body || "(none)";
|
||||
try {
|
||||
const normalized = String(text).replace(/^\uFEFF/, "").trimStart()
|
||||
@@ -137,8 +136,14 @@ function createDebugContentLog({
|
||||
for (let depth = 0; depth < 2 && typeof value === "string" && /^[\s]*[\[{]/.test(value); depth += 1) {
|
||||
try { value = JSON.parse(value); } catch { break; }
|
||||
}
|
||||
if (!debugState.debugSecretsVisible && requestName === "requestSrTokenCatalog") {
|
||||
value = value && typeof value === "object" && !Array.isArray(value)
|
||||
? { ...value, ...(Object.hasOwn(value, "result") ? { result: formatHiddenDebugValue(value.result) } : {}) }
|
||||
: formatHiddenDebugValue(value);
|
||||
}
|
||||
return JSON.stringify(debugState.debugSecretsVisible ? value : maskSensitiveDebugValue(value), null, 2);
|
||||
} catch (error) {
|
||||
if (!debugState.debugSecretsVisible && /^(requestSrTokenCatalog|requestCurrentUser|requestTokenRelay.*)$/.test(requestName)) return formatHiddenDebugValue(text);
|
||||
return text;
|
||||
}
|
||||
}
|
||||
@@ -162,7 +167,7 @@ function createDebugContentLog({
|
||||
}
|
||||
|
||||
function isSensitiveDebugKey(key) {
|
||||
return /authorization|cookie|password|secret|(^|[_-])token($|[_-])|token$/i.test(key);
|
||||
return /authorization|cookie|password|secret|(^|[_-])token($|[_-])|token$|x-appbuilder-client-id/i.test(key);
|
||||
}
|
||||
|
||||
function formatHiddenDebugValue(value) {
|
||||
|
||||
@@ -26,6 +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 (["requestSRInfo", "requestLookupTypePlatform", "requestInternalSrServices", "requestPostInternalServiceRequest"].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";
|
||||
@@ -249,7 +250,7 @@ function createDebugContentView({
|
||||
appendDebugBlock(details, "Request Headers", formatHeaderBlock(entry.requestHeaders));
|
||||
appendDebugBlock(details, "Request Body", formatDebugBody(entry.requestBody));
|
||||
appendDebugBlock(details, "Response Headers", formatHeaderBlock(entry.responseHeaders));
|
||||
appendDebugBlock(details, "Response Body", formatDebugBody(entry.responseBody));
|
||||
appendDebugBlock(details, "Response Body", formatDebugBody(entry.responseBody, entry.name));
|
||||
|
||||
item.append(heading, details);
|
||||
list.append(item);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurrentPeriodRequest, bindPopover, closePopover, createSrButton, closeSrForm, allowsCrmActivities, reportProgress, activityCache, setBadgeContent, environment }) {
|
||||
function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurrentPeriodRequest, bindPopover, closePopover, createSrButton, closeSrForm, allowsCrmActivities, reportProgress, activityCache, setBadgeContent, formatDate, environment }) {
|
||||
let status = "idle";
|
||||
let byOpportunity = new Map();
|
||||
let pending = null;
|
||||
@@ -79,6 +79,7 @@ function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurre
|
||||
`Title: ${display(item.Title)}`,
|
||||
`Opportunity: ${display(item.OpportunityName_Id_c?.OptyNumber)}`,
|
||||
`Reported Hours: ${display(item.ReportedHours_c)}`,
|
||||
`Customer Deadline: ${display(formatDate(item.CustomerDeadline_c))}`,
|
||||
`Owner: ${[item.Owner?.PersonFirstName, item.Owner?.PersonLastName].filter(Boolean).join(" ") || "N/D"}`,
|
||||
`Email: ${display(item.Owner?.PrimaryEmail?.EmailAddress)}`,
|
||||
`Do Not Call: ${display(item.DoNotCallFlag)}`, `Do Not Email: ${display(item.DoNotEmailFlag)}`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, requestDetails, environment }) {
|
||||
function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, requestDetails, formatDate, openInternalSrForm, environment }) {
|
||||
const { document, window, setTimeout, clearTimeout } = environment;
|
||||
let active = null;
|
||||
let sequence = 0;
|
||||
@@ -91,6 +91,29 @@ function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, reque
|
||||
label.textContent = "SR Number:";
|
||||
heading.append(label, link);
|
||||
if (number) heading.append(createCopyButton(String(number), "SR number", "SR Number"));
|
||||
const add = document.createElement("button");
|
||||
add.type = "button";
|
||||
add.className = "opportunities-extension-copy-button opportunities-extension-internal-sr-add";
|
||||
add.title = "Create Internal Service Request";
|
||||
add.setAttribute("aria-label", add.title);
|
||||
add.setAttribute("aria-haspopup", "dialog");
|
||||
const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
icon.setAttribute("viewBox", "0 0 24 24");
|
||||
icon.setAttribute("aria-hidden", "true");
|
||||
icon.setAttribute("focusable", "false");
|
||||
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||
path.setAttribute("d", "M12 5v14M5 12h14");
|
||||
path.setAttribute("stroke", "currentColor");
|
||||
path.setAttribute("stroke-width", "2");
|
||||
path.setAttribute("stroke-linecap", "round");
|
||||
icon.append(path);
|
||||
add.append(icon);
|
||||
add.addEventListener("click", event => {
|
||||
event.preventDefault(); event.stopPropagation();
|
||||
close();
|
||||
openInternalSrForm(item, button);
|
||||
});
|
||||
heading.append(add);
|
||||
const details = document.createElement("div");
|
||||
details.className = "opportunities-extension-sr-details";
|
||||
details.textContent = descriptions[index];
|
||||
@@ -126,7 +149,7 @@ function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, reque
|
||||
body.className = "opportunities-extension-sr-details";
|
||||
body.textContent = ["Title", "OpportunityName_c", "StatusCd", "ServiceNew_c", "AccountPartyName",
|
||||
"LastUpdateDate", "SvcHealth_c", "AssigneePersonName", "OpenDate"].map(key =>
|
||||
`${key === "ServiceNew_c" ? "Service Queue" : key}: ${record[key] == null || record[key] === "" ? "N/D" : String(record[key])}`).join("\n");
|
||||
`${key === "ServiceNew_c" ? "Service Queue" : key}: ${record[key] == null || record[key] === "" ? "N/D" : String(key === "LastUpdateDate" || key === "OpenDate" ? formatDate(record[key]) : record[key])}`).join("\n");
|
||||
child.append(row, body);
|
||||
children.append(child);
|
||||
}
|
||||
|
||||
197
src/features/opportunities/internal-sr-form.js
Normal file
197
src/features/opportunities/internal-sr-form.js
Normal file
@@ -0,0 +1,197 @@
|
||||
function createInternalSrForm({ MODAL_ID, searchableSelect, formatDate, getOpportunity, requestPlatform, requestServices, requestSave, notify, environment }) {
|
||||
const { document } = environment;
|
||||
let active = null;
|
||||
function node(tag, className, text) {
|
||||
const el = document.createElement(tag);
|
||||
if (className) el.className = className;
|
||||
if (text !== undefined) el.textContent = text;
|
||||
return el;
|
||||
}
|
||||
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 open(parent, trigger) {
|
||||
if (active?.saving) return;
|
||||
const host = document.getElementById(MODAL_ID);
|
||||
if (!host || host.hidden) return;
|
||||
close(false);
|
||||
const modal = node("section", "opportunities-extension-action-form-modal");
|
||||
modal.setAttribute("role", "dialog");
|
||||
modal.setAttribute("aria-modal", "true");
|
||||
modal.setAttribute("aria-labelledby", "internal-sr-form-title");
|
||||
const dialog = node("div", "opportunities-extension-action-form-dialog opportunities-extension-internal-sr-dialog");
|
||||
const header = node("header", "opportunities-extension-account-plans-header");
|
||||
const heading = node("div", "opportunities-extension-action-form-heading");
|
||||
const title = node("h2", "", "Create Internal Service Request");
|
||||
title.id = "internal-sr-form-title";
|
||||
const context = node("dl", "opportunities-extension-action-form-context");
|
||||
const opportunity = getOpportunity(parent.OpportunityName_Id_c?.OptyNumber);
|
||||
for (const [label, value] of [["SR Number", parent.SrNumber], ["Opportunity", opportunity?.Name], ["Title", parent.Title]]) {
|
||||
context.append(node("dt", "", label), node("dd", "", value == null || value === "" ? "N/D" : String(value)));
|
||||
}
|
||||
heading.append(title, context);
|
||||
header.append(heading);
|
||||
const form = node("form", "opportunities-extension-action-form");
|
||||
form.noValidate = true;
|
||||
const fields = new Map();
|
||||
const combos = new Map();
|
||||
const ready = new Set();
|
||||
function field(name, label, type, wide = false) {
|
||||
const wrapper = node("label", "opportunities-extension-action-form-field" + (wide ? " opportunities-extension-action-form-wide" : ""));
|
||||
const input = node(type === "textarea" || type === "select" ? type : "input");
|
||||
if (!["textarea", "select"].includes(type)) input.type = type;
|
||||
if (type === "textarea") input.rows = 4;
|
||||
input.id = `internal-sr-${name}`;
|
||||
input.name = name;
|
||||
fields.set(name, input);
|
||||
input.required = name !== "customerDeadline";
|
||||
input.setAttribute("aria-required", String(input.required));
|
||||
wrapper.htmlFor = input.id;
|
||||
wrapper.append(node("span", "", input.required ? `${label} (required)` : label), input);
|
||||
form.append(wrapper);
|
||||
return { input, wrapper };
|
||||
}
|
||||
const titleInput = field("title", "Title", "text", true).input;
|
||||
const deadline = field("customerDeadline", "Customer Deadline", "text").input;
|
||||
deadline.readOnly = true;
|
||||
deadline.value = formatDate(parent.CustomerDeadline_c) || "N/D";
|
||||
field("internalDeadline", "Internal Deadline", "date");
|
||||
const platform = field("platform", "Platform", "select", true);
|
||||
field("description", "Description", "textarea", true);
|
||||
const services = field("services", "Services", "select", true);
|
||||
const footer = node("footer", "opportunities-extension-action-form-footer");
|
||||
const cancel = node("button", "", "Cancel");
|
||||
const save = node("button", "opportunities-extension-action-form-save", "Save");
|
||||
save.type = "submit";
|
||||
save.disabled = true;
|
||||
const feedback = node("p", "opportunities-extension-action-form-status opportunities-extension-action-form-wide");
|
||||
feedback.setAttribute("role", "status");
|
||||
feedback.hidden = true;
|
||||
cancel.type = "button";
|
||||
cancel.addEventListener("click", () => close());
|
||||
footer.append(save, cancel);
|
||||
form.append(feedback, footer);
|
||||
dialog.append(header, form);
|
||||
modal.append(dialog);
|
||||
host.append(modal);
|
||||
const entry = { modal, trigger, saving: false };
|
||||
active = entry;
|
||||
const validDate = value => /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(`${value}T00:00:00Z`))
|
||||
&& new Date(`${value}T00:00:00Z`).toISOString().slice(0, 10) === value;
|
||||
form.addEventListener("submit", async event => {
|
||||
event.preventDefault();
|
||||
if (active !== entry || entry.saving || ready.size !== 2) return;
|
||||
let firstInvalid = null;
|
||||
for (const [name, input] of fields) {
|
||||
if (!input.required) continue;
|
||||
const value = String(input.value || "").trim();
|
||||
const valid = !!value && (name !== "internalDeadline" || validDate(value))
|
||||
&& (!combos.has(name) || Array.from(input.children).some(option => option.value === value));
|
||||
input.setAttribute("aria-invalid", String(!valid));
|
||||
const visible = combos.get(name)?.input || input;
|
||||
visible.setAttribute("aria-invalid", String(!valid));
|
||||
if (!valid && !firstInvalid) firstInvalid = visible;
|
||||
}
|
||||
feedback.hidden = false;
|
||||
if (firstInvalid) {
|
||||
feedback.setAttribute("role", "alert");
|
||||
feedback.textContent = "Complete all required fields with valid values.";
|
||||
firstInvalid.focus();
|
||||
return;
|
||||
}
|
||||
const value = name => String(fields.get(name).value || "").trim();
|
||||
const dateParts = value("customerDeadline").match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
|
||||
const payload = {
|
||||
Title: value("title"),
|
||||
ServiceNew_Id_c: value("services"),
|
||||
ProblemDescription: value("description"),
|
||||
CustomerDeadline_c: dateParts ? `${dateParts[3]}-${dateParts[2]}-${dateParts[1]}` : "",
|
||||
TechPlatformLookUp_c: value("platform"),
|
||||
ServiceRequest_Id_SRToInternalSR: String(parent.SrId ?? ""),
|
||||
SvcQuestionsCollection_c: [], InternalDeadline_c: value("internalDeadline")
|
||||
};
|
||||
entry.saving = true;
|
||||
form.setAttribute("aria-busy", "true");
|
||||
save.disabled = cancel.disabled = true;
|
||||
save.textContent = "Saving...";
|
||||
for (const [name, input] of fields) { input.disabled = true; combos.get(name)?.sync(); }
|
||||
feedback.setAttribute("role", "status");
|
||||
feedback.textContent = "Creating Internal Service Request...";
|
||||
try {
|
||||
await requestSave(payload);
|
||||
entry.saving = false;
|
||||
close();
|
||||
notify("Internal Service Request created successfully.");
|
||||
} catch (error) {
|
||||
if (active !== entry) return;
|
||||
feedback.setAttribute("role", "alert");
|
||||
feedback.textContent = error.message || "Unable to create Internal Service Request.";
|
||||
notify(feedback.textContent, true);
|
||||
} finally {
|
||||
entry.saving = false;
|
||||
if (active === entry) {
|
||||
form.setAttribute("aria-busy", "false");
|
||||
save.disabled = cancel.disabled = false;
|
||||
save.textContent = "Save";
|
||||
for (const [name, input] of fields) { input.disabled = false; combos.get(name)?.sync(); }
|
||||
}
|
||||
}
|
||||
});
|
||||
function load(target, request, service) {
|
||||
const { input, wrapper } = target;
|
||||
input.disabled = true;
|
||||
const combo = searchableSelect(input, wrapper);
|
||||
combos.set(input.name, combo);
|
||||
const status = node("p", "opportunities-extension-action-form-status", "Loading...");
|
||||
status.setAttribute("role", "status");
|
||||
wrapper.append(status);
|
||||
wrapper.setAttribute("aria-busy", "true");
|
||||
Promise.resolve().then(request).then(items => {
|
||||
if (active !== entry) return;
|
||||
const placeholder = node("option", "", items.length ? "Select an option" : "No options available");
|
||||
placeholder.value = "";
|
||||
input.append(placeholder);
|
||||
const seen = new Set();
|
||||
for (const item of items) {
|
||||
const value = String(service ? item.Id : item.code);
|
||||
if (seen.has(value)) continue;
|
||||
seen.add(value);
|
||||
const option = node("option", "", service ? item.RecordName : item.name);
|
||||
option.value = value;
|
||||
if (service && item.Description_c) option.setAttribute("data-description", item.Description_c);
|
||||
input.append(option);
|
||||
}
|
||||
input.value = "";
|
||||
input.disabled = !items.length;
|
||||
if (items.length) ready.add(input.name);
|
||||
save.disabled = ready.size !== 2;
|
||||
status.textContent = items.length ? "" : "No options available.";
|
||||
status.hidden = !!items.length;
|
||||
combo.sync();
|
||||
}).catch(error => {
|
||||
if (active !== entry) return;
|
||||
status.setAttribute("role", "alert");
|
||||
status.textContent = error.message || "Unable to load options. Close and reopen the form to retry.";
|
||||
}).finally(() => { if (active === entry) wrapper.setAttribute("aria-busy", "false"); });
|
||||
}
|
||||
load(platform, requestPlatform, false);
|
||||
load(services, () => requestServices(parent.SrId), true);
|
||||
modal.addEventListener("keydown", event => {
|
||||
if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(); }
|
||||
if (event.key === "Tab") {
|
||||
const focusable = Array.from(form.querySelectorAll("input, textarea, button")).filter(el => !el.disabled && !el.hidden);
|
||||
const first = focusable[0], last = focusable.at(-1);
|
||||
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
|
||||
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
|
||||
}
|
||||
});
|
||||
titleInput.focus();
|
||||
}
|
||||
return { open, close };
|
||||
}
|
||||
@@ -66,11 +66,13 @@ function createSearchableSelect({ environment }) {
|
||||
}
|
||||
function render(query = "") {
|
||||
if (input.disabled) return;
|
||||
matches = options().filter(item => normalize(`${item.textContent} ${item.value}`).includes(normalize(query)));
|
||||
matches = options().filter(item => normalize(`${item.textContent} ${item.getAttribute?.("data-description") || ""} ${item.value}`).includes(normalize(query)));
|
||||
index = -1;
|
||||
list.textContent = "";
|
||||
matches.forEach((item, i) => {
|
||||
const row = element("div", "opportunities-extension-action-option", item.textContent);
|
||||
const description = item.getAttribute?.("data-description");
|
||||
if (description) row.append(element("small", "opportunities-extension-option-description", description));
|
||||
row.id = `${list.id}-${i}`;
|
||||
row.setAttribute("role", "option");
|
||||
row.setAttribute("aria-selected", "false");
|
||||
|
||||
@@ -626,3 +626,5 @@
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.opportunities-extension-refresh-all[aria-busy="true"] svg { animation: none; }
|
||||
}
|
||||
.opportunities-extension-option-description { display: block; font-size: 12px; line-height: 1.4; margin-top: 4px; white-space: normal; overflow-wrap: anywhere; opacity: .85; }
|
||||
.opportunities-extension-action-form-dialog.opportunities-extension-internal-sr-dialog { width: min(760px, 100%); }
|
||||
|
||||
@@ -11,7 +11,7 @@ function createFusionCrmServiceRequests({ isCurrentPeriodRequest,
|
||||
{ op: "$eq", attribute: "RecordSet", value: "ORA_SVC_MY_SR_TEAM_SRS" }
|
||||
] },
|
||||
keywords: null,
|
||||
fields: ["SrNumber", "OpportunityName_Id_c.OptyNumber", "ReportedHours_c", "_actions", "Title",
|
||||
fields: ["SrNumber", "OpportunityName_Id_c.OptyNumber", "ReportedHours_c", "CustomerDeadline_c", "PrimaryPillarNew_c", "_actions", "Title",
|
||||
"Owner.PersonFirstName", "Owner.PersonLastName", "Owner.PrimaryEmail.EmailAddress", "DoNotCallFlag", "DoNotEmailFlag"],
|
||||
sort: [{ attribute: "_score", direction: "descending" }], language: "en",
|
||||
keywordsFields: ["SrNumber", "ServiceNew_Id_c.RecordName", "CustomerAccount", "OpportunityName_Id_c",
|
||||
@@ -29,7 +29,7 @@ function createFusionCrmServiceRequests({ isCurrentPeriodRequest,
|
||||
Origin: "https://eeho.fa.us2.oraclecloud.com", Preference: "transient"
|
||||
};
|
||||
const entry = logRequestStart({ name: "resquestCrmServiceRequest", method: "POST", url,
|
||||
headers: { ...headers, Authorization: `Bearer ${maskToken(token)}` }, body: JSON.stringify(payload, null, 2), metadata: { offset, attempt } });
|
||||
headers: { ...headers }, body: JSON.stringify(payload, null, 2), metadata: { offset, attempt } });
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST", credentials: "include", headers, body: JSON.stringify(payload)
|
||||
|
||||
@@ -24,7 +24,7 @@ function createFusionOpportunities({
|
||||
url: OPPORTUNITIES_QUERY_URL,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${maskToken(token)}`,
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
Origin: "https://eeho.fa.us2.oraclecloud.com",
|
||||
Preference: "transient"
|
||||
@@ -151,7 +151,8 @@ function createFusionOpportunities({
|
||||
"CustomerAccount.PartyUniqueName",
|
||||
"CustomerAccount.PartyId",
|
||||
"CustomerAccount.PartyNumber",
|
||||
"CustomerAccount.Region_c"
|
||||
"CustomerAccount.Region_c",
|
||||
"CustomerAccount.PrimaryAddress.CountryCode"
|
||||
],
|
||||
sort: [],
|
||||
language: "en",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders }) {
|
||||
function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders, internalServiceRequestHeaders }) {
|
||||
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";
|
||||
@@ -11,26 +11,76 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
|
||||
if (typeof value !== "string" || !value.trim()) throw new Error(`Missing ${name}.`);
|
||||
return value.trim();
|
||||
}
|
||||
function dateOnly(value, name) {
|
||||
const date = text(value, name);
|
||||
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 ${name}.`);
|
||||
return date;
|
||||
}
|
||||
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.");
|
||||
dateOnly(payload.CustomerDeadline_c, "Customer deadline");
|
||||
return { ...payload, SvcQuestionsCollection_c: [], SourceCd: "SVC_OPPORTUNITY_SCREEN", SeverityCd: "ORA_SVC_SEV2", ChannelTypeCd: "ORA_SVC_WEB" };
|
||||
}
|
||||
async function requestResourceUser(email, debugRequests) {
|
||||
const emailAddress = text(email, "current user email");
|
||||
const resource = await fetchJson(`${resourceUsers}?emailAddress=${encodeURIComponent(emailAddress)}`, {
|
||||
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
|
||||
}, "requestResourceUser", debugRequests);
|
||||
return numericId(resource?.resourcePartyId, "resourcePartyId");
|
||||
}
|
||||
async function requestPostInternalServiceRequest(message) {
|
||||
const debugRequests = [];
|
||||
try {
|
||||
const source = message.payload;
|
||||
const parentSrId = numericId(source?.ServiceRequest_Id_SRToInternalSR, "parent SrId");
|
||||
const payload = {
|
||||
Title: text(source?.Title, "Title"),
|
||||
ServiceNew_Id_c: numericId(source?.ServiceNew_Id_c, "ServiceNew_Id_c"),
|
||||
ProblemDescription: text(source?.ProblemDescription, "Description"),
|
||||
CustomerDeadline_c: dateOnly(source?.CustomerDeadline_c, "Customer deadline"),
|
||||
TechPlatformLookUp_c: text(source?.TechPlatformLookUp_c, "Platform"),
|
||||
InternalDeadline_c: `${dateOnly(source?.InternalDeadline_c, "Internal Deadline")}T00:00:00.000Z`,
|
||||
SvcQuestionsCollection_c: []
|
||||
};
|
||||
const parent = await requestSRInfo(parentSrId, debugRequests);
|
||||
payload.ServiceRequest_Id_SRToInternalSR = String(parentSrId);
|
||||
for (const name of ["AccountPartyId", "OpportunityName_Id_c"]) payload[name] = numericId(parent[name], name);
|
||||
for (const name of ["OpportunityName_c", "RegionText_c", "SvcHealth_c", "Language_c", "CountryText_c", "PrimaryPillarNew_c"]) payload[name] = text(parent[name], name);
|
||||
payload.SecondaryPillarNew_c = parent.SecondaryPillarNew_c == null ? null : text(parent.SecondaryPillarNew_c, "SecondaryPillarNew_c");
|
||||
payload.ReportedByPartyId = await requestResourceUser(message.emailAddress, debugRequests);
|
||||
const url = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPICloudAccount/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests";
|
||||
const result = await fetchJson(url, { method: "POST", credentials: "include",
|
||||
headers: await internalServiceRequestHeaders(url),
|
||||
body: JSON.stringify(payload)
|
||||
}, "requestPostInternalServiceRequest", debugRequests, { allowEmpty: true });
|
||||
return { ok: true, result, debugRequests };
|
||||
} catch (error) { error.debugRequests = debugRequests; throw error; }
|
||||
}
|
||||
async function requestSRInfo(parentSrId, debugRequests) {
|
||||
const url = new URL("https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPI/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests");
|
||||
url.searchParams.set("fields", "PrimaryPillarNew_c,SecondaryPillarNew_c,SvcHealth_c,SrNumber,AccountPartyId,CountryText_c,Language_c,OpportunityName_Id_c,OpportunityName_c,RegionText_c,ServiceNew_Id_c");
|
||||
url.searchParams.set("onlyData", "true");
|
||||
url.searchParams.set("orderBy", "LastUpdateDate:desc");
|
||||
url.searchParams.set("q", `SrId=${parentSrId}`);
|
||||
const result = await fetchJson(url.href, {
|
||||
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
|
||||
}, "requestSRInfo", debugRequests);
|
||||
if (!Array.isArray(result?.items) || result.items.length !== 1 || !result.items[0]
|
||||
|| (result.items[0].SrId != null && String(result.items[0].SrId) !== String(parentSrId))) {
|
||||
throw new Error("requestSRInfo did not return the selected parent Service Request.");
|
||||
}
|
||||
return result.items[0];
|
||||
}
|
||||
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");
|
||||
payload.ReportedByPartyId = await requestResourceUser(message.emailAddress, debugRequests);
|
||||
const sharedHeaders = await previewTokenHeaders(catalog);
|
||||
return { ok: true, payload, debugRequests, headers: {
|
||||
requestSrTokenCatalog: sharedHeaders,
|
||||
@@ -38,34 +88,9 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
|
||||
} };
|
||||
}
|
||||
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 headers = await requestCreationHeaders(message.catalogId, endpoint, debugRequests);
|
||||
const result = await fetchJson(endpoint, {
|
||||
method: "POST", credentials: "include", headers: { ...catalogHeaders, "vb-proxy-header-x-oracle-ectoken": token.result.trim() },
|
||||
method: "POST", credentials: "include", headers,
|
||||
body: JSON.stringify(payload)
|
||||
}, "requestPostServiceRequest", debugRequests, { allowEmpty: true });
|
||||
return { ok: true, result, debugRequests };
|
||||
@@ -73,16 +98,43 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
|
||||
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)";
|
||||
annotateSigningRequest(debugRequests);
|
||||
}
|
||||
}
|
||||
async function requestCreationHeaders(id, target, debugRequests) {
|
||||
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, target);
|
||||
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.");
|
||||
}
|
||||
return { ...catalogHeaders, "vb-proxy-header-x-oracle-ectoken": token.result.trim() };
|
||||
}
|
||||
function annotateSigningRequest(debugRequests) {
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
return { requestPostServiceRequest };
|
||||
return { requestPostServiceRequest, requestPostInternalServiceRequest };
|
||||
}
|
||||
|
||||
@@ -194,7 +194,33 @@ function createFusionSrDetails({ fetchJson, tabsCreate, tabsRemove, supportsFire
|
||||
return { ok: true, items: result.items.map(({ Id, RecordName, Abstract_c }) => ({ Id: String(Id), RecordName, Abstract_c: Abstract_c == null ? "" : String(Abstract_c) })), debugRequests };
|
||||
} catch (error) { error.debugRequests = debugRequests; throw error; }
|
||||
}
|
||||
async function requestInternalSrOptions(message, services = false) {
|
||||
const debugRequests = [];
|
||||
const name = services ? "requestInternalSrServices" : "requestLookupTypePlatform";
|
||||
try {
|
||||
const base = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetSeaaSOKECustomRestAPI/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/";
|
||||
let path = "lookups?type=STANDARD&lookupType=SENGG_TECH_PLATFORM";
|
||||
if (services) {
|
||||
const id = String(message.parentServiceId ?? "");
|
||||
if (!/^\d+$/.test(id)) throw new Error("A numeric parent SrId is required.");
|
||||
if (typeof message.emailAddress !== "string" || !message.emailAddress.trim()) throw new Error("Current user email is unavailable.");
|
||||
path = `services?emailAddress=${encodeURIComponent(message.emailAddress.trim())}&parentServiceId=${encodeURIComponent(id)}&serviceType=INTERNAL`;
|
||||
}
|
||||
const result = await fetchJson(base + path, { method: "GET", credentials: "include",
|
||||
headers: { Accept: "application/json", Authorization: "Session" }
|
||||
}, name, debugRequests);
|
||||
if (!Array.isArray(result?.items) || result.items.some(item => services
|
||||
? !/^\d+$/.test(String(item?.Id ?? "")) || typeof item?.RecordName !== "string"
|
||||
: typeof item?.code !== "string" || typeof item?.name !== "string")) throw new Error(`Invalid ${name} response.`);
|
||||
const items = services
|
||||
? result.items.map(item => ({ Id: String(item.Id), RecordName: item.RecordName, Description_c: String(item.Description_c ?? "") }))
|
||||
: result.items.filter(item => item.enabledFlag !== "N").map(({ code, name }) => ({ code, name }));
|
||||
return { ok: true, items, debugRequests };
|
||||
} catch (error) { error.debugRequests = debugRequests; throw error; }
|
||||
}
|
||||
return { requestLookupWithSession, requestInternalServiceRequest, requestSrLookupOpportunityBusiness,
|
||||
requestLookupTypePlatform: () => requestInternalSrOptions({}),
|
||||
requestInternalSrServices: message => requestInternalSrOptions(message, true),
|
||||
prepareSession: sender => requestLookupWithSession(null, {}, sender, true),
|
||||
requestSrLookupPrimaryPillar: () => requestLookup("SENGG_PRIMARY_PILLAR", "requestSrLookupPrimaryPillar"),
|
||||
requestSrLookupLanguage: () => requestLookup("SENGG_LANGUAGE", "requestSrLookupLanguage"),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
function createHttpBackgroundJson({
|
||||
previewDebugBody,
|
||||
DEBUG_BODY_LIMIT,
|
||||
observeResponseHeaders = () => {},
|
||||
environment
|
||||
}) {
|
||||
@@ -127,7 +126,7 @@ function createHttpBackgroundJson({
|
||||
}
|
||||
|
||||
function createDebugBody(text) {
|
||||
return previewDebugBody(text, DEBUG_BODY_LIMIT);
|
||||
return previewDebugBody(text);
|
||||
}
|
||||
|
||||
function normalizeDebugHeaders(headers) {
|
||||
|
||||
@@ -15,12 +15,6 @@ function createSpaCurrentUser({ fetchJson }) {
|
||||
for (const entry of debugRequests) { entry.status = "failed"; entry.error = error.message; }
|
||||
error.debugRequests = debugRequests;
|
||||
throw error;
|
||||
} finally {
|
||||
// The identity token is not needed by content scripts or debug, including on failures.
|
||||
for (const entry of debugRequests) {
|
||||
entry.responseBody = JSON.stringify(user ? { user, token: "(redacted)" } : { error: "Identity response omitted." }, null, 2);
|
||||
entry.responseBodyTruncated = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { requestCurrentUser };
|
||||
|
||||
@@ -1,40 +1,3 @@
|
||||
function createDebugBodyPreview(text, limit) {
|
||||
if (text.length <= limit) return { value: text || "(empty response body)", truncated: false };
|
||||
try {
|
||||
const normalized = text.replace(/^\uFEFF/, "").trimStart()
|
||||
.replace(/^\)\]\}',?\s*/, "").replace(/^while\s*\(1\);\s*/, "");
|
||||
let value = JSON.parse(normalized);
|
||||
for (let depth = 0; depth < 2 && typeof value === "string" && /^\s*[\[{]/.test(value); depth += 1) {
|
||||
try { value = JSON.parse(value); } catch { break; }
|
||||
}
|
||||
// Bound retained data without cutting JSON syntax or losing sensitive field names.
|
||||
let remaining = Math.max(0, limit - 256);
|
||||
const visit = (node, depth = 0) => {
|
||||
if (remaining <= 64 || depth >= 30) return "[omitted from debug preview]";
|
||||
if (node === null || typeof node !== "object") {
|
||||
const serialized = JSON.stringify(node);
|
||||
if (serialized.length > remaining) {
|
||||
remaining = 0;
|
||||
return "[omitted from debug preview]";
|
||||
}
|
||||
remaining -= serialized.length + 1;
|
||||
return node;
|
||||
}
|
||||
const result = Array.isArray(node) ? [] : Object.create(null);
|
||||
remaining -= 2;
|
||||
for (const key of Object.keys(node)) {
|
||||
const cost = Array.isArray(node) ? 1 : JSON.stringify(key).length + 2;
|
||||
if (remaining < cost + 64) break;
|
||||
remaining -= cost;
|
||||
result[key] = visit(node[key], depth + 1);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
return {
|
||||
value: JSON.stringify({ debugNotice: "Partial response preview: remaining fields/items omitted due to the debug size limit.", preview: visit(value) }),
|
||||
truncated: true
|
||||
};
|
||||
} catch {
|
||||
return { value: `${text.slice(0, limit)}\n... truncated ${text.length - limit} characters`, truncated: true };
|
||||
}
|
||||
function createDebugBodyPreview(text) {
|
||||
return { value: text || "(empty response body)", truncated: false };
|
||||
}
|
||||
|
||||
@@ -17,9 +17,12 @@ function createMessageTypes() {
|
||||
requestInternalServiceRequest: "opportunitiesExtension.requestInternalServiceRequest",
|
||||
requestSrLookupPrimaryPillar: "opportunitiesExtension.requestSrLookupPrimaryPillar",
|
||||
requestPostServiceRequest: "opportunitiesExtension.requestPostServiceRequest",
|
||||
requestPostInternalServiceRequest: "opportunitiesExtension.requestPostInternalServiceRequest",
|
||||
prepareSrSession: "opportunitiesExtension.prepareSrSession",
|
||||
requestSrLookupLanguage: "opportunitiesExtension.requestSrLookupLanguage",
|
||||
requestSrLookupTechPlatform: "opportunitiesExtension.requestSrLookupTechPlatform",
|
||||
requestLookupTypePlatform: "opportunitiesExtension.requestLookupTypePlatform",
|
||||
requestInternalSrServices: "opportunitiesExtension.requestInternalSrServices",
|
||||
requestSrLookupOpportunityBusiness: "opportunitiesExtension.requestSrLookupOpportunityBusiness",
|
||||
requestCurrentUser: "opportunitiesExtension.requestCurrentUser",
|
||||
invalidateProductsCache: "opportunitiesExtension.invalidateProductsCache",
|
||||
|
||||
Reference in New Issue
Block a user