Implemented a tooltip on the SR to display the Internal SR

This commit is contained in:
2026-09-29 13:53:38 -03:00
parent a182609415
commit 4d3f1f9760
21 changed files with 400 additions and 28 deletions

View File

@@ -17,6 +17,7 @@ function createMessageTypes() {
requestActionTeamMembers: "opportunitiesExtension.requestActionTeamMembers",
requestActionFindLookup: "opportunitiesExtension.requestActionFindLookup",
requestMgmtAction: "opportunitiesExtension.requestMgmtAction",
requestInternalServiceRequest: "opportunitiesExtension.requestInternalServiceRequest",
requestCurrentUser: "opportunitiesExtension.requestCurrentUser",
invalidateProductsCache: "opportunitiesExtension.invalidateProductsCache",
requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend",
@@ -741,6 +742,35 @@ function createSpaActionOptions({ fetchJson }) {
return { requestActionTeamMembers, requestActionFindLookup, requestMgmtAction };
}
// Source: src/services/fusion/sr-details.js
function createFusionSrDetails({ fetchJson }) {
async function requestInternalServiceRequest(message) {
const debugRequests = [];
try {
const id = String(message.srId ?? "");
if (!/^\d+$/.test(id)) throw new Error("A numeric SrId is required.");
const fields = "Title,SrNumber,SrId,OpportunityName_c,QueueManagers_c,StatusCd,resourceMembers;ServiceRequest_SRToInternalSR_Tgt:ReportedHours_c,Title,OpportunityName_c,StatusCd,SrNumber,ServiceNew_c,AccountPartyName,LastUpdateDate,SvcHealth_c,AssigneePersonName,OpenDate,SrId";
const 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"
+ `?fields=${encodeURIComponent(fields)}&onlyData=true&q=${encodeURIComponent(`SrId=${id}`)}`;
const result = await fetchJson(url, { method: "GET", credentials: "include",
headers: { Accept: "application/json", Authorization: "Session" }
}, "requestInternalServiceRequest", debugRequests);
if (!Array.isArray(result?.items)) throw new Error("Invalid SR details response.");
const items = result.items.flatMap(item => {
const children = item.ServiceRequest_SRToInternalSR_Tgt;
if (children == null) return [];
if (!Array.isArray(children) || children.some(child => !child || typeof child !== "object")) throw new Error("Invalid linked SR collection.");
return children;
});
return { ok: true, items, debugRequests };
} catch (error) {
error.debugRequests = debugRequests;
throw error;
}
}
return { requestInternalServiceRequest };
}
// Source: src/services/spa/current-user.js
function createSpaCurrentUser({ fetchJson }) {
async function requestCurrentUser() {
@@ -1839,6 +1869,7 @@ function registerBackgroundMessages({
requestActionTeamMembers,
requestActionFindLookup,
requestMgmtAction,
requestInternalServiceRequest,
requestCurrentUser,
requestConsumerTokenService,
requestConsumptionTrend,
@@ -1933,10 +1964,10 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
return true;
}
if ([messageTypes.requestActionTeamMembers, messageTypes.requestActionFindLookup, messageTypes.requestMgmtAction].includes(message.type)) {
const request = message.type === messageTypes.requestMgmtAction ? requestMgmtAction : message.type === messageTypes.requestActionTeamMembers ? requestActionTeamMembers : requestActionFindLookup;
if ([messageTypes.requestActionTeamMembers, messageTypes.requestActionFindLookup, messageTypes.requestMgmtAction, messageTypes.requestInternalServiceRequest].includes(message.type)) {
const request = message.type === messageTypes.requestInternalServiceRequest ? requestInternalServiceRequest : message.type === messageTypes.requestMgmtAction ? requestMgmtAction : message.type === messageTypes.requestActionTeamMembers ? requestActionTeamMembers : requestActionFindLookup;
request(message).then(sendResponse).catch(error => sendResponse({
ok: false, error: error.message || "Unable to load Action options.",
ok: false, httpStatus: error.httpStatus, error: error.message || "Unable to complete request.",
debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : []
}));
return true;
@@ -2142,8 +2173,9 @@ function createBackgroundApplication(overrides = {}) {
const requestRatecardByPlan = (...args) => spaConsumption.requestRatecardByPlan(...args);
const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args);
const runtimeApi = environment.browser || environment.chrome;
const fusionSrDetails = createFusionSrDetails({ fetchJson: (...args) => httpBackgroundJson.fetchJson(...args) });
function start() {
startBackgroundApplication({ messageTypes, requestMgmtAction: message => spaActionOptions.requestMgmtAction(message), invalidateProductsCache: () => productsCache.invalidate(), requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestActionTeamMembers, requestActionFindLookup, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi });
startBackgroundApplication({ messageTypes, requestInternalServiceRequest: message => fusionSrDetails.requestInternalServiceRequest(message), 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,

File diff suppressed because one or more lines are too long

View File

@@ -9,7 +9,8 @@
],
"host_permissions": [
"https://eeho.fa.us2.oraclecloud.com/*",
"https://spa.oracle.com/*"
"https://spa.oracle.com/*",
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com/*"
],
"content_scripts": [
{

View File

@@ -17,6 +17,7 @@ function createMessageTypes() {
requestActionTeamMembers: "opportunitiesExtension.requestActionTeamMembers",
requestActionFindLookup: "opportunitiesExtension.requestActionFindLookup",
requestMgmtAction: "opportunitiesExtension.requestMgmtAction",
requestInternalServiceRequest: "opportunitiesExtension.requestInternalServiceRequest",
requestCurrentUser: "opportunitiesExtension.requestCurrentUser",
invalidateProductsCache: "opportunitiesExtension.invalidateProductsCache",
requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend",
@@ -741,6 +742,35 @@ function createSpaActionOptions({ fetchJson }) {
return { requestActionTeamMembers, requestActionFindLookup, requestMgmtAction };
}
// Source: src/services/fusion/sr-details.js
function createFusionSrDetails({ fetchJson }) {
async function requestInternalServiceRequest(message) {
const debugRequests = [];
try {
const id = String(message.srId ?? "");
if (!/^\d+$/.test(id)) throw new Error("A numeric SrId is required.");
const fields = "Title,SrNumber,SrId,OpportunityName_c,QueueManagers_c,StatusCd,resourceMembers;ServiceRequest_SRToInternalSR_Tgt:ReportedHours_c,Title,OpportunityName_c,StatusCd,SrNumber,ServiceNew_c,AccountPartyName,LastUpdateDate,SvcHealth_c,AssigneePersonName,OpenDate,SrId";
const 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"
+ `?fields=${encodeURIComponent(fields)}&onlyData=true&q=${encodeURIComponent(`SrId=${id}`)}`;
const result = await fetchJson(url, { method: "GET", credentials: "include",
headers: { Accept: "application/json", Authorization: "Session" }
}, "requestInternalServiceRequest", debugRequests);
if (!Array.isArray(result?.items)) throw new Error("Invalid SR details response.");
const items = result.items.flatMap(item => {
const children = item.ServiceRequest_SRToInternalSR_Tgt;
if (children == null) return [];
if (!Array.isArray(children) || children.some(child => !child || typeof child !== "object")) throw new Error("Invalid linked SR collection.");
return children;
});
return { ok: true, items, debugRequests };
} catch (error) {
error.debugRequests = debugRequests;
throw error;
}
}
return { requestInternalServiceRequest };
}
// Source: src/services/spa/current-user.js
function createSpaCurrentUser({ fetchJson }) {
async function requestCurrentUser() {
@@ -1839,6 +1869,7 @@ function registerBackgroundMessages({
requestActionTeamMembers,
requestActionFindLookup,
requestMgmtAction,
requestInternalServiceRequest,
requestCurrentUser,
requestConsumerTokenService,
requestConsumptionTrend,
@@ -1933,10 +1964,10 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
return true;
}
if ([messageTypes.requestActionTeamMembers, messageTypes.requestActionFindLookup, messageTypes.requestMgmtAction].includes(message.type)) {
const request = message.type === messageTypes.requestMgmtAction ? requestMgmtAction : message.type === messageTypes.requestActionTeamMembers ? requestActionTeamMembers : requestActionFindLookup;
if ([messageTypes.requestActionTeamMembers, messageTypes.requestActionFindLookup, messageTypes.requestMgmtAction, messageTypes.requestInternalServiceRequest].includes(message.type)) {
const request = message.type === messageTypes.requestInternalServiceRequest ? requestInternalServiceRequest : message.type === messageTypes.requestMgmtAction ? requestMgmtAction : message.type === messageTypes.requestActionTeamMembers ? requestActionTeamMembers : requestActionFindLookup;
request(message).then(sendResponse).catch(error => sendResponse({
ok: false, error: error.message || "Unable to load Action options.",
ok: false, httpStatus: error.httpStatus, error: error.message || "Unable to complete request.",
debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : []
}));
return true;
@@ -2142,8 +2173,9 @@ function createBackgroundApplication(overrides = {}) {
const requestRatecardByPlan = (...args) => spaConsumption.requestRatecardByPlan(...args);
const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args);
const runtimeApi = environment.browser || environment.chrome;
const fusionSrDetails = createFusionSrDetails({ fetchJson: (...args) => httpBackgroundJson.fetchJson(...args) });
function start() {
startBackgroundApplication({ messageTypes, requestMgmtAction: message => spaActionOptions.requestMgmtAction(message), invalidateProductsCache: () => productsCache.invalidate(), requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestActionTeamMembers, requestActionFindLookup, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi });
startBackgroundApplication({ messageTypes, requestInternalServiceRequest: message => fusionSrDetails.requestInternalServiceRequest(message), 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,

File diff suppressed because one or more lines are too long

View File

@@ -9,7 +9,8 @@
],
"host_permissions": [
"https://eeho.fa.us2.oraclecloud.com/*",
"https://spa.oracle.com/*"
"https://spa.oracle.com/*",
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com/*"
],
"content_scripts": [
{

View File

@@ -21,7 +21,8 @@ const baseManifest = {
],
host_permissions: [
"https://eeho.fa.us2.oraclecloud.com/*",
"https://spa.oracle.com/*"
"https://spa.oracle.com/*",
"https://comcipapic-oalprod.integration.ocp.oraclecloud.com/*"
],
content_scripts: [
{

View File

@@ -86,6 +86,7 @@ module.exports = {
"src/services/spa/customer-service-requests.js",
"src/services/spa/workload-actions.js",
"src/services/spa/action-options.js",
"src/services/fusion/sr-details.js",
"src/services/spa/current-user.js",
"src/services/spa/consumption.js",
"src/services/spa/response-shapes.js",

View File

@@ -125,8 +125,9 @@ function createBackgroundApplication(overrides = {}) {
const requestRatecardByPlan = (...args) => spaConsumption.requestRatecardByPlan(...args);
const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args);
const runtimeApi = environment.browser || environment.chrome;
const fusionSrDetails = createFusionSrDetails({ fetchJson: (...args) => httpBackgroundJson.fetchJson(...args) });
function start() {
startBackgroundApplication({ messageTypes, requestMgmtAction: message => spaActionOptions.requestMgmtAction(message), invalidateProductsCache: () => productsCache.invalidate(), requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestActionTeamMembers, requestActionFindLookup, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi });
startBackgroundApplication({ messageTypes, requestInternalServiceRequest: message => fusionSrDetails.requestInternalServiceRequest(message), 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,
requestInternalServiceRequest,
requestCurrentUser,
requestConsumerTokenService,
requestConsumptionTrend,
@@ -105,10 +106,10 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
return true;
}
if ([messageTypes.requestActionTeamMembers, messageTypes.requestActionFindLookup, messageTypes.requestMgmtAction].includes(message.type)) {
const request = message.type === messageTypes.requestMgmtAction ? requestMgmtAction : message.type === messageTypes.requestActionTeamMembers ? requestActionTeamMembers : requestActionFindLookup;
if ([messageTypes.requestActionTeamMembers, messageTypes.requestActionFindLookup, messageTypes.requestMgmtAction, messageTypes.requestInternalServiceRequest].includes(message.type)) {
const request = message.type === messageTypes.requestInternalServiceRequest ? requestInternalServiceRequest : message.type === messageTypes.requestMgmtAction ? requestMgmtAction : message.type === messageTypes.requestActionTeamMembers ? requestActionTeamMembers : requestActionFindLookup;
request(message).then(sendResponse).catch(error => sendResponse({
ok: false, error: error.message || "Unable to load Action options.",
ok: false, httpStatus: error.httpStatus, error: error.message || "Unable to complete request.",
debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : []
}));
return true;

View File

@@ -217,6 +217,12 @@ function createContentApplication(overrides = {}) {
maskToken: (...args) => debugContentLog.maskToken(...args), environment
});
const crmSrPopover = createCrmSrPopover({
requestDetails: async srId => {
const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestInternalServiceRequest, srId });
debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []);
if (!result?.ok) throw new Error(result?.error || "Unable to load SR details.");
return result.items;
},
MODAL_ID: contracts.MODAL_ID,
createCopyButton: (...args) => opportunitiesLinks.createCopyOpportunityButton(...args),
createCloseIcon: (...args) => contentIcons.createCloseIcon(...args), environment

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 (entry.name === "requestCustomerServiceRequest" || entry.name === "resquestCrmServiceRequest") return "serviceRequests";
if (["requestCustomerServiceRequest", "resquestCrmServiceRequest", "requestInternalServiceRequest"].includes(entry.name)) return "serviceRequests";
if (/^request(?:AccountSummary|AccountPlans|ConsumptionService|ConsumptionPlanDetail|RatecardByPlan)/.test(entry.name)) return "consumption";
return "other";
}

View File

@@ -13,7 +13,7 @@ function createActivityFilters({ allowsCrmActivities, getSrSummary, getActionSum
container.className = "opportunities-extension-badge-filters";
for (const [kind, title, options] of [
["sr", "Service Request", ["matched", "under4", "missing"]],
["actions", "Action", ["matched", "missing"]]
["actions", "WL Action", ["matched", "missing"]]
]) {
const section = environment.document.createElement("section");
section.className = "opportunities-extension-activity-filter";

View File

@@ -1,4 +1,4 @@
function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, environment }) {
function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, requestDetails, environment }) {
const { document, window, setTimeout, clearTimeout } = environment;
let active = null;
let sequence = 0;
@@ -74,6 +74,7 @@ function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, envir
dismiss.append(createCloseIcon());
dismiss.addEventListener("click", () => close(true));
panel.append(dismiss);
const detailLoads = [];
items.forEach((item, index) => {
const section = document.createElement("section");
const heading = document.createElement("div");
@@ -94,6 +95,52 @@ function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, envir
details.className = "opportunities-extension-sr-details";
details.textContent = descriptions[index];
section.append(heading, details);
const children = document.createElement("div");
children.className = "opportunities-extension-sr-children";
children.setAttribute("aria-busy", "true");
children.setAttribute("role", "status");
children.textContent = "Loading SR details...";
section.append(children);
detailLoads.push(async () => {
try {
const records = await requestDetails(item.SrId);
if (active !== entry || entry.panel !== panel) return;
children.textContent = "";
if (!records.length) children.textContent = "No further details.";
for (const record of records) {
const child = document.createElement("section");
const row = document.createElement("div");
row.className = "opportunities-extension-sr-number";
const label = document.createElement("span");
label.textContent = "Internal SR Number:";
const link = document.createElement(record.SrNumber ? "a" : "span");
link.textContent = record.SrNumber ? String(record.SrNumber) : "N/D";
if (record.SrNumber) {
link.href = `https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/service/ec/container/sr/edit?srNumber=${encodeURIComponent(String(record.SrNumber))}&srId=${encodeURIComponent(String(record.SrId ?? ""))}`;
link.target = "_blank";
link.rel = "noopener noreferrer";
}
row.append(label, link);
if (record.SrNumber) row.append(createCopyButton(String(record.SrNumber), "SR number", "SR Number"));
const body = document.createElement("div");
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");
child.append(row, body);
children.append(child);
}
} catch (error) {
if (active !== entry || entry.panel !== panel) return;
children.setAttribute("role", "alert");
children.textContent = error.message || "Unable to load SR details.";
} finally {
if (active === entry && entry.panel === panel) {
children.setAttribute("aria-busy", "false");
entry.position();
}
}
});
panel.append(section);
});
panel.addEventListener("pointerenter", () => clearTimeout(entry.timer));
@@ -101,6 +148,7 @@ function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, envir
panel.addEventListener("focusout", focusOut);
host.append(panel);
active = entry;
detailLoads.forEach(load => load());
button.setAttribute("aria-expanded", "true");
entry.position();
document.addEventListener("pointerdown", entry.outside, true);

View File

@@ -533,6 +533,11 @@
.opportunities-extension-sr-number { display: flex; align-items: center; gap: 8px; padding-right: 30px; font-weight: 600; }
.opportunities-extension-sr-popover a { color: #00677d; text-decoration: underline; }
.opportunities-extension-sr-details { white-space: pre-wrap; margin-top: 6px; }
.opportunities-extension-sr-children { margin: 10px 0 0 8px; padding-left: 10px; border-left: 2px solid #8c969a; overflow-wrap: anywhere; }
.opportunities-extension-sr-children[aria-busy="true"] { min-height: 28px; animation: opportunities-action-options-loading 1s ease-in-out infinite alternate; }
.opportunities-extension-sr-children[role="alert"] { color: #a12d27; }
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-sr-children[role="alert"] { color: #ffd9d5; }
@media (prefers-reduced-motion: reduce) { .opportunities-extension-sr-children[aria-busy="true"] { animation: none; } }
.opportunities-extension-sr-dismiss {
position: absolute; top: 6px; right: 6px; width: 28px; height: 28px;
display: grid; place-items: center; padding: 4px; border: 0;

View File

@@ -0,0 +1,27 @@
function createFusionSrDetails({ fetchJson }) {
async function requestInternalServiceRequest(message) {
const debugRequests = [];
try {
const id = String(message.srId ?? "");
if (!/^\d+$/.test(id)) throw new Error("A numeric SrId is required.");
const fields = "Title,SrNumber,SrId,OpportunityName_c,QueueManagers_c,StatusCd,resourceMembers;ServiceRequest_SRToInternalSR_Tgt:ReportedHours_c,Title,OpportunityName_c,StatusCd,SrNumber,ServiceNew_c,AccountPartyName,LastUpdateDate,SvcHealth_c,AssigneePersonName,OpenDate,SrId";
const 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"
+ `?fields=${encodeURIComponent(fields)}&onlyData=true&q=${encodeURIComponent(`SrId=${id}`)}`;
const result = await fetchJson(url, { method: "GET", credentials: "include",
headers: { Accept: "application/json", Authorization: "Session" }
}, "requestInternalServiceRequest", debugRequests);
if (!Array.isArray(result?.items)) throw new Error("Invalid SR details response.");
const items = result.items.flatMap(item => {
const children = item.ServiceRequest_SRToInternalSR_Tgt;
if (children == null) return [];
if (!Array.isArray(children) || children.some(child => !child || typeof child !== "object")) throw new Error("Invalid linked SR collection.");
return children;
});
return { ok: true, items, debugRequests };
} catch (error) {
error.debugRequests = debugRequests;
throw error;
}
}
return { requestInternalServiceRequest };
}

View File

@@ -14,6 +14,7 @@ function createMessageTypes() {
requestActionTeamMembers: "opportunitiesExtension.requestActionTeamMembers",
requestActionFindLookup: "opportunitiesExtension.requestActionFindLookup",
requestMgmtAction: "opportunitiesExtension.requestMgmtAction",
requestInternalServiceRequest: "opportunitiesExtension.requestInternalServiceRequest",
requestCurrentUser: "opportunitiesExtension.requestCurrentUser",
invalidateProductsCache: "opportunitiesExtension.invalidateProductsCache",
requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend",

View File

@@ -37,7 +37,7 @@ test("activity badge filters precede Status, are accessible and combine without
assert.equal(h.button("sr", "matched").textContent, "");
assert.equal(h.button("actions", "missing").textContent, "");
assert.equal(h.button("actions", "matched").type, "button");
assert.equal(h.controls.children[1].children[0].textContent, "Action");
assert.equal(h.controls.children[1].children[0].textContent, "WL Action");
for (const value of ["matched", "missing"]) {
assert.equal(h.button("actions", value).textContent, "");
assert.ok(h.button("actions", value).title.startsWith("Whether the Action is registered in Workload Workbench.\n"));

View File

@@ -41,7 +41,7 @@ test("distribution contracts: manifests, execution boundaries and deterministic
manifest_version: 3, name: "Opportunities Extension",
description: "Adiciona um atalho de Opportunities Extension nas paginas Oracle Fusion permitidas.",
version: "1.0.2", permissions: ["cookies", "storage"],
host_permissions: ["https://eeho.fa.us2.oraclecloud.com/*", "https://spa.oracle.com/*"],
host_permissions: ["https://eeho.fa.us2.oraclecloud.com/*", "https://spa.oracle.com/*", "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/*"],
content_scripts: [{
matches: ["https://eeho.fa.us2.oraclecloud.com/hcmUI/faces/FuseWelcome*", "https://eeho.fa.us2.oraclecloud.com/fscmUI/faces/FuseWelcome*"],
js: ["content.js"], all_frames: true, run_at: "document_idle"

View File

@@ -0,0 +1,41 @@
const assert = require("node:assert/strict");
const { test } = require("node:test");
const { loadBackground, response } = require("./support/characterization.cjs");
const type = "opportunitiesExtension.requestInternalServiceRequest";
for (const family of ["chrome", "firefox"]) {
test(`${family}: SR detail GET uses exact scope and Session authentication`, async () => {
const calls = [];
const children = [{ SrId: 123, SrNumber: "SR123", Title: "Synthetic" }];
const h = loadBackground({ family, fetch: async (url, options) => {
calls.push({ url, options });
return response({ items: [{ ServiceRequest_SRToInternalSR_Tgt: children }] });
} });
const result = await h.dispatch({ type, srId: "987", token: "secret-test" });
assert.equal(result.ok, true);
assert.deepEqual(result.items, children);
const url = new URL(calls[0].url);
assert.equal(url.hostname, "comcipapic-oalprod.integration.ocp.oraclecloud.com");
assert.equal(url.searchParams.get("q"), "SrId=987");
assert.equal(url.searchParams.get("onlyData"), "true");
assert.ok(url.searchParams.get("fields").includes("ServiceRequest_SRToInternalSR_Tgt:ReportedHours_c"));
assert.equal(calls[0].options.method, "GET");
assert.equal(calls[0].options.headers.Authorization, "Session");
assert.equal(calls[0].options.credentials, "include");
assert.equal(result.debugRequests[0].requestHeaders.Authorization, "Session");
assert.ok(!JSON.stringify(result).includes("secret-test"));
assert.equal(result.debugRequests[0].name, "requestInternalServiceRequest");
});
}
test("SR details distinguish empty collections from errors and validate inputs", async () => {
for (const body of [{ items: [] }, { items: [{}] }, { items: [{ ServiceRequest_SRToInternalSR_Tgt: [] }] }]) {
const h = loadBackground({ fetch: async () => response(body) });
assert.deepEqual((await h.dispatch({ type, srId: "1" })).items, []);
}
for (const fetch of [async () => response({}, 403), async () => response({}), async () => response({ items: [{ ServiceRequest_SRToInternalSR_Tgt: {} }] })]) {
const h = loadBackground({ fetch });
assert.equal((await h.dispatch({ type, srId: "1", token: "test" })).ok, false);
}
const h = loadBackground({ fetch: () => assert.fail("Invalid input must not fetch") });
assert.equal((await h.dispatch({ type, srId: "1 OR 1=1", token: "test" })).ok, false);
assert.equal((await h.dispatch({ type })).ok, false);
});

View File

@@ -1,6 +1,31 @@
const assert = require("node:assert/strict");
const { test } = require("node:test");
const { loadApplication } = require("./support/application.cjs");
const flush = () => new Promise(resolve => setImmediate(resolve));
test("internal SR uses Session without TokenRelay or retries on 401", async () => {
const h = setup();
let renewals = 0;
const sent = [];
h.app.authSessionState.accessToken = "stale-token";
h.app.authTokenRelay.requestTokenRelay = async () => `fresh-${++renewals}`;
h.app.debugContentView.renderDebugPanel = () => {};
h.app.platformContentMessages.sendRuntimeMessage = async message => {
sent.push(message);
return { ok: false, httpStatus: 401, error: "Unauthorized" };
};
h.app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => [{ SrId: "123", SrNumber: "SR123", OpportunityName_Id_c: { OptyNumber: "OP1" } }];
await h.app.opportunityCrmServiceRequests.load("synthetic", 1);
const badge = h.app.opportunityCrmServiceRequests.createBadge("OP1");
h.modal.append(badge);
badge.listeners.pointerenter();
await flush();
assert.equal(renewals, 0);
assert.equal(sent.length, 1);
assert.equal(sent[0].token, undefined);
assert.ok(sent.every(item => item.type === "opportunitiesExtension.requestInternalServiceRequest"));
assert.ok(h.walk(h.panel()).some(node => node.textContent === "Unauthorized"));
});
function setup() {
const document = { activeElement: null, listeners: new Map() };
@@ -104,3 +129,42 @@ test("SR popover pins on click, closes on Escape/outside/scroll/reset and remove
assert.equal(panel(), undefined);
assert.equal(document.listeners.size, 0);
});
test("SR tooltip loads nested details with copy/link and empty text, discarding stale responses", async () => {
const h = setup();
const calls = [];
h.app.authSessionState.accessToken = "crm-token";
h.app.authTokenRelay.requestTokenRelay = async () => "fresh-crm-token";
h.app.debugContentView.renderDebugPanel = () => {};
h.app.platformContentMessages.sendRuntimeMessage = message => new Promise(resolve => calls.push({ message, resolve }));
await h.app.opportunityCrmServiceRequests.load("synthetic", 1);
const badge = h.app.opportunityCrmServiceRequests.createBadge("OP1");
h.modal.append(badge);
badge.listeners.pointerenter();
await flush();
assert.equal(calls.length, 2);
assert.equal(calls[0].message.token, undefined);
assert.equal(calls[0].message.srId, "NOT-THE-URL-ID");
assert.ok(h.walk(h.panel()).some(node => node.textContent === "Loading SR details..."));
calls[0].resolve({ ok: true, items: [{ SrId: 99, SrNumber: "SR &child", Title: "<child>", ServiceNew_c: "Queue" }] });
calls[1].resolve({ ok: true, items: [] });
await flush();
const nodes = h.walk(h.panel());
const link = nodes.find(node => node.tag === "a" && node.textContent === "SR &child");
assert.equal(link.parent.children[0].textContent, "Internal SR Number:");
assert.ok(link.href.endsWith("srNumber=SR%20%26child&srId=99"));
assert.equal(link.parent.children[2].title, "Copy SR number");
assert.ok(nodes.some(node => node.textContent.includes("Service Queue: Queue")));
assert.ok(nodes.some(node => node.textContent === "No further details."));
h.app.crmSrPopover?.close();
h.app.opportunityCrmServiceRequests.reset();
badge.listeners.pointerenter();
const oldPanel = h.panel();
await flush();
h.app.opportunityCrmServiceRequests.reset();
calls[2].resolve({ ok: true, items: [{ SrNumber: "STALE" }] });
calls[3].resolve({ ok: false, error: "Forbidden" });
await flush();
assert.equal(h.panel(), undefined);
assert.ok(!h.walk(oldPanel).some(node => node.textContent === "STALE"));
});