Compare commits
2 Commits
4d3f1f9760
...
f512ef9928
| Author | SHA1 | Date | |
|---|---|---|---|
|
f512ef9928
|
|||
|
3fd89a307c
|
226
dist/chromium/background.js
vendored
226
dist/chromium/background.js
vendored
@@ -18,6 +18,11 @@ function createMessageTypes() {
|
|||||||
requestActionFindLookup: "opportunitiesExtension.requestActionFindLookup",
|
requestActionFindLookup: "opportunitiesExtension.requestActionFindLookup",
|
||||||
requestMgmtAction: "opportunitiesExtension.requestMgmtAction",
|
requestMgmtAction: "opportunitiesExtension.requestMgmtAction",
|
||||||
requestInternalServiceRequest: "opportunitiesExtension.requestInternalServiceRequest",
|
requestInternalServiceRequest: "opportunitiesExtension.requestInternalServiceRequest",
|
||||||
|
requestSrLookupPrimaryPillar: "opportunitiesExtension.requestSrLookupPrimaryPillar",
|
||||||
|
prepareSrSession: "opportunitiesExtension.prepareSrSession",
|
||||||
|
requestSrLookupLanguage: "opportunitiesExtension.requestSrLookupLanguage",
|
||||||
|
requestSrLookupTechPlatform: "opportunitiesExtension.requestSrLookupTechPlatform",
|
||||||
|
requestSrLookupOpportunityBusiness: "opportunitiesExtension.requestSrLookupOpportunityBusiness",
|
||||||
requestCurrentUser: "opportunitiesExtension.requestCurrentUser",
|
requestCurrentUser: "opportunitiesExtension.requestCurrentUser",
|
||||||
invalidateProductsCache: "opportunitiesExtension.invalidateProductsCache",
|
invalidateProductsCache: "opportunitiesExtension.invalidateProductsCache",
|
||||||
requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend",
|
requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend",
|
||||||
@@ -743,7 +748,149 @@ function createSpaActionOptions({ fetchJson }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Source: src/services/fusion/sr-details.js
|
// Source: src/services/fusion/sr-details.js
|
||||||
function createFusionSrDetails({ fetchJson }) {
|
function createFusionSrDetails({ fetchJson, tabsCreate, tabsRemove, supportsFirefoxCookieStoreTabs, cookiesGetAll, cookieChanges, wait }) {
|
||||||
|
const sessions = new Map();
|
||||||
|
function cookiePart(cookie) {
|
||||||
|
if (!cookie) return null;
|
||||||
|
const match = /^ORA_OCIS_CG_SESSION_idcs-([a-zA-Z0-9-]+)_comcipapic-oalprod\.integration\.ocp\.oraclecloud\.com_([12])$/.exec(cookie.name);
|
||||||
|
const domain = "comcipapic-oalprod.integration.ocp.oraclecloud.com";
|
||||||
|
const cookieDomain = cookie.domain?.replace(/^\./, "");
|
||||||
|
if (!match || (cookieDomain && cookieDomain !== domain && !domain.endsWith(`.${cookieDomain}`))) return null;
|
||||||
|
return { key: `${match[1]}|${cookie.path || "/"}|${cookie.storeId || ""}`, part: match[2],
|
||||||
|
valid: Boolean(cookie.value) && (cookie.expirationDate == null || cookie.expirationDate > Date.now() / 1000) };
|
||||||
|
}
|
||||||
|
function watchSession(session, sender) {
|
||||||
|
const pairs = new Map();
|
||||||
|
let available;
|
||||||
|
session.cookieReady = new Promise(resolve => { available = resolve; });
|
||||||
|
const listener = change => {
|
||||||
|
const cookie = change.cookie;
|
||||||
|
const part = cookiePart(cookie);
|
||||||
|
if (!part) return;
|
||||||
|
if (sender?.tab?.cookieStoreId && cookie.storeId !== sender.tab.cookieStoreId) return;
|
||||||
|
if (!pairs.has(part.key)) pairs.set(part.key, new Map());
|
||||||
|
const pair = pairs.get(part.key);
|
||||||
|
if (change.removed || !part.valid) pair.delete(part.part);
|
||||||
|
else pair.set(part.part, cookie);
|
||||||
|
if (pair.size === 2 && [...pair.values()].every(item => cookiePart(item)?.valid)) available(new Set([part.key]));
|
||||||
|
session.wake?.();
|
||||||
|
};
|
||||||
|
cookieChanges?.addListener(listener);
|
||||||
|
session.unwatch = () => cookieChanges?.removeListener(listener);
|
||||||
|
}
|
||||||
|
function waitForSessionCookies(session, sender) {
|
||||||
|
if (session.cookieWait) return session.cookieWait;
|
||||||
|
session.cookieWait = (async () => {
|
||||||
|
const deadline = Date.now() + 1000;
|
||||||
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||||
|
// Install the wake-up before reading so a renewal during getAll is not missed.
|
||||||
|
const changed = new Promise(resolve => { session.wake = resolve; });
|
||||||
|
const current = await sessionCookies(sender);
|
||||||
|
const renewed = [...current].some(pair => !session.baseline.has(pair));
|
||||||
|
if (renewed || Date.now() >= deadline) {
|
||||||
|
session.baseline = current;
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
await Promise.race([changed, wait(250)]);
|
||||||
|
}
|
||||||
|
return sessionCookies(sender);
|
||||||
|
})().finally(() => { session.cookieWait = null; session.wake = null; });
|
||||||
|
return session.cookieWait;
|
||||||
|
}
|
||||||
|
async function sessionCookies(sender) {
|
||||||
|
const domain = "comcipapic-oalprod.integration.ocp.oraclecloud.com";
|
||||||
|
const details = { domain };
|
||||||
|
if (sender?.tab?.cookieStoreId) details.storeId = sender.tab.cookieStoreId;
|
||||||
|
try {
|
||||||
|
const groups = new Map();
|
||||||
|
for (const cookie of await cookiesGetAll(details)) {
|
||||||
|
const part = cookiePart(cookie);
|
||||||
|
if (!part?.valid) continue;
|
||||||
|
if (!groups.has(part.key)) groups.set(part.key, {});
|
||||||
|
groups.get(part.key)[part.part] = [cookie.name, cookie.value, cookie.expirationDate ?? null];
|
||||||
|
}
|
||||||
|
// Fingerprints remain in memory only; the browser sends the actual Cookie header.
|
||||||
|
return new Set([...groups.values()].filter(pair => pair[1] && pair[2]).map(pair => JSON.stringify([pair[1], pair[2]])));
|
||||||
|
} catch { return new Set(); }
|
||||||
|
}
|
||||||
|
async function requestLookupWithSession(operation, message, sender, initialize = false) {
|
||||||
|
const existingCookies = initialize ? await sessionCookies(sender) : null;
|
||||||
|
if (existingCookies?.size) return { ok: true, debugRequests: [] };
|
||||||
|
const logs = [];
|
||||||
|
const query = async () => {
|
||||||
|
try {
|
||||||
|
const result = await operation(message);
|
||||||
|
logs.push(...(result.debugRequests || []));
|
||||||
|
return { ...result, debugRequests: logs };
|
||||||
|
} catch (error) {
|
||||||
|
logs.push(...(error.debugRequests || []));
|
||||||
|
error.debugRequests = logs;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (!initialize) {
|
||||||
|
try { return await query(); } catch (error) { if (error.httpStatus !== 401) throw error; }
|
||||||
|
}
|
||||||
|
const key = sender?.tab?.cookieStoreId || "default";
|
||||||
|
let session = sessions.get(key);
|
||||||
|
if (!session) {
|
||||||
|
const details = {
|
||||||
|
url: "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_initiation/live/webApps/external/",
|
||||||
|
active: false
|
||||||
|
};
|
||||||
|
if (Number.isInteger(sender?.tab?.windowId)) details.windowId = sender.tab.windowId;
|
||||||
|
if (supportsFirefoxCookieStoreTabs() && sender?.tab?.cookieStoreId) details.cookieStoreId = sender.tab.cookieStoreId;
|
||||||
|
session = { users: 0, closed: null, baseline: null };
|
||||||
|
watchSession(session, sender);
|
||||||
|
session.tab = (async () => {
|
||||||
|
session.baseline = existingCookies || await sessionCookies(sender);
|
||||||
|
return tabsCreate(details);
|
||||||
|
})();
|
||||||
|
sessions.set(key, session);
|
||||||
|
}
|
||||||
|
session.users += 1;
|
||||||
|
let tab;
|
||||||
|
try {
|
||||||
|
tab = await session.tab;
|
||||||
|
const deadline = Date.now() + 60000;
|
||||||
|
for (let attempt = 0; attempt < 240 && Date.now() < deadline; attempt += 1) {
|
||||||
|
const pendingCookies = initialize && attempt === 0
|
||||||
|
? sessionCookies(sender) : waitForSessionCookies(session, sender);
|
||||||
|
const cookies = await (initialize ? Promise.race([session.cookieReady, pendingCookies]) : pendingCookies);
|
||||||
|
if (initialize) {
|
||||||
|
// Proactive preparation observes cookies only; API requests belong to the form.
|
||||||
|
if (!cookies.size) continue;
|
||||||
|
await closeSessionTab();
|
||||||
|
return { ok: true, debugRequests: [] };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await query();
|
||||||
|
// Any successful lookup confirms the shared session; stop the page immediately.
|
||||||
|
await closeSessionTab();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.httpStatus !== 401) throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("Service Request session could not be established after 60 seconds. Sign in to Oracle and retry.");
|
||||||
|
} catch (error) {
|
||||||
|
error.debugRequests = logs;
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
session.users -= 1;
|
||||||
|
if (session.users === 0) {
|
||||||
|
sessions.delete(key);
|
||||||
|
session.unwatch();
|
||||||
|
await closeSessionTab();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function closeSessionTab() {
|
||||||
|
if (!session.closed && Number.isInteger(tab?.id)) {
|
||||||
|
session.closed = tabsRemove(tab.id).catch(() => {});
|
||||||
|
}
|
||||||
|
return session.closed;
|
||||||
|
}
|
||||||
|
}
|
||||||
async function requestInternalServiceRequest(message) {
|
async function requestInternalServiceRequest(message) {
|
||||||
const debugRequests = [];
|
const debugRequests = [];
|
||||||
try {
|
try {
|
||||||
@@ -752,10 +899,14 @@ function createFusionSrDetails({ fetchJson }) {
|
|||||||
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 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"
|
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}`)}`;
|
+ `?fields=${encodeURIComponent(fields)}&onlyData=true&q=${encodeURIComponent(`SrId=${id}`)}`;
|
||||||
const result = await fetchJson(url, { method: "GET", credentials: "include",
|
const query = async () => {
|
||||||
|
const result = await fetchJson(url, { method: "GET", credentials: "include",
|
||||||
headers: { Accept: "application/json", Authorization: "Session" }
|
headers: { Accept: "application/json", Authorization: "Session" }
|
||||||
}, "requestInternalServiceRequest", debugRequests);
|
}, "requestInternalServiceRequest", debugRequests);
|
||||||
if (!Array.isArray(result?.items)) throw new Error("Invalid SR details response.");
|
if (!Array.isArray(result?.items)) throw new Error("Invalid SR details response.");
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
const result = await query();
|
||||||
const items = result.items.flatMap(item => {
|
const items = result.items.flatMap(item => {
|
||||||
const children = item.ServiceRequest_SRToInternalSR_Tgt;
|
const children = item.ServiceRequest_SRToInternalSR_Tgt;
|
||||||
if (children == null) return [];
|
if (children == null) return [];
|
||||||
@@ -768,7 +919,36 @@ function createFusionSrDetails({ fetchJson }) {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { requestInternalServiceRequest };
|
async function requestLookup(lookupType, requestName) {
|
||||||
|
const debugRequests = [];
|
||||||
|
try {
|
||||||
|
const url = `https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_initiation/live;profile=PROD/services/auth/1.1/proxy/oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/lookups?lookupType=${lookupType}&type=STANDARD`;
|
||||||
|
const result = await fetchJson(url, { method: "GET", credentials: "include",
|
||||||
|
headers: { Accept: "application/json", Authorization: "Session" }
|
||||||
|
}, requestName, debugRequests);
|
||||||
|
if (!Array.isArray(result?.items) || result.items.some(item => typeof item?.code !== "string" || typeof item?.name !== "string")) throw new Error(`Invalid ${requestName} response.`);
|
||||||
|
const items = result.items.filter(item => item.enabledFlag !== "N")
|
||||||
|
.sort((a, b) => (Number(a.displaySequence) || 0) - (Number(b.displaySequence) || 0))
|
||||||
|
.map(({ code, name }) => ({ code, name }));
|
||||||
|
return { ok: true, items, debugRequests };
|
||||||
|
} catch (error) { error.debugRequests = debugRequests; throw error; }
|
||||||
|
}
|
||||||
|
async function requestSrLookupOpportunityBusiness(message) {
|
||||||
|
const debugRequests = [];
|
||||||
|
try {
|
||||||
|
if (typeof message.emailAddress !== "string" || !message.emailAddress.trim()) throw new Error("Current user email is unavailable.");
|
||||||
|
const url = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_initiation/live;profile=PROD/services/auth/1.1/proxy/oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/services"
|
||||||
|
+ `?emailAddress=${encodeURIComponent(message.emailAddress.trim())}&salesObject=OPPORTUNITY&serviceType=BUSINESS`;
|
||||||
|
const result = await fetchJson(url, { method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" } }, "requestSrLookupOpportunityBusiness", debugRequests);
|
||||||
|
if (!Array.isArray(result?.items) || result.items.some(item => !/^\d+$/.test(String(item?.Id ?? "")) || typeof item?.RecordName !== "string")) throw new Error("Invalid Services response.");
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
return { requestLookupWithSession, requestInternalServiceRequest, requestSrLookupOpportunityBusiness,
|
||||||
|
prepareSession: sender => requestLookupWithSession(null, {}, sender, true),
|
||||||
|
requestSrLookupPrimaryPillar: () => requestLookup("SENGG_PRIMARY_PILLAR", "requestSrLookupPrimaryPillar"),
|
||||||
|
requestSrLookupLanguage: () => requestLookup("SENGG_LANGUAGE", "requestSrLookupLanguage"),
|
||||||
|
requestSrLookupTechPlatform: () => requestLookup("SENGG_TECH_PLATFORM", "requestSrLookupTechPlatform") };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Source: src/services/spa/current-user.js
|
// Source: src/services/spa/current-user.js
|
||||||
@@ -1870,6 +2050,12 @@ function registerBackgroundMessages({
|
|||||||
requestActionFindLookup,
|
requestActionFindLookup,
|
||||||
requestMgmtAction,
|
requestMgmtAction,
|
||||||
requestInternalServiceRequest,
|
requestInternalServiceRequest,
|
||||||
|
requestSrLookupPrimaryPillar,
|
||||||
|
requestSrLookupWithSession,
|
||||||
|
prepareSrSession,
|
||||||
|
requestSrLookupLanguage,
|
||||||
|
requestSrLookupTechPlatform,
|
||||||
|
requestSrLookupOpportunityBusiness,
|
||||||
requestCurrentUser,
|
requestCurrentUser,
|
||||||
requestConsumerTokenService,
|
requestConsumerTokenService,
|
||||||
requestConsumptionTrend,
|
requestConsumptionTrend,
|
||||||
@@ -1882,6 +2068,22 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
if (!message || !message.type) {
|
if (!message || !message.type) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (message.type === messageTypes.prepareSrSession) {
|
||||||
|
prepareSrSession(sender).then(sendResponse).catch(error => sendResponse({ ok: false,
|
||||||
|
error: error.message, debugRequests: error.debugRequests || [] }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const srLookups = {
|
||||||
|
[messageTypes.requestSrLookupPrimaryPillar]: requestSrLookupPrimaryPillar,
|
||||||
|
[messageTypes.requestSrLookupLanguage]: requestSrLookupLanguage,
|
||||||
|
[messageTypes.requestSrLookupTechPlatform]: requestSrLookupTechPlatform,
|
||||||
|
[messageTypes.requestSrLookupOpportunityBusiness]: requestSrLookupOpportunityBusiness
|
||||||
|
};
|
||||||
|
if (Object.hasOwn(srLookups, message.type)) {
|
||||||
|
requestSrLookupWithSession(srLookups[message.type], message, sender).then(sendResponse).catch(error => sendResponse({ ok: false,
|
||||||
|
httpStatus: error.httpStatus, error: error.message, debugRequests: error.debugRequests || [] }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (message.type === messageTypes.invalidateProductsCache) {
|
if (message.type === messageTypes.invalidateProductsCache) {
|
||||||
invalidateProductsCache().then(() => sendResponse({ ok: true })).catch(error => sendResponse({ ok: false, error: error.message || "Unable to clear products cache." }));
|
invalidateProductsCache().then(() => sendResponse({ ok: true })).catch(error => sendResponse({ ok: false, error: error.message || "Unable to clear products cache." }));
|
||||||
@@ -1966,7 +2168,7 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
|
|
||||||
if ([messageTypes.requestActionTeamMembers, messageTypes.requestActionFindLookup, messageTypes.requestMgmtAction, messageTypes.requestInternalServiceRequest].includes(message.type)) {
|
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;
|
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({
|
request(message, sender).then(sendResponse).catch(error => sendResponse({
|
||||||
ok: false, httpStatus: error.httpStatus, error: error.message || "Unable to complete request.",
|
ok: false, httpStatus: error.httpStatus, error: error.message || "Unable to complete request.",
|
||||||
debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : []
|
debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : []
|
||||||
}));
|
}));
|
||||||
@@ -2173,9 +2375,17 @@ function createBackgroundApplication(overrides = {}) {
|
|||||||
const requestRatecardByPlan = (...args) => spaConsumption.requestRatecardByPlan(...args);
|
const requestRatecardByPlan = (...args) => spaConsumption.requestRatecardByPlan(...args);
|
||||||
const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args);
|
const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args);
|
||||||
const runtimeApi = environment.browser || environment.chrome;
|
const runtimeApi = environment.browser || environment.chrome;
|
||||||
const fusionSrDetails = createFusionSrDetails({ fetchJson: (...args) => httpBackgroundJson.fetchJson(...args) });
|
const fusionSrDetails = createFusionSrDetails({
|
||||||
|
cookieChanges: runtimeApi?.cookies?.onChanged,
|
||||||
|
cookiesGetAll: (...args) => platformBackgroundApi.cookiesGetAll(...args),
|
||||||
|
tabsCreate: (...args) => platformBackgroundApi.tabsCreate(...args),
|
||||||
|
tabsRemove: (...args) => platformBackgroundApi.tabsRemove(...args),
|
||||||
|
supportsFirefoxCookieStoreTabs: () => platformBackgroundApi.supportsFirefoxCookieStoreTabs(),
|
||||||
|
wait: (...args) => platformBackgroundApi.wait(...args),
|
||||||
|
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args)
|
||||||
|
});
|
||||||
function start() {
|
function start() {
|
||||||
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 });
|
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 });
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
start, contracts,
|
start, contracts,
|
||||||
|
|||||||
613
dist/chromium/content.js
vendored
613
dist/chromium/content.js
vendored
File diff suppressed because one or more lines are too long
226
dist/firefox/background.js
vendored
226
dist/firefox/background.js
vendored
@@ -18,6 +18,11 @@ function createMessageTypes() {
|
|||||||
requestActionFindLookup: "opportunitiesExtension.requestActionFindLookup",
|
requestActionFindLookup: "opportunitiesExtension.requestActionFindLookup",
|
||||||
requestMgmtAction: "opportunitiesExtension.requestMgmtAction",
|
requestMgmtAction: "opportunitiesExtension.requestMgmtAction",
|
||||||
requestInternalServiceRequest: "opportunitiesExtension.requestInternalServiceRequest",
|
requestInternalServiceRequest: "opportunitiesExtension.requestInternalServiceRequest",
|
||||||
|
requestSrLookupPrimaryPillar: "opportunitiesExtension.requestSrLookupPrimaryPillar",
|
||||||
|
prepareSrSession: "opportunitiesExtension.prepareSrSession",
|
||||||
|
requestSrLookupLanguage: "opportunitiesExtension.requestSrLookupLanguage",
|
||||||
|
requestSrLookupTechPlatform: "opportunitiesExtension.requestSrLookupTechPlatform",
|
||||||
|
requestSrLookupOpportunityBusiness: "opportunitiesExtension.requestSrLookupOpportunityBusiness",
|
||||||
requestCurrentUser: "opportunitiesExtension.requestCurrentUser",
|
requestCurrentUser: "opportunitiesExtension.requestCurrentUser",
|
||||||
invalidateProductsCache: "opportunitiesExtension.invalidateProductsCache",
|
invalidateProductsCache: "opportunitiesExtension.invalidateProductsCache",
|
||||||
requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend",
|
requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend",
|
||||||
@@ -743,7 +748,149 @@ function createSpaActionOptions({ fetchJson }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Source: src/services/fusion/sr-details.js
|
// Source: src/services/fusion/sr-details.js
|
||||||
function createFusionSrDetails({ fetchJson }) {
|
function createFusionSrDetails({ fetchJson, tabsCreate, tabsRemove, supportsFirefoxCookieStoreTabs, cookiesGetAll, cookieChanges, wait }) {
|
||||||
|
const sessions = new Map();
|
||||||
|
function cookiePart(cookie) {
|
||||||
|
if (!cookie) return null;
|
||||||
|
const match = /^ORA_OCIS_CG_SESSION_idcs-([a-zA-Z0-9-]+)_comcipapic-oalprod\.integration\.ocp\.oraclecloud\.com_([12])$/.exec(cookie.name);
|
||||||
|
const domain = "comcipapic-oalprod.integration.ocp.oraclecloud.com";
|
||||||
|
const cookieDomain = cookie.domain?.replace(/^\./, "");
|
||||||
|
if (!match || (cookieDomain && cookieDomain !== domain && !domain.endsWith(`.${cookieDomain}`))) return null;
|
||||||
|
return { key: `${match[1]}|${cookie.path || "/"}|${cookie.storeId || ""}`, part: match[2],
|
||||||
|
valid: Boolean(cookie.value) && (cookie.expirationDate == null || cookie.expirationDate > Date.now() / 1000) };
|
||||||
|
}
|
||||||
|
function watchSession(session, sender) {
|
||||||
|
const pairs = new Map();
|
||||||
|
let available;
|
||||||
|
session.cookieReady = new Promise(resolve => { available = resolve; });
|
||||||
|
const listener = change => {
|
||||||
|
const cookie = change.cookie;
|
||||||
|
const part = cookiePart(cookie);
|
||||||
|
if (!part) return;
|
||||||
|
if (sender?.tab?.cookieStoreId && cookie.storeId !== sender.tab.cookieStoreId) return;
|
||||||
|
if (!pairs.has(part.key)) pairs.set(part.key, new Map());
|
||||||
|
const pair = pairs.get(part.key);
|
||||||
|
if (change.removed || !part.valid) pair.delete(part.part);
|
||||||
|
else pair.set(part.part, cookie);
|
||||||
|
if (pair.size === 2 && [...pair.values()].every(item => cookiePart(item)?.valid)) available(new Set([part.key]));
|
||||||
|
session.wake?.();
|
||||||
|
};
|
||||||
|
cookieChanges?.addListener(listener);
|
||||||
|
session.unwatch = () => cookieChanges?.removeListener(listener);
|
||||||
|
}
|
||||||
|
function waitForSessionCookies(session, sender) {
|
||||||
|
if (session.cookieWait) return session.cookieWait;
|
||||||
|
session.cookieWait = (async () => {
|
||||||
|
const deadline = Date.now() + 1000;
|
||||||
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||||
|
// Install the wake-up before reading so a renewal during getAll is not missed.
|
||||||
|
const changed = new Promise(resolve => { session.wake = resolve; });
|
||||||
|
const current = await sessionCookies(sender);
|
||||||
|
const renewed = [...current].some(pair => !session.baseline.has(pair));
|
||||||
|
if (renewed || Date.now() >= deadline) {
|
||||||
|
session.baseline = current;
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
await Promise.race([changed, wait(250)]);
|
||||||
|
}
|
||||||
|
return sessionCookies(sender);
|
||||||
|
})().finally(() => { session.cookieWait = null; session.wake = null; });
|
||||||
|
return session.cookieWait;
|
||||||
|
}
|
||||||
|
async function sessionCookies(sender) {
|
||||||
|
const domain = "comcipapic-oalprod.integration.ocp.oraclecloud.com";
|
||||||
|
const details = { domain };
|
||||||
|
if (sender?.tab?.cookieStoreId) details.storeId = sender.tab.cookieStoreId;
|
||||||
|
try {
|
||||||
|
const groups = new Map();
|
||||||
|
for (const cookie of await cookiesGetAll(details)) {
|
||||||
|
const part = cookiePart(cookie);
|
||||||
|
if (!part?.valid) continue;
|
||||||
|
if (!groups.has(part.key)) groups.set(part.key, {});
|
||||||
|
groups.get(part.key)[part.part] = [cookie.name, cookie.value, cookie.expirationDate ?? null];
|
||||||
|
}
|
||||||
|
// Fingerprints remain in memory only; the browser sends the actual Cookie header.
|
||||||
|
return new Set([...groups.values()].filter(pair => pair[1] && pair[2]).map(pair => JSON.stringify([pair[1], pair[2]])));
|
||||||
|
} catch { return new Set(); }
|
||||||
|
}
|
||||||
|
async function requestLookupWithSession(operation, message, sender, initialize = false) {
|
||||||
|
const existingCookies = initialize ? await sessionCookies(sender) : null;
|
||||||
|
if (existingCookies?.size) return { ok: true, debugRequests: [] };
|
||||||
|
const logs = [];
|
||||||
|
const query = async () => {
|
||||||
|
try {
|
||||||
|
const result = await operation(message);
|
||||||
|
logs.push(...(result.debugRequests || []));
|
||||||
|
return { ...result, debugRequests: logs };
|
||||||
|
} catch (error) {
|
||||||
|
logs.push(...(error.debugRequests || []));
|
||||||
|
error.debugRequests = logs;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (!initialize) {
|
||||||
|
try { return await query(); } catch (error) { if (error.httpStatus !== 401) throw error; }
|
||||||
|
}
|
||||||
|
const key = sender?.tab?.cookieStoreId || "default";
|
||||||
|
let session = sessions.get(key);
|
||||||
|
if (!session) {
|
||||||
|
const details = {
|
||||||
|
url: "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_initiation/live/webApps/external/",
|
||||||
|
active: false
|
||||||
|
};
|
||||||
|
if (Number.isInteger(sender?.tab?.windowId)) details.windowId = sender.tab.windowId;
|
||||||
|
if (supportsFirefoxCookieStoreTabs() && sender?.tab?.cookieStoreId) details.cookieStoreId = sender.tab.cookieStoreId;
|
||||||
|
session = { users: 0, closed: null, baseline: null };
|
||||||
|
watchSession(session, sender);
|
||||||
|
session.tab = (async () => {
|
||||||
|
session.baseline = existingCookies || await sessionCookies(sender);
|
||||||
|
return tabsCreate(details);
|
||||||
|
})();
|
||||||
|
sessions.set(key, session);
|
||||||
|
}
|
||||||
|
session.users += 1;
|
||||||
|
let tab;
|
||||||
|
try {
|
||||||
|
tab = await session.tab;
|
||||||
|
const deadline = Date.now() + 60000;
|
||||||
|
for (let attempt = 0; attempt < 240 && Date.now() < deadline; attempt += 1) {
|
||||||
|
const pendingCookies = initialize && attempt === 0
|
||||||
|
? sessionCookies(sender) : waitForSessionCookies(session, sender);
|
||||||
|
const cookies = await (initialize ? Promise.race([session.cookieReady, pendingCookies]) : pendingCookies);
|
||||||
|
if (initialize) {
|
||||||
|
// Proactive preparation observes cookies only; API requests belong to the form.
|
||||||
|
if (!cookies.size) continue;
|
||||||
|
await closeSessionTab();
|
||||||
|
return { ok: true, debugRequests: [] };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await query();
|
||||||
|
// Any successful lookup confirms the shared session; stop the page immediately.
|
||||||
|
await closeSessionTab();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.httpStatus !== 401) throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("Service Request session could not be established after 60 seconds. Sign in to Oracle and retry.");
|
||||||
|
} catch (error) {
|
||||||
|
error.debugRequests = logs;
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
session.users -= 1;
|
||||||
|
if (session.users === 0) {
|
||||||
|
sessions.delete(key);
|
||||||
|
session.unwatch();
|
||||||
|
await closeSessionTab();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function closeSessionTab() {
|
||||||
|
if (!session.closed && Number.isInteger(tab?.id)) {
|
||||||
|
session.closed = tabsRemove(tab.id).catch(() => {});
|
||||||
|
}
|
||||||
|
return session.closed;
|
||||||
|
}
|
||||||
|
}
|
||||||
async function requestInternalServiceRequest(message) {
|
async function requestInternalServiceRequest(message) {
|
||||||
const debugRequests = [];
|
const debugRequests = [];
|
||||||
try {
|
try {
|
||||||
@@ -752,10 +899,14 @@ function createFusionSrDetails({ fetchJson }) {
|
|||||||
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 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"
|
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}`)}`;
|
+ `?fields=${encodeURIComponent(fields)}&onlyData=true&q=${encodeURIComponent(`SrId=${id}`)}`;
|
||||||
const result = await fetchJson(url, { method: "GET", credentials: "include",
|
const query = async () => {
|
||||||
|
const result = await fetchJson(url, { method: "GET", credentials: "include",
|
||||||
headers: { Accept: "application/json", Authorization: "Session" }
|
headers: { Accept: "application/json", Authorization: "Session" }
|
||||||
}, "requestInternalServiceRequest", debugRequests);
|
}, "requestInternalServiceRequest", debugRequests);
|
||||||
if (!Array.isArray(result?.items)) throw new Error("Invalid SR details response.");
|
if (!Array.isArray(result?.items)) throw new Error("Invalid SR details response.");
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
const result = await query();
|
||||||
const items = result.items.flatMap(item => {
|
const items = result.items.flatMap(item => {
|
||||||
const children = item.ServiceRequest_SRToInternalSR_Tgt;
|
const children = item.ServiceRequest_SRToInternalSR_Tgt;
|
||||||
if (children == null) return [];
|
if (children == null) return [];
|
||||||
@@ -768,7 +919,36 @@ function createFusionSrDetails({ fetchJson }) {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { requestInternalServiceRequest };
|
async function requestLookup(lookupType, requestName) {
|
||||||
|
const debugRequests = [];
|
||||||
|
try {
|
||||||
|
const url = `https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_initiation/live;profile=PROD/services/auth/1.1/proxy/oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/lookups?lookupType=${lookupType}&type=STANDARD`;
|
||||||
|
const result = await fetchJson(url, { method: "GET", credentials: "include",
|
||||||
|
headers: { Accept: "application/json", Authorization: "Session" }
|
||||||
|
}, requestName, debugRequests);
|
||||||
|
if (!Array.isArray(result?.items) || result.items.some(item => typeof item?.code !== "string" || typeof item?.name !== "string")) throw new Error(`Invalid ${requestName} response.`);
|
||||||
|
const items = result.items.filter(item => item.enabledFlag !== "N")
|
||||||
|
.sort((a, b) => (Number(a.displaySequence) || 0) - (Number(b.displaySequence) || 0))
|
||||||
|
.map(({ code, name }) => ({ code, name }));
|
||||||
|
return { ok: true, items, debugRequests };
|
||||||
|
} catch (error) { error.debugRequests = debugRequests; throw error; }
|
||||||
|
}
|
||||||
|
async function requestSrLookupOpportunityBusiness(message) {
|
||||||
|
const debugRequests = [];
|
||||||
|
try {
|
||||||
|
if (typeof message.emailAddress !== "string" || !message.emailAddress.trim()) throw new Error("Current user email is unavailable.");
|
||||||
|
const url = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_initiation/live;profile=PROD/services/auth/1.1/proxy/oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/services"
|
||||||
|
+ `?emailAddress=${encodeURIComponent(message.emailAddress.trim())}&salesObject=OPPORTUNITY&serviceType=BUSINESS`;
|
||||||
|
const result = await fetchJson(url, { method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" } }, "requestSrLookupOpportunityBusiness", debugRequests);
|
||||||
|
if (!Array.isArray(result?.items) || result.items.some(item => !/^\d+$/.test(String(item?.Id ?? "")) || typeof item?.RecordName !== "string")) throw new Error("Invalid Services response.");
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
return { requestLookupWithSession, requestInternalServiceRequest, requestSrLookupOpportunityBusiness,
|
||||||
|
prepareSession: sender => requestLookupWithSession(null, {}, sender, true),
|
||||||
|
requestSrLookupPrimaryPillar: () => requestLookup("SENGG_PRIMARY_PILLAR", "requestSrLookupPrimaryPillar"),
|
||||||
|
requestSrLookupLanguage: () => requestLookup("SENGG_LANGUAGE", "requestSrLookupLanguage"),
|
||||||
|
requestSrLookupTechPlatform: () => requestLookup("SENGG_TECH_PLATFORM", "requestSrLookupTechPlatform") };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Source: src/services/spa/current-user.js
|
// Source: src/services/spa/current-user.js
|
||||||
@@ -1870,6 +2050,12 @@ function registerBackgroundMessages({
|
|||||||
requestActionFindLookup,
|
requestActionFindLookup,
|
||||||
requestMgmtAction,
|
requestMgmtAction,
|
||||||
requestInternalServiceRequest,
|
requestInternalServiceRequest,
|
||||||
|
requestSrLookupPrimaryPillar,
|
||||||
|
requestSrLookupWithSession,
|
||||||
|
prepareSrSession,
|
||||||
|
requestSrLookupLanguage,
|
||||||
|
requestSrLookupTechPlatform,
|
||||||
|
requestSrLookupOpportunityBusiness,
|
||||||
requestCurrentUser,
|
requestCurrentUser,
|
||||||
requestConsumerTokenService,
|
requestConsumerTokenService,
|
||||||
requestConsumptionTrend,
|
requestConsumptionTrend,
|
||||||
@@ -1882,6 +2068,22 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
if (!message || !message.type) {
|
if (!message || !message.type) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (message.type === messageTypes.prepareSrSession) {
|
||||||
|
prepareSrSession(sender).then(sendResponse).catch(error => sendResponse({ ok: false,
|
||||||
|
error: error.message, debugRequests: error.debugRequests || [] }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const srLookups = {
|
||||||
|
[messageTypes.requestSrLookupPrimaryPillar]: requestSrLookupPrimaryPillar,
|
||||||
|
[messageTypes.requestSrLookupLanguage]: requestSrLookupLanguage,
|
||||||
|
[messageTypes.requestSrLookupTechPlatform]: requestSrLookupTechPlatform,
|
||||||
|
[messageTypes.requestSrLookupOpportunityBusiness]: requestSrLookupOpportunityBusiness
|
||||||
|
};
|
||||||
|
if (Object.hasOwn(srLookups, message.type)) {
|
||||||
|
requestSrLookupWithSession(srLookups[message.type], message, sender).then(sendResponse).catch(error => sendResponse({ ok: false,
|
||||||
|
httpStatus: error.httpStatus, error: error.message, debugRequests: error.debugRequests || [] }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (message.type === messageTypes.invalidateProductsCache) {
|
if (message.type === messageTypes.invalidateProductsCache) {
|
||||||
invalidateProductsCache().then(() => sendResponse({ ok: true })).catch(error => sendResponse({ ok: false, error: error.message || "Unable to clear products cache." }));
|
invalidateProductsCache().then(() => sendResponse({ ok: true })).catch(error => sendResponse({ ok: false, error: error.message || "Unable to clear products cache." }));
|
||||||
@@ -1966,7 +2168,7 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
|
|
||||||
if ([messageTypes.requestActionTeamMembers, messageTypes.requestActionFindLookup, messageTypes.requestMgmtAction, messageTypes.requestInternalServiceRequest].includes(message.type)) {
|
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;
|
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({
|
request(message, sender).then(sendResponse).catch(error => sendResponse({
|
||||||
ok: false, httpStatus: error.httpStatus, error: error.message || "Unable to complete request.",
|
ok: false, httpStatus: error.httpStatus, error: error.message || "Unable to complete request.",
|
||||||
debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : []
|
debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : []
|
||||||
}));
|
}));
|
||||||
@@ -2173,9 +2375,17 @@ function createBackgroundApplication(overrides = {}) {
|
|||||||
const requestRatecardByPlan = (...args) => spaConsumption.requestRatecardByPlan(...args);
|
const requestRatecardByPlan = (...args) => spaConsumption.requestRatecardByPlan(...args);
|
||||||
const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args);
|
const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args);
|
||||||
const runtimeApi = environment.browser || environment.chrome;
|
const runtimeApi = environment.browser || environment.chrome;
|
||||||
const fusionSrDetails = createFusionSrDetails({ fetchJson: (...args) => httpBackgroundJson.fetchJson(...args) });
|
const fusionSrDetails = createFusionSrDetails({
|
||||||
|
cookieChanges: runtimeApi?.cookies?.onChanged,
|
||||||
|
cookiesGetAll: (...args) => platformBackgroundApi.cookiesGetAll(...args),
|
||||||
|
tabsCreate: (...args) => platformBackgroundApi.tabsCreate(...args),
|
||||||
|
tabsRemove: (...args) => platformBackgroundApi.tabsRemove(...args),
|
||||||
|
supportsFirefoxCookieStoreTabs: () => platformBackgroundApi.supportsFirefoxCookieStoreTabs(),
|
||||||
|
wait: (...args) => platformBackgroundApi.wait(...args),
|
||||||
|
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args)
|
||||||
|
});
|
||||||
function start() {
|
function start() {
|
||||||
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 });
|
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 });
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
start, contracts,
|
start, contracts,
|
||||||
|
|||||||
613
dist/firefox/content.js
vendored
613
dist/firefox/content.js
vendored
File diff suppressed because one or more lines are too long
@@ -39,6 +39,10 @@ module.exports = {
|
|||||||
"src/features/opportunities/activity-progress.js",
|
"src/features/opportunities/activity-progress.js",
|
||||||
"src/features/opportunities/activity-filters.js",
|
"src/features/opportunities/activity-filters.js",
|
||||||
"src/features/opportunities/crm-sr-popover.js",
|
"src/features/opportunities/crm-sr-popover.js",
|
||||||
|
"src/features/opportunities/crm-sr-form.js",
|
||||||
|
"src/features/opportunities/searchable-select.js",
|
||||||
|
"src/features/opportunities/internal-sr-session.js",
|
||||||
|
"src/features/opportunities/sr-session-preparation.js",
|
||||||
"src/features/opportunities/workload-actions.js",
|
"src/features/opportunities/workload-actions.js",
|
||||||
"src/features/opportunities/workload-action-form.js",
|
"src/features/opportunities/workload-action-form.js",
|
||||||
"src/features/opportunities/workload-action-options.js",
|
"src/features/opportunities/workload-action-options.js",
|
||||||
|
|||||||
@@ -125,9 +125,17 @@ function createBackgroundApplication(overrides = {}) {
|
|||||||
const requestRatecardByPlan = (...args) => spaConsumption.requestRatecardByPlan(...args);
|
const requestRatecardByPlan = (...args) => spaConsumption.requestRatecardByPlan(...args);
|
||||||
const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args);
|
const requestOpptyProducts = (...args) => productsBackgroundJobs.requestOpptyProducts(...args);
|
||||||
const runtimeApi = environment.browser || environment.chrome;
|
const runtimeApi = environment.browser || environment.chrome;
|
||||||
const fusionSrDetails = createFusionSrDetails({ fetchJson: (...args) => httpBackgroundJson.fetchJson(...args) });
|
const fusionSrDetails = createFusionSrDetails({
|
||||||
|
cookieChanges: runtimeApi?.cookies?.onChanged,
|
||||||
|
cookiesGetAll: (...args) => platformBackgroundApi.cookiesGetAll(...args),
|
||||||
|
tabsCreate: (...args) => platformBackgroundApi.tabsCreate(...args),
|
||||||
|
tabsRemove: (...args) => platformBackgroundApi.tabsRemove(...args),
|
||||||
|
supportsFirefoxCookieStoreTabs: () => platformBackgroundApi.supportsFirefoxCookieStoreTabs(),
|
||||||
|
wait: (...args) => platformBackgroundApi.wait(...args),
|
||||||
|
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args)
|
||||||
|
});
|
||||||
function start() {
|
function start() {
|
||||||
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 });
|
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 });
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
start, contracts,
|
start, contracts,
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ function registerBackgroundMessages({
|
|||||||
requestActionFindLookup,
|
requestActionFindLookup,
|
||||||
requestMgmtAction,
|
requestMgmtAction,
|
||||||
requestInternalServiceRequest,
|
requestInternalServiceRequest,
|
||||||
|
requestSrLookupPrimaryPillar,
|
||||||
|
requestSrLookupWithSession,
|
||||||
|
prepareSrSession,
|
||||||
|
requestSrLookupLanguage,
|
||||||
|
requestSrLookupTechPlatform,
|
||||||
|
requestSrLookupOpportunityBusiness,
|
||||||
requestCurrentUser,
|
requestCurrentUser,
|
||||||
requestConsumerTokenService,
|
requestConsumerTokenService,
|
||||||
requestConsumptionTrend,
|
requestConsumptionTrend,
|
||||||
@@ -24,6 +30,22 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
if (!message || !message.type) {
|
if (!message || !message.type) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (message.type === messageTypes.prepareSrSession) {
|
||||||
|
prepareSrSession(sender).then(sendResponse).catch(error => sendResponse({ ok: false,
|
||||||
|
error: error.message, debugRequests: error.debugRequests || [] }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const srLookups = {
|
||||||
|
[messageTypes.requestSrLookupPrimaryPillar]: requestSrLookupPrimaryPillar,
|
||||||
|
[messageTypes.requestSrLookupLanguage]: requestSrLookupLanguage,
|
||||||
|
[messageTypes.requestSrLookupTechPlatform]: requestSrLookupTechPlatform,
|
||||||
|
[messageTypes.requestSrLookupOpportunityBusiness]: requestSrLookupOpportunityBusiness
|
||||||
|
};
|
||||||
|
if (Object.hasOwn(srLookups, message.type)) {
|
||||||
|
requestSrLookupWithSession(srLookups[message.type], message, sender).then(sendResponse).catch(error => sendResponse({ ok: false,
|
||||||
|
httpStatus: error.httpStatus, error: error.message, debugRequests: error.debugRequests || [] }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (message.type === messageTypes.invalidateProductsCache) {
|
if (message.type === messageTypes.invalidateProductsCache) {
|
||||||
invalidateProductsCache().then(() => sendResponse({ ok: true })).catch(error => sendResponse({ ok: false, error: error.message || "Unable to clear products cache." }));
|
invalidateProductsCache().then(() => sendResponse({ ok: true })).catch(error => sendResponse({ ok: false, error: error.message || "Unable to clear products cache." }));
|
||||||
@@ -108,7 +130,7 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
|
|
||||||
if ([messageTypes.requestActionTeamMembers, messageTypes.requestActionFindLookup, messageTypes.requestMgmtAction, messageTypes.requestInternalServiceRequest].includes(message.type)) {
|
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;
|
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({
|
request(message, sender).then(sendResponse).catch(error => sendResponse({
|
||||||
ok: false, httpStatus: error.httpStatus, error: error.message || "Unable to complete request.",
|
ok: false, httpStatus: error.httpStatus, error: error.message || "Unable to complete request.",
|
||||||
debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : []
|
debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : []
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ function createContentApplication(overrides = {}) {
|
|||||||
appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args)
|
appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args)
|
||||||
});
|
});
|
||||||
const contracts = createContentContracts();
|
const contracts = createContentContracts();
|
||||||
|
const searchableSelect = createSearchableSelect({ environment });
|
||||||
const authSessionState = createAuthSessionState(contracts);
|
const authSessionState = createAuthSessionState(contracts);
|
||||||
const authSalesCommonState = createAuthSalesCommonState();
|
const authSalesCommonState = createAuthSalesCommonState();
|
||||||
const opportunitiesQueryState = createOpportunitiesQueryState();
|
const opportunitiesQueryState = createOpportunitiesQueryState();
|
||||||
@@ -55,7 +56,8 @@ function createContentApplication(overrides = {}) {
|
|||||||
environment
|
environment
|
||||||
});
|
});
|
||||||
const contentModal = createContentModal({
|
const contentModal = createContentModal({
|
||||||
closeActionForm: (...args) => workloadActionForm.close(...args),
|
prepareSrSession: () => srSessionPreparation.ensure(),
|
||||||
|
closeActionForm: (...args) => crmSrForm.close(...args) || workloadActionForm.close(...args),
|
||||||
createActivityFilterControls: (...args) => activityFilters.createControls(...args),
|
createActivityFilterControls: (...args) => activityFilters.createControls(...args),
|
||||||
closeCrmSrPopover: () => crmSrPopover.close(),
|
closeCrmSrPopover: () => crmSrPopover.close(),
|
||||||
ensureCurrentUser: () => currentUser.ensure(),
|
ensureCurrentUser: () => currentUser.ensure(),
|
||||||
@@ -216,19 +218,43 @@ function createContentApplication(overrides = {}) {
|
|||||||
readResponseBodyForDebug: (...args) => debugContentLog.readResponseBodyForDebug(...args),
|
readResponseBodyForDebug: (...args) => debugContentLog.readResponseBodyForDebug(...args),
|
||||||
maskToken: (...args) => debugContentLog.maskToken(...args), environment
|
maskToken: (...args) => debugContentLog.maskToken(...args), environment
|
||||||
});
|
});
|
||||||
const crmSrPopover = createCrmSrPopover({
|
const internalSrSession = createInternalSrSession({
|
||||||
requestDetails: async srId => {
|
environment,
|
||||||
|
query: async srId => {
|
||||||
const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestInternalServiceRequest, srId });
|
const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestInternalServiceRequest, srId });
|
||||||
debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []);
|
debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []);
|
||||||
if (!result?.ok) throw new Error(result?.error || "Unable to load SR details.");
|
return result;
|
||||||
return result.items;
|
}
|
||||||
},
|
});
|
||||||
|
const crmSrPopover = createCrmSrPopover({
|
||||||
|
requestDetails: srId => internalSrSession.request(srId),
|
||||||
MODAL_ID: contracts.MODAL_ID,
|
MODAL_ID: contracts.MODAL_ID,
|
||||||
createCopyButton: (...args) => opportunitiesLinks.createCopyOpportunityButton(...args),
|
createCopyButton: (...args) => opportunitiesLinks.createCopyOpportunityButton(...args),
|
||||||
createCloseIcon: (...args) => contentIcons.createCloseIcon(...args), environment
|
createCloseIcon: (...args) => contentIcons.createCloseIcon(...args), environment
|
||||||
});
|
});
|
||||||
const opportunityBadgeContent = createOpportunityBadgeContent({ environment });
|
const opportunityBadgeContent = createOpportunityBadgeContent({ environment });
|
||||||
|
async function requestSrFormLookup(type, fields = {}) {
|
||||||
|
const result = await platformContentMessages.sendRuntimeMessage({ type, ...fields });
|
||||||
|
debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []);
|
||||||
|
if (!result?.ok) throw new Error(result?.error || "Unable to load Service Request options.");
|
||||||
|
return result.items;
|
||||||
|
}
|
||||||
|
const crmSrForm = createCrmSrForm({
|
||||||
|
getOpportunity: number => opportunitiesTableState.opportunitiesTableState.items.find(item => String(item.OptyNumber) === String(number)),
|
||||||
|
formatDate: value => opportunitiesFormatters.formatOracleResponseDate(value),
|
||||||
|
requestLanguage: () => requestSrFormLookup(messageTypes.requestSrLookupLanguage),
|
||||||
|
requestPlatform: () => requestSrFormLookup(messageTypes.requestSrLookupTechPlatform),
|
||||||
|
requestServices: () => requestSrFormLookup(messageTypes.requestSrLookupOpportunityBusiness, { emailAddress: currentUser.getUserId() }),
|
||||||
|
searchableSelect,
|
||||||
|
MODAL_ID: contracts.MODAL_ID, environment,
|
||||||
|
allowsCrmActivities: () => currentUser.allowsCrmActivities(),
|
||||||
|
isMissing: number => opportunityCrmServiceRequests.getSummary(number)?.state === "missing",
|
||||||
|
createCloseIcon: () => contentIcons.createCloseIcon(),
|
||||||
|
requestPillars: () => requestSrFormLookup(messageTypes.requestSrLookupPrimaryPillar)
|
||||||
|
});
|
||||||
const opportunityCrmServiceRequests = createOpportunityCrmServiceRequests({
|
const opportunityCrmServiceRequests = createOpportunityCrmServiceRequests({
|
||||||
|
createSrButton: number => crmSrForm.createButton(number),
|
||||||
|
closeSrForm: () => crmSrForm.close(false),
|
||||||
setBadgeContent: (...args) => opportunityBadgeContent.setContent(...args),
|
setBadgeContent: (...args) => opportunityBadgeContent.setContent(...args),
|
||||||
activityCache,
|
activityCache,
|
||||||
reportProgress: (...args) => activityProgress.update("sr", ...args),
|
reportProgress: (...args) => activityProgress.update("sr", ...args),
|
||||||
@@ -364,6 +390,13 @@ function createContentApplication(overrides = {}) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
const activityProgress = createActivityProgress({ MODAL_ID: contracts.MODAL_ID, environment });
|
const activityProgress = createActivityProgress({ MODAL_ID: contracts.MODAL_ID, environment });
|
||||||
|
const srSessionPreparation = createSrSessionPreparation({
|
||||||
|
ensureCurrentUser: () => currentUser.ensure(),
|
||||||
|
allowsCrmActivities: () => currentUser.allowsCrmActivities(),
|
||||||
|
sendRequest: () => platformContentMessages.sendRuntimeMessage({ type: messageTypes.prepareSrSession }),
|
||||||
|
reportProgress: state => activityProgress.update("session", state),
|
||||||
|
appendLog: entries => debugContentLog.appendBackgroundRequestLog(entries)
|
||||||
|
});
|
||||||
const activityFilters = createActivityFilters({
|
const activityFilters = createActivityFilters({
|
||||||
setBadgeContent: (...args) => opportunityBadgeContent.setContent(...args),
|
setBadgeContent: (...args) => opportunityBadgeContent.setContent(...args),
|
||||||
allowsCrmActivities: () => currentUser.allowsCrmActivities(),
|
allowsCrmActivities: () => currentUser.allowsCrmActivities(),
|
||||||
@@ -403,6 +436,7 @@ function createContentApplication(overrides = {}) {
|
|||||||
messageTypes
|
messageTypes
|
||||||
});
|
});
|
||||||
const workloadActionForm = createWorkloadActionForm({
|
const workloadActionForm = createWorkloadActionForm({
|
||||||
|
searchableSelect,
|
||||||
onActionSaved: (...args) => opportunityWorkloadActions.applySavedAction(...args),
|
onActionSaved: (...args) => opportunityWorkloadActions.applySavedAction(...args),
|
||||||
saveAction: (...args) => workloadActionOptions.save(...args),
|
saveAction: (...args) => workloadActionOptions.save(...args),
|
||||||
notify: (...args) => opportunitiesLinks.showCopyConfirmation(...args),
|
notify: (...args) => opportunitiesLinks.showCopyConfirmation(...args),
|
||||||
@@ -638,6 +672,7 @@ function createContentApplication(overrides = {}) {
|
|||||||
productsContentJobs,
|
productsContentJobs,
|
||||||
productActivities,
|
productActivities,
|
||||||
activityProgress,
|
activityProgress,
|
||||||
|
srSessionPreparation,
|
||||||
refreshAll,
|
refreshAll,
|
||||||
activityCache,
|
activityCache,
|
||||||
productsDetails,
|
productsDetails,
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ function createContentModal({
|
|||||||
closeCrmSrPopover,
|
closeCrmSrPopover,
|
||||||
requestTokenRelayOnce,
|
requestTokenRelayOnce,
|
||||||
ensureCurrentUser,
|
ensureCurrentUser,
|
||||||
|
prepareSrSession,
|
||||||
resetCustomerAndOwnerFiltersForPeriod,
|
resetCustomerAndOwnerFiltersForPeriod,
|
||||||
resetOpportunitiesTable,
|
resetOpportunitiesTable,
|
||||||
restoreFilterPreferences,
|
restoreFilterPreferences,
|
||||||
@@ -83,6 +84,7 @@ function createContentModal({
|
|||||||
|
|
||||||
if (existingModal) {
|
if (existingModal) {
|
||||||
existingModal.hidden = false;
|
existingModal.hidden = false;
|
||||||
|
prepareSrSession?.();
|
||||||
existingModal.querySelector("select").focus();
|
existingModal.querySelector("select").focus();
|
||||||
document.documentElement.classList.add("opportunities-extension-scroll-lock");
|
document.documentElement.classList.add("opportunities-extension-scroll-lock");
|
||||||
ensureEscapeKeyHandler();
|
ensureEscapeKeyHandler();
|
||||||
@@ -420,6 +422,7 @@ function createContentModal({
|
|||||||
renderOwnerFilter();
|
renderOwnerFilter();
|
||||||
renderOpportunitiesTable();
|
renderOpportunitiesTable();
|
||||||
requestOpportunitiesForPeriod(select.value);
|
requestOpportunitiesForPeriod(select.value);
|
||||||
|
prepareSrSession?.();
|
||||||
select.focus();
|
select.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function createDebugContentView({
|
|||||||
if (["requestWlActions", "requestActionTeamMembers", "requestActionFindLookup", "requestMgmtAction"].includes(entry.name)) return "actions";
|
if (["requestWlActions", "requestActionTeamMembers", "requestActionFindLookup", "requestMgmtAction"].includes(entry.name)) return "actions";
|
||||||
if (entry.name === "requestGenerateAiSummary") return "ai";
|
if (entry.name === "requestGenerateAiSummary") return "ai";
|
||||||
if (entry.name === "requestInstallBase") return "installBase";
|
if (entry.name === "requestInstallBase") return "installBase";
|
||||||
if (["requestCustomerServiceRequest", "resquestCrmServiceRequest", "requestInternalServiceRequest"].includes(entry.name)) return "serviceRequests";
|
if (["requestCustomerServiceRequest", "resquestCrmServiceRequest", "requestInternalServiceRequest", "requestSrLookupPrimaryPillar", "requestSrLookupLanguage", "requestSrLookupTechPlatform", "requestSrLookupOpportunityBusiness"].includes(entry.name)) return "serviceRequests";
|
||||||
if (/^request(?:AccountSummary|AccountPlans|ConsumptionService|ConsumptionPlanDetail|RatecardByPlan)/.test(entry.name)) return "consumption";
|
if (/^request(?:AccountSummary|AccountPlans|ConsumptionService|ConsumptionPlanDetail|RatecardByPlan)/.test(entry.name)) return "consumption";
|
||||||
return "other";
|
return "other";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function createActivityProgress({ MODAL_ID, environment }) {
|
|||||||
clearTimeout(entry.timer);
|
clearTimeout(entry.timer);
|
||||||
clearTimeout(entry.hideTimer);
|
clearTimeout(entry.hideTimer);
|
||||||
entry.state = state;
|
entry.state = state;
|
||||||
const label = key === "sr" ? "Service Requests" : "Actions";
|
const label = key === "session" ? "SR session" : key === "sr" ? "Service Requests" : "Actions";
|
||||||
entry.badge.hidden = state === "idle";
|
entry.badge.hidden = state === "idle";
|
||||||
entry.badge.setAttribute("data-state", state);
|
entry.badge.setAttribute("data-state", state);
|
||||||
entry.badge.setAttribute("data-fading", "false");
|
entry.badge.setAttribute("data-fading", "false");
|
||||||
@@ -27,7 +27,8 @@ function createActivityProgress({ MODAL_ID, environment }) {
|
|||||||
entry.badge.textContent = state === "error" ? `${label} unavailable`
|
entry.badge.textContent = state === "error" ? `${label} unavailable`
|
||||||
: state === "complete-with-errors" ? `${label} ${count} (errors)`
|
: state === "complete-with-errors" ? `${label} ${count} (errors)`
|
||||||
: state === "complete" ? `${label} ready ${completed}` : `${label} ${count}`;
|
: state === "complete" ? `${label} ready ${completed}` : `${label} ${count}`;
|
||||||
entry.badge.title = key === "sr" ? "CRM service request records loaded" : "Workload action requests completed";
|
if (key === "session") entry.badge.textContent = state === "complete" ? "SR session ready" : state === "error" ? "SR session unavailable" : "SR session loading";
|
||||||
|
entry.badge.title = key === "session" ? "Service Request session initialization" : key === "sr" ? "CRM service request records loaded" : "Workload action requests completed";
|
||||||
if (state === "complete") entry.timer = setTimeout(() => {
|
if (state === "complete") entry.timer = setTimeout(() => {
|
||||||
entry.badge.setAttribute("data-fading", "true");
|
entry.badge.setAttribute("data-fading", "true");
|
||||||
const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurrentPeriodRequest, bindPopover, closePopover, allowsCrmActivities, reportProgress, activityCache, setBadgeContent, environment }) {
|
function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurrentPeriodRequest, bindPopover, closePopover, createSrButton, closeSrForm, allowsCrmActivities, reportProgress, activityCache, setBadgeContent, environment }) {
|
||||||
let status = "idle";
|
let status = "idle";
|
||||||
let byOpportunity = new Map();
|
let byOpportunity = new Map();
|
||||||
let pending = null;
|
let pending = null;
|
||||||
let generation = 0;
|
let generation = 0;
|
||||||
let loadedItems = [];
|
let loadedItems = [];
|
||||||
function reset() { closePopover(); generation += 1; pending = null; status = "idle"; byOpportunity = new Map(); loadedItems = []; reportProgress("idle"); }
|
function reset() { closeSrForm(); closePopover(); generation += 1; pending = null; status = "idle"; byOpportunity = new Map(); loadedItems = []; reportProgress("idle"); }
|
||||||
function load(token, version, cacheOptions = null) {
|
function load(token, version, cacheOptions = null) {
|
||||||
if (!allowsCrmActivities() || !isCurrentPeriodRequest(version)) return;
|
if (!allowsCrmActivities() || !isCurrentPeriodRequest(version)) return;
|
||||||
if (status === "ready") {
|
if (status === "ready") {
|
||||||
@@ -89,6 +89,7 @@ function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurre
|
|||||||
badge.setAttribute("aria-label", description);
|
badge.setAttribute("aria-label", description);
|
||||||
badge.title = "Whether a Service Request has been created for the opportunities.";
|
badge.title = "Whether a Service Request has been created for the opportunities.";
|
||||||
if (items.length && status === "ready") bindPopover(badge, items, descriptions);
|
if (items.length && status === "ready") bindPopover(badge, items, descriptions);
|
||||||
|
if (!items.length && status === "ready") badge.append(createSrButton(String(optyNumber)));
|
||||||
badge.tabIndex = 0;
|
badge.tabIndex = 0;
|
||||||
return badge;
|
return badge;
|
||||||
}
|
}
|
||||||
|
|||||||
237
src/features/opportunities/crm-sr-form.js
Normal file
237
src/features/opportunities/crm-sr-form.js
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
function createCrmSrForm({ MODAL_ID, allowsCrmActivities, isMissing, getOpportunity, formatDate, requestPillars, requestLanguage, requestPlatform, requestServices, createCloseIcon, searchableSelect, environment }) {
|
||||||
|
const { document } = environment;
|
||||||
|
let active = null;
|
||||||
|
function node(tag, className, text) {
|
||||||
|
const element = document.createElement(tag);
|
||||||
|
if (className) element.className = className;
|
||||||
|
if (text !== undefined) element.textContent = text;
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
function close(restoreFocus = true) {
|
||||||
|
if (!active) return false;
|
||||||
|
const previous = active;
|
||||||
|
active = null;
|
||||||
|
previous.modal.remove();
|
||||||
|
if (restoreFocus && previous.trigger?.isConnected) previous.trigger.focus();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function createButton(number) {
|
||||||
|
const button = node("button", "opportunities-extension-add-action", "+");
|
||||||
|
button.type = "button";
|
||||||
|
button.title = "Add Service Request";
|
||||||
|
button.setAttribute("aria-label", `Add Service Request for ${number}`);
|
||||||
|
button.setAttribute("aria-haspopup", "dialog");
|
||||||
|
button.addEventListener("click", event => { event.preventDefault(); event.stopPropagation(); open(number, button); });
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
function open(number, trigger) {
|
||||||
|
const host = document.getElementById(MODAL_ID);
|
||||||
|
if (!host || host.hidden || !allowsCrmActivities() || !isMissing(number)) 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", "opportunities-extension-sr-form-title");
|
||||||
|
const dialog = node("div", "opportunities-extension-action-form-dialog opportunities-extension-sr-form-dialog");
|
||||||
|
const header = node("header", "opportunities-extension-account-plans-header");
|
||||||
|
const heading = node("div", "opportunities-extension-action-form-heading");
|
||||||
|
const title = node("h2", "", "Add Service Request");
|
||||||
|
title.id = "opportunities-extension-sr-form-title";
|
||||||
|
heading.append(title, node("p", "", String(number)));
|
||||||
|
const opportunity = getOpportunity(number);
|
||||||
|
const details = node("dl", "opportunities-extension-action-form-context");
|
||||||
|
for (const [label, value] of [
|
||||||
|
["Customer", opportunity?.CustomerAccount?.PartyUniqueName],
|
||||||
|
["Name", opportunity?.Name],
|
||||||
|
["Close Date", formatDate(opportunity?.EffectiveDate)]
|
||||||
|
]) {
|
||||||
|
details.append(node("dt", "", label), node("dd", "", value == null || value === "" ? "-" : String(value)));
|
||||||
|
}
|
||||||
|
heading.append(details);
|
||||||
|
const dismiss = node("button", "opportunities-extension-icon-button");
|
||||||
|
dismiss.type = "button";
|
||||||
|
dismiss.setAttribute("aria-label", "Close Service Request form");
|
||||||
|
dismiss.append(createCloseIcon());
|
||||||
|
dismiss.addEventListener("click", () => close());
|
||||||
|
header.append(heading, dismiss);
|
||||||
|
const form = node("form", "opportunities-extension-action-form");
|
||||||
|
form.addEventListener("submit", event => event.preventDefault());
|
||||||
|
const field = node("label", "opportunities-extension-action-form-field");
|
||||||
|
const select = node("select");
|
||||||
|
select.id = "opportunities-extension-sr-primary-pillar";
|
||||||
|
select.name = "primaryPillar";
|
||||||
|
field.htmlFor = select.id;
|
||||||
|
field.append(node("span", "", "Primary pillar"), select);
|
||||||
|
const autocomplete = searchableSelect(select, field);
|
||||||
|
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");
|
||||||
|
cancel.type = "button";
|
||||||
|
cancel.addEventListener("click", () => close());
|
||||||
|
footer.append(retry, cancel);
|
||||||
|
form.append(field);
|
||||||
|
const extraControls = [];
|
||||||
|
const extraLoads = [];
|
||||||
|
function textField(name, label, type) {
|
||||||
|
const wrapper = node("label", "opportunities-extension-action-form-field" + (type === "textarea" ? " opportunities-extension-action-form-wide" : ""));
|
||||||
|
const input = node(type === "textarea" ? "textarea" : "input");
|
||||||
|
if (type === "textarea") input.rows = 4;
|
||||||
|
else input.type = type;
|
||||||
|
input.id = `opportunities-extension-sr-${name}`;
|
||||||
|
input.name = name;
|
||||||
|
wrapper.htmlFor = input.id;
|
||||||
|
wrapper.append(node("span", "", label), input);
|
||||||
|
form.append(wrapper);
|
||||||
|
extraControls.push(input);
|
||||||
|
}
|
||||||
|
textField("customerDeadline", "Customer deadline", "date");
|
||||||
|
function lookupField(name, label, request, radios = false) {
|
||||||
|
const wrapper = node(radios ? "fieldset" : "div", "opportunities-extension-sr-lookup" + (radios ? " opportunities-extension-action-form-wide" : ""));
|
||||||
|
const content = node("div", radios ? "opportunities-extension-sr-services" : "");
|
||||||
|
const message = node("p", "opportunities-extension-action-form-status");
|
||||||
|
const retryOption = node("button", "opportunities-extension-sr-lookup-retry", "Retry");
|
||||||
|
retryOption.type = "button";
|
||||||
|
retryOption.setAttribute("aria-label", `Retry ${label}`);
|
||||||
|
retryOption.hidden = true;
|
||||||
|
extraControls.push(retryOption);
|
||||||
|
let input, combo;
|
||||||
|
if (radios) wrapper.append(node("legend", "", label));
|
||||||
|
else {
|
||||||
|
const field = node("label", "opportunities-extension-action-form-field");
|
||||||
|
input = node("select");
|
||||||
|
input.id = `opportunities-extension-sr-${name}`;
|
||||||
|
input.name = name;
|
||||||
|
field.htmlFor = input.id;
|
||||||
|
field.append(node("span", "", label), input);
|
||||||
|
combo = searchableSelect(input, field);
|
||||||
|
content.append(field);
|
||||||
|
extraControls.push(combo.input);
|
||||||
|
}
|
||||||
|
wrapper.append(content, message, retryOption);
|
||||||
|
form.append(wrapper);
|
||||||
|
let loading = false;
|
||||||
|
async function loadOptions() {
|
||||||
|
if (loading || !current()) return;
|
||||||
|
loading = true;
|
||||||
|
retryOption.hidden = true;
|
||||||
|
message.hidden = false;
|
||||||
|
message.setAttribute("role", "status");
|
||||||
|
message.textContent = `Loading ${label.toLowerCase()}...`;
|
||||||
|
wrapper.setAttribute("aria-busy", "true");
|
||||||
|
if (input) { input.disabled = true; input.textContent = ""; combo.sync(); }
|
||||||
|
try {
|
||||||
|
const items = await request();
|
||||||
|
if (!current()) return;
|
||||||
|
if (input) {
|
||||||
|
const blank = node("option", "", items.length ? `Select ${label.toLowerCase()}` : "No options available");
|
||||||
|
blank.value = "";
|
||||||
|
input.append(blank);
|
||||||
|
items.forEach(item => { const option = node("option", "", item.name); option.value = item.code; input.append(option); });
|
||||||
|
input.value = "";
|
||||||
|
input.disabled = !items.length;
|
||||||
|
} else {
|
||||||
|
content.textContent = "";
|
||||||
|
const seen = new Set();
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
if (seen.has(String(item.Id))) return;
|
||||||
|
seen.add(String(item.Id));
|
||||||
|
const row = node("label", "opportunities-extension-sr-service-option");
|
||||||
|
const radio = node("input");
|
||||||
|
radio.type = "radio";
|
||||||
|
radio.name = "service";
|
||||||
|
radio.value = String(item.Id);
|
||||||
|
radio.id = `opportunities-extension-sr-service-${index}`;
|
||||||
|
const copy = node("span");
|
||||||
|
const abstract = node("span", "opportunities-extension-sr-service-abstract", item.Abstract_c || "");
|
||||||
|
abstract.id = `${radio.id}-description`;
|
||||||
|
radio.setAttribute("aria-describedby", abstract.id);
|
||||||
|
row.htmlFor = radio.id;
|
||||||
|
copy.append(node("span", "opportunities-extension-sr-service-name", item.RecordName), abstract);
|
||||||
|
row.append(radio, copy);
|
||||||
|
content.append(row);
|
||||||
|
extraControls.push(radio);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
message.textContent = items.length ? "" : "No options available.";
|
||||||
|
message.hidden = !!items.length;
|
||||||
|
} catch (error) {
|
||||||
|
if (!current()) return;
|
||||||
|
message.setAttribute("role", "alert");
|
||||||
|
message.textContent = error.message || `Unable to load ${label.toLowerCase()}.`;
|
||||||
|
retryOption.hidden = false;
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
if (current()) { combo?.sync(); wrapper.setAttribute("aria-busy", "false"); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
retryOption.addEventListener("click", loadOptions);
|
||||||
|
extraLoads.push(loadOptions);
|
||||||
|
}
|
||||||
|
lookupField("language", "Language of delivery", requestLanguage);
|
||||||
|
lookupField("platform", "Platform", requestPlatform);
|
||||||
|
textField("description", "Description", "textarea");
|
||||||
|
lookupField("services", "Services", requestServices, true);
|
||||||
|
form.append(status, footer);
|
||||||
|
dialog.append(header, form);
|
||||||
|
modal.append(dialog);
|
||||||
|
host.append(modal);
|
||||||
|
const view = { modal, trigger, loading: false };
|
||||||
|
active = view;
|
||||||
|
const current = () => active === view && allowsCrmActivities();
|
||||||
|
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;
|
||||||
|
retry.hidden = true;
|
||||||
|
select.disabled = true;
|
||||||
|
select.textContent = "";
|
||||||
|
option("", "Loading...");
|
||||||
|
autocomplete.sync();
|
||||||
|
form.setAttribute("data-state", "loading");
|
||||||
|
form.setAttribute("aria-busy", "true");
|
||||||
|
status.hidden = false;
|
||||||
|
status.setAttribute("role", "status");
|
||||||
|
status.textContent = "Loading primary pillars...";
|
||||||
|
try {
|
||||||
|
const items = await requestPillars();
|
||||||
|
if (!current()) return;
|
||||||
|
select.textContent = "";
|
||||||
|
option("", items.length ? "Select primary pillar" : "No options available");
|
||||||
|
items.forEach(item => option(item.code, item.name));
|
||||||
|
select.value = "";
|
||||||
|
select.disabled = !items.length;
|
||||||
|
form.setAttribute("data-state", "ready");
|
||||||
|
status.hidden = true;
|
||||||
|
} catch (error) {
|
||||||
|
if (!current()) return;
|
||||||
|
form.setAttribute("data-state", "error");
|
||||||
|
select.textContent = "";
|
||||||
|
option("", "Unavailable");
|
||||||
|
status.setAttribute("role", "alert");
|
||||||
|
status.textContent = error.message || "Unable to load primary pillars.";
|
||||||
|
retry.hidden = false;
|
||||||
|
} finally {
|
||||||
|
view.loading = false;
|
||||||
|
if (current()) { autocomplete.sync(); form.setAttribute("aria-busy", "false"); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
retry.addEventListener("click", load);
|
||||||
|
modal.addEventListener("click", event => { if (event.target === modal) close(); });
|
||||||
|
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 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dismiss.focus();
|
||||||
|
load();
|
||||||
|
extraLoads.forEach(loadOptions => loadOptions());
|
||||||
|
}
|
||||||
|
return { createButton, close, open };
|
||||||
|
}
|
||||||
60
src/features/opportunities/internal-sr-session.js
Normal file
60
src/features/opportunities/internal-sr-session.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
function createInternalSrSession({ query, environment, sessionUrl, sessionLabel = "Internal SR" }) {
|
||||||
|
const { document, setTimeout } = environment;
|
||||||
|
let recovery = null;
|
||||||
|
const wait = duration => new Promise(resolve => setTimeout(resolve, duration));
|
||||||
|
const now = () => environment.performance?.now() ?? Date.now();
|
||||||
|
function isUnauthorized(result) {
|
||||||
|
const status = result?.httpStatus ?? result?.debugRequests?.at(-1)?.httpStatus;
|
||||||
|
if (status != null) return Number(status) === 401;
|
||||||
|
// Older background workers may return only the request error text.
|
||||||
|
return /failed with status 401\.$/.test(result?.error || "");
|
||||||
|
}
|
||||||
|
async function warmSession(id) {
|
||||||
|
const frame = document.createElement("iframe");
|
||||||
|
frame.src = sessionUrl || "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/service/ec/container/sr/edit?srNumber=SR0001421040&srId=302963804324543&view=showisr";
|
||||||
|
frame.title = `${sessionLabel} session initialization`;
|
||||||
|
frame.tabIndex = -1;
|
||||||
|
frame.setAttribute("aria-hidden", "true");
|
||||||
|
frame.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms");
|
||||||
|
frame.style.cssText = "position:fixed;width:1px;height:1px;left:-10000px;top:0;border:0;opacity:0;pointer-events:none;";
|
||||||
|
let blocked = false;
|
||||||
|
let loaded = false;
|
||||||
|
frame.addEventListener("error", () => { blocked = true; });
|
||||||
|
frame.addEventListener("load", () => { loaded = true; });
|
||||||
|
try {
|
||||||
|
if (!document.body) throw new Error(`The current page cannot initialize the ${sessionLabel} session.`);
|
||||||
|
document.body.append(frame);
|
||||||
|
const deadline = now() + 60000;
|
||||||
|
for (let attempt = 0; now() < deadline; attempt += 1) {
|
||||||
|
// Check immediately, then briefly poll faster before backing off.
|
||||||
|
if (attempt > 0) await wait(Math.min(attempt <= 4 ? 250 : 1000, deadline - now()));
|
||||||
|
if (now() >= deadline) break;
|
||||||
|
if (blocked) throw new Error(`The ${sessionLabel} session iframe could not be loaded. Sign in manually and try again.`);
|
||||||
|
const result = await query(id);
|
||||||
|
if (result?.ok) return { id, result };
|
||||||
|
if (!isUnauthorized(result)) throw new Error(result?.error || `Unable to load ${sessionLabel} data.`);
|
||||||
|
}
|
||||||
|
throw new Error(`${sessionLabel} session timed out after 60 seconds: the API still returned HTTP 401. ${loaded ? "The iframe emitted a load event, but authentication was not confirmed." : "The iframe did not emit a load event."} Sign in manually and try again.`);
|
||||||
|
} finally {
|
||||||
|
// Stop remaining page resources without stopping the caller's page.
|
||||||
|
try { frame.contentWindow?.stop(); } catch { /* Redirects can make the frame cross-origin. */ }
|
||||||
|
frame.src = "about:blank";
|
||||||
|
frame.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function request(id) {
|
||||||
|
let result = await query(id);
|
||||||
|
for (let attempt = 0; !result?.ok && isUnauthorized(result) && attempt < 2; attempt += 1) {
|
||||||
|
if (!recovery) {
|
||||||
|
recovery = warmSession(id);
|
||||||
|
const pending = recovery;
|
||||||
|
pending.finally(() => { if (recovery === pending) recovery = null; }).catch(() => {});
|
||||||
|
}
|
||||||
|
const recovered = await recovery;
|
||||||
|
result = recovered.id === id ? recovered.result : await query(id);
|
||||||
|
}
|
||||||
|
if (!result?.ok) throw new Error(result?.error || `Unable to load ${sessionLabel} data.`);
|
||||||
|
return result.items;
|
||||||
|
}
|
||||||
|
return { request };
|
||||||
|
}
|
||||||
113
src/features/opportunities/searchable-select.js
Normal file
113
src/features/opportunities/searchable-select.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
function createSearchableSelect({ environment }) {
|
||||||
|
const { document } = environment;
|
||||||
|
function element(tag, className, text) {
|
||||||
|
const node = document.createElement(tag);
|
||||||
|
if (className) node.className = className;
|
||||||
|
if (text !== undefined) node.textContent = text;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
function searchableSelect(select, field, onChange) {
|
||||||
|
const box = element("div", "opportunities-extension-action-combobox");
|
||||||
|
const input = element("input");
|
||||||
|
input.type = "text";
|
||||||
|
input.autocomplete = "off";
|
||||||
|
input.required = !!select.required;
|
||||||
|
input.setAttribute("aria-required", String(!!select.required));
|
||||||
|
select.addEventListener("invalid", event => { event.preventDefault(); input.focus(); });
|
||||||
|
input.id = `${select.id}-search`;
|
||||||
|
field.htmlFor = input.id;
|
||||||
|
const list = element("div", "opportunities-extension-action-options");
|
||||||
|
list.id = `${select.id}-options`;
|
||||||
|
list.setAttribute("role", "listbox");
|
||||||
|
input.setAttribute("role", "combobox");
|
||||||
|
input.setAttribute("aria-autocomplete", "list");
|
||||||
|
input.setAttribute("aria-controls", list.id);
|
||||||
|
input.setAttribute("aria-expanded", "false");
|
||||||
|
select.hidden = true;
|
||||||
|
select.tabIndex = -1;
|
||||||
|
box.append(input, list);
|
||||||
|
field.append(box);
|
||||||
|
let matches = [], index = -1;
|
||||||
|
const options = () => Array.from(select.children).filter(item => item.value);
|
||||||
|
const normalize = value => String(value).normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
|
||||||
|
function closeList() {
|
||||||
|
list.hidden = true;
|
||||||
|
input.setAttribute("aria-expanded", "false");
|
||||||
|
input.setAttribute("aria-activedescendant", "");
|
||||||
|
}
|
||||||
|
function sync() {
|
||||||
|
closeList();
|
||||||
|
input.disabled = !!select.disabled;
|
||||||
|
input.placeholder = select.children[0]?.textContent || "Select";
|
||||||
|
input.value = options().find(item => item.value === select.value)?.textContent || "";
|
||||||
|
input.title = input.value;
|
||||||
|
}
|
||||||
|
function highlight() {
|
||||||
|
Array.from(list.children).forEach((node, i) => node.setAttribute("aria-selected", String(i === index)));
|
||||||
|
input.setAttribute("aria-activedescendant", matches[index] ? `${list.id}-${index}` : "");
|
||||||
|
list.children[index]?.scrollIntoView?.({ block: "nearest" });
|
||||||
|
}
|
||||||
|
function choose(i) {
|
||||||
|
if (!matches[i]) return;
|
||||||
|
const changed = select.value !== matches[i].value;
|
||||||
|
select.value = matches[i].value;
|
||||||
|
sync();
|
||||||
|
if (changed) onChange?.();
|
||||||
|
}
|
||||||
|
function positionList() {
|
||||||
|
const bounds = input.closest?.(".opportunities-extension-action-form-dialog")?.getBoundingClientRect();
|
||||||
|
const anchor = input.getBoundingClientRect?.();
|
||||||
|
if (!bounds || !anchor) return;
|
||||||
|
const below = Math.max(0, bounds.bottom - anchor.bottom - 8);
|
||||||
|
const above = Math.max(0, anchor.top - bounds.top - 8);
|
||||||
|
const opensAbove = below < 180 && above > below;
|
||||||
|
list.setAttribute("data-placement", opensAbove ? "above" : "below");
|
||||||
|
list.style.maxHeight = `${Math.min(180, opensAbove ? above : below)}px`;
|
||||||
|
}
|
||||||
|
function render(query = "") {
|
||||||
|
if (input.disabled) return;
|
||||||
|
matches = options().filter(item => normalize(`${item.textContent} ${item.value}`).includes(normalize(query)));
|
||||||
|
index = -1;
|
||||||
|
list.textContent = "";
|
||||||
|
matches.forEach((item, i) => {
|
||||||
|
const row = element("div", "opportunities-extension-action-option", item.textContent);
|
||||||
|
row.id = `${list.id}-${i}`;
|
||||||
|
row.setAttribute("role", "option");
|
||||||
|
row.setAttribute("aria-selected", "false");
|
||||||
|
row.addEventListener("mousedown", event => event.preventDefault());
|
||||||
|
row.addEventListener("click", event => { event.preventDefault(); choose(i); });
|
||||||
|
list.append(row);
|
||||||
|
});
|
||||||
|
if (!matches.length) {
|
||||||
|
const empty = element("div", "opportunities-extension-action-option", "No results");
|
||||||
|
empty.setAttribute("role", "status");
|
||||||
|
list.append(empty);
|
||||||
|
}
|
||||||
|
positionList();
|
||||||
|
list.hidden = false;
|
||||||
|
input.setAttribute("aria-expanded", "true");
|
||||||
|
input.setAttribute("aria-activedescendant", "");
|
||||||
|
}
|
||||||
|
input.addEventListener("click", () => { render(); input.select?.(); });
|
||||||
|
input.addEventListener("input", () => render(input.value));
|
||||||
|
input.addEventListener("blur", sync);
|
||||||
|
input.addEventListener("keydown", event => {
|
||||||
|
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
||||||
|
event.preventDefault();
|
||||||
|
if (list.hidden) render();
|
||||||
|
if (matches.length) index = (index + (event.key === "ArrowDown" ? 1 : index < 0 ? 0 : -1) + matches.length) % matches.length;
|
||||||
|
highlight();
|
||||||
|
} else if (event.key === "Enter" && !list.hidden) {
|
||||||
|
event.preventDefault();
|
||||||
|
choose(index < 0 && matches.length === 1 ? 0 : index);
|
||||||
|
} else if (event.key === "Escape" && !list.hidden) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
sync();
|
||||||
|
} else if (event.key === "Tab") sync();
|
||||||
|
});
|
||||||
|
sync();
|
||||||
|
return { input, sync };
|
||||||
|
}
|
||||||
|
return searchableSelect;
|
||||||
|
}
|
||||||
21
src/features/opportunities/sr-session-preparation.js
Normal file
21
src/features/opportunities/sr-session-preparation.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
function createSrSessionPreparation({ ensureCurrentUser, allowsCrmActivities, sendRequest, reportProgress, appendLog }) {
|
||||||
|
let pending = null;
|
||||||
|
function ensure() {
|
||||||
|
if (pending) return pending;
|
||||||
|
pending = (async () => {
|
||||||
|
try {
|
||||||
|
await ensureCurrentUser();
|
||||||
|
if (!allowsCrmActivities()) return;
|
||||||
|
reportProgress("running");
|
||||||
|
const result = await sendRequest();
|
||||||
|
appendLog(result?.debugRequests || []);
|
||||||
|
if (!result?.ok) throw new Error(result?.error || "Session initialization failed.");
|
||||||
|
reportProgress("complete");
|
||||||
|
} catch {
|
||||||
|
reportProgress("error");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
return { ensure };
|
||||||
|
}
|
||||||
@@ -309,6 +309,36 @@
|
|||||||
.opportunities-extension-action-form-field textarea { resize: vertical; min-height: 96px; }
|
.opportunities-extension-action-form-field textarea { resize: vertical; min-height: 96px; }
|
||||||
.opportunities-extension-action-form-field select { height: 38px; }
|
.opportunities-extension-action-form-field select { height: 38px; }
|
||||||
.opportunities-extension-action-form-field select[hidden] { display: none; }
|
.opportunities-extension-action-form-field select[hidden] { display: none; }
|
||||||
|
.opportunities-extension-sr-form-dialog { width: min(900px, 100%); }
|
||||||
|
.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; }
|
||||||
|
.opportunities-extension-sr-form-dialog textarea { min-height: 112px; }
|
||||||
|
.opportunities-extension-sr-form-dialog .opportunities-extension-sr-service-option { padding: 12px 0; gap: 10px; }
|
||||||
|
.opportunities-extension-sr-form-dialog .opportunities-extension-sr-service-abstract { font-size: 13px; line-height: 20px; }
|
||||||
|
@media (min-width: 1000px) {
|
||||||
|
.opportunities-extension-sr-form-dialog { width: min(1120px, 100%); }
|
||||||
|
.opportunities-extension-sr-form-dialog > .opportunities-extension-action-form { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px 24px; }
|
||||||
|
.opportunities-extension-sr-services { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 28px; align-items: start; }
|
||||||
|
.opportunities-extension-sr-services .opportunities-extension-sr-service-option { margin: 0; border-top: 1px solid #aaa6a040; }
|
||||||
|
.opportunities-extension-sr-services .opportunities-extension-sr-service-option:nth-child(-n+2) { border-top: 0; }
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.opportunities-extension-sr-form-dialog > .opportunities-extension-account-plans-header { padding: 16px; }
|
||||||
|
.opportunities-extension-sr-form-dialog > .opportunities-extension-action-form { grid-template-columns: minmax(0, 1fr); gap: 18px; padding: 16px; }
|
||||||
|
}
|
||||||
|
.opportunities-extension-sr-lookup { min-width: 0; padding: 0; margin: 0; border: 0; font-size: 13px; }
|
||||||
|
.opportunities-extension-sr-lookup legend { padding: 0 0 8px; font-weight: 600; }
|
||||||
|
.opportunities-extension-sr-lookup[aria-busy="true"] > p { opacity: .65; }
|
||||||
|
.opportunities-extension-sr-service-option { display: flex; align-items: flex-start; gap: 8px; padding: 9px 0; cursor: pointer; }
|
||||||
|
.opportunities-extension-sr-service-option + .opportunities-extension-sr-service-option { border-top: 1px solid #aaa6a040; }
|
||||||
|
.opportunities-extension-sr-service-option input { flex: 0 0 16px; width: 16px; height: 16px; margin: 2px 0 0; accent-color: #00758f; }
|
||||||
|
.opportunities-extension-sr-service-option > span { min-width: 0; overflow-wrap: anywhere; }
|
||||||
|
.opportunities-extension-sr-service-name { display: block; font-weight: 600; line-height: 20px; }
|
||||||
|
.opportunities-extension-sr-service-abstract { display: block; margin-top: 3px; color: #625d58; font-size: 12px; line-height: 18px; white-space: pre-wrap; }
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-sr-service-abstract { color: #c7c2bc; }
|
||||||
|
.opportunities-extension-sr-lookup-retry { color: inherit; background: transparent; border: 1px solid #8f8983; border-radius: 4px; padding: 5px 10px; cursor: pointer; }
|
||||||
|
.opportunities-extension-sr-lookup-retry[hidden] { display: none; }
|
||||||
.opportunities-extension-action-form [aria-invalid="true"] { border-color: #b3261e; outline: 1px solid #b3261e; }
|
.opportunities-extension-action-form [aria-invalid="true"] { border-color: #b3261e; outline: 1px solid #b3261e; }
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-action-form [aria-invalid="true"] { border-color: #ffaaa4; outline-color: #ffaaa4; }
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-action-form [aria-invalid="true"] { border-color: #ffaaa4; outline-color: #ffaaa4; }
|
||||||
.opportunities-extension-modal .opportunities-extension-copy-toast { z-index: 20; }
|
.opportunities-extension-modal .opportunities-extension-copy-toast { z-index: 20; }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
function createWorkloadActionForm({ MODAL_ID, allowsCrmActivities, isMissingAction, getOpportunity, getProducts,
|
function createWorkloadActionForm({ MODAL_ID, allowsCrmActivities, isMissingAction, getOpportunity, getProducts,
|
||||||
requestTeamMembers, requestLookup, getCurrentUserId, saveAction, onActionSaved, notify, formatDate, createCloseIcon, environment }) {
|
requestTeamMembers, requestLookup, getCurrentUserId, saveAction, onActionSaved, notify, formatDate, createCloseIcon, searchableSelect, environment }) {
|
||||||
const { document } = environment;
|
const { document } = environment;
|
||||||
let active = null;
|
let active = null;
|
||||||
function element(tag, className, text) {
|
function element(tag, className, text) {
|
||||||
@@ -8,109 +8,6 @@ function createWorkloadActionForm({ MODAL_ID, allowsCrmActivities, isMissingActi
|
|||||||
if (text !== undefined) node.textContent = text;
|
if (text !== undefined) node.textContent = text;
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
function searchableSelect(select, field, onChange) {
|
|
||||||
const box = element("div", "opportunities-extension-action-combobox");
|
|
||||||
const input = element("input");
|
|
||||||
input.type = "text";
|
|
||||||
input.autocomplete = "off";
|
|
||||||
input.required = !!select.required;
|
|
||||||
input.setAttribute("aria-required", String(!!select.required));
|
|
||||||
select.addEventListener("invalid", event => { event.preventDefault(); input.focus(); });
|
|
||||||
input.id = `${select.id}-search`;
|
|
||||||
field.htmlFor = input.id;
|
|
||||||
const list = element("div", "opportunities-extension-action-options");
|
|
||||||
list.id = `${select.id}-options`;
|
|
||||||
list.setAttribute("role", "listbox");
|
|
||||||
input.setAttribute("role", "combobox");
|
|
||||||
input.setAttribute("aria-autocomplete", "list");
|
|
||||||
input.setAttribute("aria-controls", list.id);
|
|
||||||
input.setAttribute("aria-expanded", "false");
|
|
||||||
select.hidden = true;
|
|
||||||
select.tabIndex = -1;
|
|
||||||
box.append(input, list);
|
|
||||||
field.append(box);
|
|
||||||
let matches = [], index = -1;
|
|
||||||
const options = () => Array.from(select.children).filter(item => item.value);
|
|
||||||
const normalize = value => String(value).normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
|
|
||||||
function closeList() {
|
|
||||||
list.hidden = true;
|
|
||||||
input.setAttribute("aria-expanded", "false");
|
|
||||||
input.setAttribute("aria-activedescendant", "");
|
|
||||||
}
|
|
||||||
function sync() {
|
|
||||||
closeList();
|
|
||||||
input.disabled = !!select.disabled;
|
|
||||||
input.placeholder = select.children[0]?.textContent || "Select";
|
|
||||||
input.value = options().find(item => item.value === select.value)?.textContent || "";
|
|
||||||
input.title = input.value;
|
|
||||||
}
|
|
||||||
function highlight() {
|
|
||||||
Array.from(list.children).forEach((node, i) => node.setAttribute("aria-selected", String(i === index)));
|
|
||||||
input.setAttribute("aria-activedescendant", matches[index] ? `${list.id}-${index}` : "");
|
|
||||||
list.children[index]?.scrollIntoView?.({ block: "nearest" });
|
|
||||||
}
|
|
||||||
function choose(i) {
|
|
||||||
if (!matches[i]) return;
|
|
||||||
const changed = select.value !== matches[i].value;
|
|
||||||
select.value = matches[i].value;
|
|
||||||
sync();
|
|
||||||
if (changed) onChange?.();
|
|
||||||
}
|
|
||||||
function positionList() {
|
|
||||||
const bounds = input.closest?.(".opportunities-extension-action-form-dialog")?.getBoundingClientRect();
|
|
||||||
const anchor = input.getBoundingClientRect?.();
|
|
||||||
if (!bounds || !anchor) return;
|
|
||||||
const below = Math.max(0, bounds.bottom - anchor.bottom - 8);
|
|
||||||
const above = Math.max(0, anchor.top - bounds.top - 8);
|
|
||||||
const opensAbove = below < 180 && above > below;
|
|
||||||
list.setAttribute("data-placement", opensAbove ? "above" : "below");
|
|
||||||
list.style.maxHeight = `${Math.min(180, opensAbove ? above : below)}px`;
|
|
||||||
}
|
|
||||||
function render(query = "") {
|
|
||||||
if (input.disabled) return;
|
|
||||||
matches = options().filter(item => normalize(`${item.textContent} ${item.value}`).includes(normalize(query)));
|
|
||||||
index = -1;
|
|
||||||
list.textContent = "";
|
|
||||||
matches.forEach((item, i) => {
|
|
||||||
const row = element("div", "opportunities-extension-action-option", item.textContent);
|
|
||||||
row.id = `${list.id}-${i}`;
|
|
||||||
row.setAttribute("role", "option");
|
|
||||||
row.setAttribute("aria-selected", "false");
|
|
||||||
row.addEventListener("mousedown", event => event.preventDefault());
|
|
||||||
row.addEventListener("click", event => { event.preventDefault(); choose(i); });
|
|
||||||
list.append(row);
|
|
||||||
});
|
|
||||||
if (!matches.length) {
|
|
||||||
const empty = element("div", "opportunities-extension-action-option", "No results");
|
|
||||||
empty.setAttribute("role", "status");
|
|
||||||
list.append(empty);
|
|
||||||
}
|
|
||||||
positionList();
|
|
||||||
list.hidden = false;
|
|
||||||
input.setAttribute("aria-expanded", "true");
|
|
||||||
input.setAttribute("aria-activedescendant", "");
|
|
||||||
}
|
|
||||||
input.addEventListener("click", () => { render(); input.select?.(); });
|
|
||||||
input.addEventListener("input", () => render(input.value));
|
|
||||||
input.addEventListener("blur", sync);
|
|
||||||
input.addEventListener("keydown", event => {
|
|
||||||
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
||||||
event.preventDefault();
|
|
||||||
if (list.hidden) render();
|
|
||||||
if (matches.length) index = (index + (event.key === "ArrowDown" ? 1 : index < 0 ? 0 : -1) + matches.length) % matches.length;
|
|
||||||
highlight();
|
|
||||||
} else if (event.key === "Enter" && !list.hidden) {
|
|
||||||
event.preventDefault();
|
|
||||||
choose(index < 0 && matches.length === 1 ? 0 : index);
|
|
||||||
} else if (event.key === "Escape" && !list.hidden) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
sync();
|
|
||||||
} else if (event.key === "Tab") sync();
|
|
||||||
});
|
|
||||||
sync();
|
|
||||||
return { input, sync };
|
|
||||||
}
|
|
||||||
function close(restoreFocus = true) {
|
function close(restoreFocus = true) {
|
||||||
if (!active) return false;
|
if (!active) return false;
|
||||||
if (active.saving) return true;
|
if (active.saving) return true;
|
||||||
|
|||||||
@@ -1,4 +1,146 @@
|
|||||||
function createFusionSrDetails({ fetchJson }) {
|
function createFusionSrDetails({ fetchJson, tabsCreate, tabsRemove, supportsFirefoxCookieStoreTabs, cookiesGetAll, cookieChanges, wait }) {
|
||||||
|
const sessions = new Map();
|
||||||
|
function cookiePart(cookie) {
|
||||||
|
if (!cookie) return null;
|
||||||
|
const match = /^ORA_OCIS_CG_SESSION_idcs-([a-zA-Z0-9-]+)_comcipapic-oalprod\.integration\.ocp\.oraclecloud\.com_([12])$/.exec(cookie.name);
|
||||||
|
const domain = "comcipapic-oalprod.integration.ocp.oraclecloud.com";
|
||||||
|
const cookieDomain = cookie.domain?.replace(/^\./, "");
|
||||||
|
if (!match || (cookieDomain && cookieDomain !== domain && !domain.endsWith(`.${cookieDomain}`))) return null;
|
||||||
|
return { key: `${match[1]}|${cookie.path || "/"}|${cookie.storeId || ""}`, part: match[2],
|
||||||
|
valid: Boolean(cookie.value) && (cookie.expirationDate == null || cookie.expirationDate > Date.now() / 1000) };
|
||||||
|
}
|
||||||
|
function watchSession(session, sender) {
|
||||||
|
const pairs = new Map();
|
||||||
|
let available;
|
||||||
|
session.cookieReady = new Promise(resolve => { available = resolve; });
|
||||||
|
const listener = change => {
|
||||||
|
const cookie = change.cookie;
|
||||||
|
const part = cookiePart(cookie);
|
||||||
|
if (!part) return;
|
||||||
|
if (sender?.tab?.cookieStoreId && cookie.storeId !== sender.tab.cookieStoreId) return;
|
||||||
|
if (!pairs.has(part.key)) pairs.set(part.key, new Map());
|
||||||
|
const pair = pairs.get(part.key);
|
||||||
|
if (change.removed || !part.valid) pair.delete(part.part);
|
||||||
|
else pair.set(part.part, cookie);
|
||||||
|
if (pair.size === 2 && [...pair.values()].every(item => cookiePart(item)?.valid)) available(new Set([part.key]));
|
||||||
|
session.wake?.();
|
||||||
|
};
|
||||||
|
cookieChanges?.addListener(listener);
|
||||||
|
session.unwatch = () => cookieChanges?.removeListener(listener);
|
||||||
|
}
|
||||||
|
function waitForSessionCookies(session, sender) {
|
||||||
|
if (session.cookieWait) return session.cookieWait;
|
||||||
|
session.cookieWait = (async () => {
|
||||||
|
const deadline = Date.now() + 1000;
|
||||||
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||||
|
// Install the wake-up before reading so a renewal during getAll is not missed.
|
||||||
|
const changed = new Promise(resolve => { session.wake = resolve; });
|
||||||
|
const current = await sessionCookies(sender);
|
||||||
|
const renewed = [...current].some(pair => !session.baseline.has(pair));
|
||||||
|
if (renewed || Date.now() >= deadline) {
|
||||||
|
session.baseline = current;
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
await Promise.race([changed, wait(250)]);
|
||||||
|
}
|
||||||
|
return sessionCookies(sender);
|
||||||
|
})().finally(() => { session.cookieWait = null; session.wake = null; });
|
||||||
|
return session.cookieWait;
|
||||||
|
}
|
||||||
|
async function sessionCookies(sender) {
|
||||||
|
const domain = "comcipapic-oalprod.integration.ocp.oraclecloud.com";
|
||||||
|
const details = { domain };
|
||||||
|
if (sender?.tab?.cookieStoreId) details.storeId = sender.tab.cookieStoreId;
|
||||||
|
try {
|
||||||
|
const groups = new Map();
|
||||||
|
for (const cookie of await cookiesGetAll(details)) {
|
||||||
|
const part = cookiePart(cookie);
|
||||||
|
if (!part?.valid) continue;
|
||||||
|
if (!groups.has(part.key)) groups.set(part.key, {});
|
||||||
|
groups.get(part.key)[part.part] = [cookie.name, cookie.value, cookie.expirationDate ?? null];
|
||||||
|
}
|
||||||
|
// Fingerprints remain in memory only; the browser sends the actual Cookie header.
|
||||||
|
return new Set([...groups.values()].filter(pair => pair[1] && pair[2]).map(pair => JSON.stringify([pair[1], pair[2]])));
|
||||||
|
} catch { return new Set(); }
|
||||||
|
}
|
||||||
|
async function requestLookupWithSession(operation, message, sender, initialize = false) {
|
||||||
|
const existingCookies = initialize ? await sessionCookies(sender) : null;
|
||||||
|
if (existingCookies?.size) return { ok: true, debugRequests: [] };
|
||||||
|
const logs = [];
|
||||||
|
const query = async () => {
|
||||||
|
try {
|
||||||
|
const result = await operation(message);
|
||||||
|
logs.push(...(result.debugRequests || []));
|
||||||
|
return { ...result, debugRequests: logs };
|
||||||
|
} catch (error) {
|
||||||
|
logs.push(...(error.debugRequests || []));
|
||||||
|
error.debugRequests = logs;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (!initialize) {
|
||||||
|
try { return await query(); } catch (error) { if (error.httpStatus !== 401) throw error; }
|
||||||
|
}
|
||||||
|
const key = sender?.tab?.cookieStoreId || "default";
|
||||||
|
let session = sessions.get(key);
|
||||||
|
if (!session) {
|
||||||
|
const details = {
|
||||||
|
url: "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_initiation/live/webApps/external/",
|
||||||
|
active: false
|
||||||
|
};
|
||||||
|
if (Number.isInteger(sender?.tab?.windowId)) details.windowId = sender.tab.windowId;
|
||||||
|
if (supportsFirefoxCookieStoreTabs() && sender?.tab?.cookieStoreId) details.cookieStoreId = sender.tab.cookieStoreId;
|
||||||
|
session = { users: 0, closed: null, baseline: null };
|
||||||
|
watchSession(session, sender);
|
||||||
|
session.tab = (async () => {
|
||||||
|
session.baseline = existingCookies || await sessionCookies(sender);
|
||||||
|
return tabsCreate(details);
|
||||||
|
})();
|
||||||
|
sessions.set(key, session);
|
||||||
|
}
|
||||||
|
session.users += 1;
|
||||||
|
let tab;
|
||||||
|
try {
|
||||||
|
tab = await session.tab;
|
||||||
|
const deadline = Date.now() + 60000;
|
||||||
|
for (let attempt = 0; attempt < 240 && Date.now() < deadline; attempt += 1) {
|
||||||
|
const pendingCookies = initialize && attempt === 0
|
||||||
|
? sessionCookies(sender) : waitForSessionCookies(session, sender);
|
||||||
|
const cookies = await (initialize ? Promise.race([session.cookieReady, pendingCookies]) : pendingCookies);
|
||||||
|
if (initialize) {
|
||||||
|
// Proactive preparation observes cookies only; API requests belong to the form.
|
||||||
|
if (!cookies.size) continue;
|
||||||
|
await closeSessionTab();
|
||||||
|
return { ok: true, debugRequests: [] };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await query();
|
||||||
|
// Any successful lookup confirms the shared session; stop the page immediately.
|
||||||
|
await closeSessionTab();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.httpStatus !== 401) throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("Service Request session could not be established after 60 seconds. Sign in to Oracle and retry.");
|
||||||
|
} catch (error) {
|
||||||
|
error.debugRequests = logs;
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
session.users -= 1;
|
||||||
|
if (session.users === 0) {
|
||||||
|
sessions.delete(key);
|
||||||
|
session.unwatch();
|
||||||
|
await closeSessionTab();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function closeSessionTab() {
|
||||||
|
if (!session.closed && Number.isInteger(tab?.id)) {
|
||||||
|
session.closed = tabsRemove(tab.id).catch(() => {});
|
||||||
|
}
|
||||||
|
return session.closed;
|
||||||
|
}
|
||||||
|
}
|
||||||
async function requestInternalServiceRequest(message) {
|
async function requestInternalServiceRequest(message) {
|
||||||
const debugRequests = [];
|
const debugRequests = [];
|
||||||
try {
|
try {
|
||||||
@@ -7,10 +149,14 @@ function createFusionSrDetails({ fetchJson }) {
|
|||||||
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 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"
|
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}`)}`;
|
+ `?fields=${encodeURIComponent(fields)}&onlyData=true&q=${encodeURIComponent(`SrId=${id}`)}`;
|
||||||
const result = await fetchJson(url, { method: "GET", credentials: "include",
|
const query = async () => {
|
||||||
|
const result = await fetchJson(url, { method: "GET", credentials: "include",
|
||||||
headers: { Accept: "application/json", Authorization: "Session" }
|
headers: { Accept: "application/json", Authorization: "Session" }
|
||||||
}, "requestInternalServiceRequest", debugRequests);
|
}, "requestInternalServiceRequest", debugRequests);
|
||||||
if (!Array.isArray(result?.items)) throw new Error("Invalid SR details response.");
|
if (!Array.isArray(result?.items)) throw new Error("Invalid SR details response.");
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
const result = await query();
|
||||||
const items = result.items.flatMap(item => {
|
const items = result.items.flatMap(item => {
|
||||||
const children = item.ServiceRequest_SRToInternalSR_Tgt;
|
const children = item.ServiceRequest_SRToInternalSR_Tgt;
|
||||||
if (children == null) return [];
|
if (children == null) return [];
|
||||||
@@ -23,5 +169,34 @@ function createFusionSrDetails({ fetchJson }) {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { requestInternalServiceRequest };
|
async function requestLookup(lookupType, requestName) {
|
||||||
|
const debugRequests = [];
|
||||||
|
try {
|
||||||
|
const url = `https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_initiation/live;profile=PROD/services/auth/1.1/proxy/oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/lookups?lookupType=${lookupType}&type=STANDARD`;
|
||||||
|
const result = await fetchJson(url, { method: "GET", credentials: "include",
|
||||||
|
headers: { Accept: "application/json", Authorization: "Session" }
|
||||||
|
}, requestName, debugRequests);
|
||||||
|
if (!Array.isArray(result?.items) || result.items.some(item => typeof item?.code !== "string" || typeof item?.name !== "string")) throw new Error(`Invalid ${requestName} response.`);
|
||||||
|
const items = result.items.filter(item => item.enabledFlag !== "N")
|
||||||
|
.sort((a, b) => (Number(a.displaySequence) || 0) - (Number(b.displaySequence) || 0))
|
||||||
|
.map(({ code, name }) => ({ code, name }));
|
||||||
|
return { ok: true, items, debugRequests };
|
||||||
|
} catch (error) { error.debugRequests = debugRequests; throw error; }
|
||||||
|
}
|
||||||
|
async function requestSrLookupOpportunityBusiness(message) {
|
||||||
|
const debugRequests = [];
|
||||||
|
try {
|
||||||
|
if (typeof message.emailAddress !== "string" || !message.emailAddress.trim()) throw new Error("Current user email is unavailable.");
|
||||||
|
const url = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_initiation/live;profile=PROD/services/auth/1.1/proxy/oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/services"
|
||||||
|
+ `?emailAddress=${encodeURIComponent(message.emailAddress.trim())}&salesObject=OPPORTUNITY&serviceType=BUSINESS`;
|
||||||
|
const result = await fetchJson(url, { method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" } }, "requestSrLookupOpportunityBusiness", debugRequests);
|
||||||
|
if (!Array.isArray(result?.items) || result.items.some(item => !/^\d+$/.test(String(item?.Id ?? "")) || typeof item?.RecordName !== "string")) throw new Error("Invalid Services response.");
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
return { requestLookupWithSession, requestInternalServiceRequest, requestSrLookupOpportunityBusiness,
|
||||||
|
prepareSession: sender => requestLookupWithSession(null, {}, sender, true),
|
||||||
|
requestSrLookupPrimaryPillar: () => requestLookup("SENGG_PRIMARY_PILLAR", "requestSrLookupPrimaryPillar"),
|
||||||
|
requestSrLookupLanguage: () => requestLookup("SENGG_LANGUAGE", "requestSrLookupLanguage"),
|
||||||
|
requestSrLookupTechPlatform: () => requestLookup("SENGG_TECH_PLATFORM", "requestSrLookupTechPlatform") };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ function createMessageTypes() {
|
|||||||
requestActionFindLookup: "opportunitiesExtension.requestActionFindLookup",
|
requestActionFindLookup: "opportunitiesExtension.requestActionFindLookup",
|
||||||
requestMgmtAction: "opportunitiesExtension.requestMgmtAction",
|
requestMgmtAction: "opportunitiesExtension.requestMgmtAction",
|
||||||
requestInternalServiceRequest: "opportunitiesExtension.requestInternalServiceRequest",
|
requestInternalServiceRequest: "opportunitiesExtension.requestInternalServiceRequest",
|
||||||
|
requestSrLookupPrimaryPillar: "opportunitiesExtension.requestSrLookupPrimaryPillar",
|
||||||
|
prepareSrSession: "opportunitiesExtension.prepareSrSession",
|
||||||
|
requestSrLookupLanguage: "opportunitiesExtension.requestSrLookupLanguage",
|
||||||
|
requestSrLookupTechPlatform: "opportunitiesExtension.requestSrLookupTechPlatform",
|
||||||
|
requestSrLookupOpportunityBusiness: "opportunitiesExtension.requestSrLookupOpportunityBusiness",
|
||||||
requestCurrentUser: "opportunitiesExtension.requestCurrentUser",
|
requestCurrentUser: "opportunitiesExtension.requestCurrentUser",
|
||||||
invalidateProductsCache: "opportunitiesExtension.invalidateProductsCache",
|
invalidateProductsCache: "opportunitiesExtension.invalidateProductsCache",
|
||||||
requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend",
|
requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend",
|
||||||
|
|||||||
@@ -30,6 +30,13 @@ test("activity indicators share Products styles, fade after 5s, retain failures
|
|||||||
assert.equal(timers.size, 0);
|
assert.equal(timers.size, 0);
|
||||||
update("sr", "error");
|
update("sr", "error");
|
||||||
assert.equal(nodes[0].textContent, "Service Requests unavailable");
|
assert.equal(nodes[0].textContent, "Service Requests unavailable");
|
||||||
|
update("session", "running");
|
||||||
|
assert.equal(nodes[2].textContent, "SR session loading");
|
||||||
|
assert.equal(nodes[2].attributes["aria-busy"], "true");
|
||||||
|
update("session", "complete");
|
||||||
|
assert.equal(nodes[2].textContent, "SR session ready");
|
||||||
|
assert.equal(nodes[2].attributes["aria-busy"], "false");
|
||||||
|
assert.equal([...timers.values()].at(-1).delay, 5000);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("CRM reports real page counts and Actions reports queued and completed requests", async () => {
|
test("CRM reports real page counts and Actions reports queued and completed requests", async () => {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ test("consistent inline vectors replace raster icons without parentheses or lost
|
|||||||
checkIcon(sr, "sr", "4h");
|
checkIcon(sr, "sr", "4h");
|
||||||
assert.equal(sr.title, "Whether a Service Request has been created for the opportunities.");
|
assert.equal(sr.title, "Whether a Service Request has been created for the opportunities.");
|
||||||
assert.equal(typeof sr.listeners.click, "function");
|
assert.equal(typeof sr.listeners.click, "function");
|
||||||
checkIcon(app.opportunityCrmServiceRequests.createBadge("OTHER"), "sr", "0h");
|
checkIcon(app.opportunityCrmServiceRequests.createBadge("OTHER"), "sr", "0h+");
|
||||||
|
|
||||||
app.productsContentState.opptyProductsByNumber.set("TEST", products);
|
app.productsContentState.opptyProductsByNumber.set("TEST", products);
|
||||||
app.authContentSession.requestConsumerTokenServiceOnce = async () => ({ workloadActions: { value: "synthetic" } });
|
app.authContentSession.requestConsumerTokenServiceOnce = async () => ({ workloadActions: { value: "synthetic" } });
|
||||||
|
|||||||
@@ -2,6 +2,265 @@ const assert = require("node:assert/strict");
|
|||||||
const { test } = require("node:test");
|
const { test } = require("node:test");
|
||||||
const { loadBackground, response } = require("./support/characterization.cjs");
|
const { loadBackground, response } = require("./support/characterization.cjs");
|
||||||
const type = "opportunitiesExtension.requestInternalServiceRequest";
|
const type = "opportunitiesExtension.requestInternalServiceRequest";
|
||||||
|
|
||||||
|
test("preparation reuses an existing unexpired cookie pair without navigation or API calls", async () => {
|
||||||
|
const domain = "comcipapic-oalprod.integration.ocp.oraclecloud.com";
|
||||||
|
const h = loadBackground({
|
||||||
|
fetch: async () => assert.fail("No API calls during preparation"),
|
||||||
|
cookies: () => [1, 2].map(part => ({ name: `ORA_OCIS_CG_SESSION_idcs-existing_${domain}_${part}`, value: `secret-${part}`, domain }))
|
||||||
|
});
|
||||||
|
const result = await h.dispatch({ type: "opportunitiesExtension.prepareSrSession" });
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(h.tabs.length, 0);
|
||||||
|
assert.equal(h.removedTabs.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("preparation closes immediately on the second cookie event even while getAll is pending", { timeout: 2000 }, async () => {
|
||||||
|
const fs = require("node:fs"), vm = require("node:vm"), path = require("node:path");
|
||||||
|
const factory = vm.runInNewContext(`${fs.readFileSync(path.join(__dirname, "../src/services/fusion/sr-details.js"), "utf8")}\ncreateFusionSrDetails`);
|
||||||
|
const listeners = new Set();
|
||||||
|
let reads = 0, closed = 0;
|
||||||
|
const domain = "comcipapic-oalprod.integration.ocp.oraclecloud.com";
|
||||||
|
const cookie = part => ({ name: `ORA_OCIS_CG_SESSION_idcs-new_${domain}_${part}`, value: `secret-${part}`, domain, storeId: "store" });
|
||||||
|
const emit = change => { for (const listener of listeners) listener(change); };
|
||||||
|
const api = factory({
|
||||||
|
fetchJson: async () => assert.fail("No API calls during preparation"),
|
||||||
|
cookieChanges: { addListener: fn => listeners.add(fn), removeListener: fn => listeners.delete(fn) },
|
||||||
|
cookiesGetAll: () => ++reads === 1 ? Promise.resolve([]) : new Promise(() => {}),
|
||||||
|
tabsCreate: async () => {
|
||||||
|
setImmediate(() => {
|
||||||
|
emit({ cookie: cookie(1) });
|
||||||
|
emit({ cookie: { ...cookie(2), storeId: "other" } });
|
||||||
|
assert.equal(closed, 0);
|
||||||
|
emit({ cookie: cookie(2), removed: true });
|
||||||
|
assert.equal(closed, 0);
|
||||||
|
emit({ cookie: cookie(2) });
|
||||||
|
});
|
||||||
|
return { id: 7 };
|
||||||
|
},
|
||||||
|
tabsRemove: async id => { assert.equal(id, 7); closed += 1; },
|
||||||
|
supportsFirefoxCookieStoreTabs: () => false,
|
||||||
|
wait: () => new Promise(() => {})
|
||||||
|
});
|
||||||
|
assert.equal((await api.prepareSession({ tab: { cookieStoreId: "store" } })).ok, true);
|
||||||
|
assert.equal(closed, 1);
|
||||||
|
assert.equal(listeners.size, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("explicit preparation opens SeaaS initiation and observes cookies without any API requests", async () => {
|
||||||
|
let calls = 0;
|
||||||
|
let reads = 0;
|
||||||
|
const domain = "comcipapic-oalprod.integration.ocp.oraclecloud.com";
|
||||||
|
const pair = [1, 2].map(part => ({ name: `ORA_OCIS_CG_SESSION_idcs-new_${domain}_${part}`, value: `secret-${part}`, domain }));
|
||||||
|
const h = loadBackground({ fetch: async () => {
|
||||||
|
calls += 1;
|
||||||
|
assert.fail("Opening the main modal must not trigger any session-dependent API request");
|
||||||
|
}, cookies: () => {
|
||||||
|
reads += 1;
|
||||||
|
if (reads === 1) return [];
|
||||||
|
assert.equal(h.tabs.length, 1);
|
||||||
|
if (reads === 2) return pair.slice(0, 1);
|
||||||
|
if (reads === 3) return pair.map(cookie => ({ ...cookie, expirationDate: 1 }));
|
||||||
|
return pair;
|
||||||
|
} });
|
||||||
|
const result = await h.dispatch({ type: "opportunitiesExtension.prepareSrSession" });
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(calls, 0);
|
||||||
|
assert.equal(reads, 4);
|
||||||
|
assert.deepEqual(result.debugRequests, []);
|
||||||
|
assert.ok(!JSON.stringify(result).includes("secret"));
|
||||||
|
assert.equal(h.tabs[0].active, false);
|
||||||
|
assert.equal(h.tabs[0].url, "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_initiation/live/webApps/external/");
|
||||||
|
assert.deepEqual(h.removedTabs, [91]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("proactive preparation times out without issuing lookup probes", async () => {
|
||||||
|
let calls = 0;
|
||||||
|
const h = loadBackground({ messageTimeout: 10000, fetch: async () => { calls += 1; assert.fail("No probes allowed"); } });
|
||||||
|
const result = await h.dispatch({ type: "opportunitiesExtension.prepareSrSession" });
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.equal(calls, 0);
|
||||||
|
assert.match(result.error, /session could not be established/);
|
||||||
|
assert.deepEqual(h.removedTabs, [91]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("cookie change wakes recovery without waiting for a polling timer and removes its listener", { timeout: 2000 }, async () => {
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const vm = require("node:vm");
|
||||||
|
const path = require("node:path");
|
||||||
|
const factory = vm.runInNewContext(`${fs.readFileSync(path.join(__dirname, "../src/services/fusion/sr-details.js"), "utf8")}\ncreateFusionSrDetails`);
|
||||||
|
const listeners = new Set();
|
||||||
|
let ready = false;
|
||||||
|
let calls = 0;
|
||||||
|
let closed = 0;
|
||||||
|
const domain = "comcipapic-oalprod.integration.ocp.oraclecloud.com";
|
||||||
|
const cookies = [1, 2].map(part => ({ name: `ORA_OCIS_CG_SESSION_idcs-new_${domain}_${part}`, value: `secret-${part}`, domain, storeId: "store" }));
|
||||||
|
const api = factory({
|
||||||
|
cookieChanges: { addListener: fn => listeners.add(fn), removeListener: fn => listeners.delete(fn) },
|
||||||
|
cookiesGetAll: async () => ready ? cookies : [],
|
||||||
|
tabsCreate: async () => {
|
||||||
|
setImmediate(() => {
|
||||||
|
ready = true;
|
||||||
|
for (const listener of listeners) listener({ cookie: cookies[1] });
|
||||||
|
});
|
||||||
|
return { id: 7 };
|
||||||
|
},
|
||||||
|
tabsRemove: async () => { closed += 1; },
|
||||||
|
supportsFirefoxCookieStoreTabs: () => false,
|
||||||
|
wait: () => new Promise(() => {})
|
||||||
|
});
|
||||||
|
const result = await api.requestLookupWithSession(async () => {
|
||||||
|
calls += 1;
|
||||||
|
if (!ready) { const error = new Error("Unauthorized"); error.httpStatus = 401; throw error; }
|
||||||
|
return { ok: true, items: [] };
|
||||||
|
}, {}, { tab: { cookieStoreId: "store" } });
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(calls, 2);
|
||||||
|
assert.equal(closed, 1);
|
||||||
|
assert.equal(listeners.size, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renewed matching cookie fragments trigger an early retry without leaking session values", async () => {
|
||||||
|
let reads = 0;
|
||||||
|
let calls = 0;
|
||||||
|
const domain = "comcipapic-oalprod.integration.ocp.oraclecloud.com";
|
||||||
|
const cookie = (id, part, value) => ({ name: `ORA_OCIS_CG_SESSION_idcs-${id}_${domain}_${part}`, value, domain, path: "/", session: true });
|
||||||
|
const h = loadBackground({
|
||||||
|
cookies: () => {
|
||||||
|
reads += 1;
|
||||||
|
if (reads < 3) return [cookie("old", 1, "old-secret-1"), cookie("old", 2, "old-secret-2")];
|
||||||
|
if (reads === 3) return [cookie("new", 1, "new-secret-1"), cookie("other", 2, "wrong-pair")];
|
||||||
|
return [cookie("new", 1, "new-secret-1"), cookie("new", 2, "new-secret-2")];
|
||||||
|
},
|
||||||
|
fetch: async (_, options) => {
|
||||||
|
calls += 1;
|
||||||
|
assert.equal(options.credentials, "include");
|
||||||
|
assert.equal(options.headers.Cookie, undefined);
|
||||||
|
if (calls === 1) return response({}, 401);
|
||||||
|
assert.equal(reads, 4, "retry immediately when both renewed parts match, not on a partial pair");
|
||||||
|
return response({ items: [] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const result = await h.dispatch({ type: "opportunitiesExtension.requestSrLookupLanguage" });
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(calls, 2);
|
||||||
|
assert.ok(!JSON.stringify(result).includes("secret"));
|
||||||
|
assert.ok(h.cookieQueries.every(query => query.domain === domain && query.storeId === "test-store"));
|
||||||
|
assert.deepEqual(h.removedTabs, [91]);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const family of ["chrome", "firefox"]) {
|
||||||
|
test(`${family}: simultaneous lookup 401s share an inactive session tab and close it after successful retries`, async () => {
|
||||||
|
const counts = new Map();
|
||||||
|
const h = loadBackground({ family, fetch: async url => {
|
||||||
|
const count = (counts.get(url) || 0) + 1;
|
||||||
|
counts.set(url, count);
|
||||||
|
return count < 3 ? response({}, 401) : response({ items: [] });
|
||||||
|
} });
|
||||||
|
const results = await Promise.all(["PrimaryPillar", "Language", "TechPlatform", "OpportunityBusiness"].map(name =>
|
||||||
|
h.dispatch({ type: `opportunitiesExtension.requestSrLookup${name}`, emailAddress: "test@example.invalid" })));
|
||||||
|
assert.ok(results.every(result => result.ok));
|
||||||
|
assert.ok(results.every(result => result.debugRequests.length === 3));
|
||||||
|
assert.equal(h.tabs.length, 1);
|
||||||
|
assert.equal(h.tabs[0].active, false);
|
||||||
|
assert.equal(h.tabs[0].windowId, 3);
|
||||||
|
assert.equal(h.tabs[0].url, "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_initiation/live/webApps/external/");
|
||||||
|
assert.equal(h.tabs[0].cookieStoreId, family === "firefox" ? "test-store" : undefined);
|
||||||
|
assert.deepEqual(h.removedTabs, [91]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("lookup authentication timeout closes the temporary tab and returns a clear error", async () => {
|
||||||
|
const h = loadBackground({ messageTimeout: 10000, fetch: async () => response({}, 401) });
|
||||||
|
const result = await h.dispatch({ type: "opportunitiesExtension.requestSrLookupLanguage" });
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.match(result.error, /session could not be established/);
|
||||||
|
assert.equal(h.tabs.length, 1);
|
||||||
|
assert.deepEqual(h.removedTabs, [91]);
|
||||||
|
});
|
||||||
|
test("SR language, platform and services use fixed Session endpoints and dynamic email", async () => {
|
||||||
|
const calls = [];
|
||||||
|
const h = loadBackground({ fetch: async (url, options) => {
|
||||||
|
calls.push({ url, options });
|
||||||
|
return response({ items: url.includes("/services?")
|
||||||
|
? [{ Id: 123, RecordName: "Consumption", Abstract_c: "Service summary" }]
|
||||||
|
: [{ code: "CODE", name: "Label", enabledFlag: "Y", displaySequence: 1 }] });
|
||||||
|
} });
|
||||||
|
for (const [name, lookup] of [["requestSrLookupLanguage", "SENGG_LANGUAGE"], ["requestSrLookupTechPlatform", "SENGG_TECH_PLATFORM"]]) {
|
||||||
|
const result = await h.dispatch({ type: `opportunitiesExtension.${name}` });
|
||||||
|
assert.deepEqual(result.items, [{ code: "CODE", name: "Label" }]);
|
||||||
|
assert.equal(new URL(calls.at(-1).url).searchParams.get("lookupType"), lookup);
|
||||||
|
assert.equal(result.debugRequests[0].name, name);
|
||||||
|
}
|
||||||
|
const result = await h.dispatch({ type: "opportunitiesExtension.requestSrLookupOpportunityBusiness", emailAddress: "user+test@example.invalid" });
|
||||||
|
assert.deepEqual(result.items, [{ Id: "123", RecordName: "Consumption", Abstract_c: "Service summary" }]);
|
||||||
|
const url = new URL(calls.at(-1).url);
|
||||||
|
assert.equal(url.searchParams.get("emailAddress"), "user+test@example.invalid");
|
||||||
|
assert.equal(url.searchParams.get("salesObject"), "OPPORTUNITY");
|
||||||
|
assert.equal(url.searchParams.get("serviceType"), "BUSINESS");
|
||||||
|
assert.ok(calls.every(call => call.options.headers.Authorization === "Session" && call.options.method === "GET"));
|
||||||
|
assert.equal((await h.dispatch({ type: "opportunitiesExtension.requestSrLookupOpportunityBusiness" })).ok, false);
|
||||||
|
assert.equal(calls.length, 3);
|
||||||
|
});
|
||||||
|
test("Primary pillar lookup uses Session, orders enabled name/code options and rejects invalid data", async () => {
|
||||||
|
const calls = [];
|
||||||
|
const h = loadBackground({ fetch: async (url, options) => {
|
||||||
|
calls.push({ url, options });
|
||||||
|
return response({ items: [
|
||||||
|
{ code: "B", name: "Second", displaySequence: 20, enabledFlag: "Y" },
|
||||||
|
{ code: "N", name: "Disabled", displaySequence: 1, enabledFlag: "N" },
|
||||||
|
{ code: "A", name: "First", displaySequence: 10, enabledFlag: "Y" }
|
||||||
|
] });
|
||||||
|
} });
|
||||||
|
const result = await h.dispatch({ type: "opportunitiesExtension.requestSrLookupPrimaryPillar" });
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.deepEqual(result.items, [{ code: "A", name: "First" }, { code: "B", name: "Second" }]);
|
||||||
|
assert.ok(calls[0].url.includes("SeaaS_initiation/live;profile=PROD"));
|
||||||
|
assert.ok(calls[0].url.endsWith("lookupType=SENGG_PRIMARY_PILLAR&type=STANDARD"));
|
||||||
|
assert.equal(calls[0].options.headers.Authorization, "Session");
|
||||||
|
assert.equal(calls[0].options.method, "GET");
|
||||||
|
for (const fetch of [async () => response({ items: [{}] }), async () => response({}, 401)]) {
|
||||||
|
assert.equal((await loadBackground({ fetch, messageTimeout: 10000 }).dispatch({ type: "opportunitiesExtension.requestSrLookupPrimaryPillar" })).ok, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (const family of ["chrome", "firefox"]) {
|
||||||
|
test(`${family}: 401 is returned to the caller without opening tabs`, async () => {
|
||||||
|
let count = 0;
|
||||||
|
const h = loadBackground({ family, fetch: async () => ++count < 3 ? response({}, 401) : response({ items: [] }) });
|
||||||
|
const result = await h.dispatch({ type, srId: "123" });
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.equal(result.httpStatus, 401);
|
||||||
|
assert.equal(count, 1);
|
||||||
|
assert.equal(h.tabs.length, 0);
|
||||||
|
assert.deepEqual(h.removedTabs, []);
|
||||||
|
assert.deepEqual(result.debugRequests.map(entry => entry.httpStatus), [401]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
test("concurrent 401 responses do not cause background tab recovery", async () => {
|
||||||
|
let calls = 0;
|
||||||
|
const h = loadBackground({ fetch: async url => {
|
||||||
|
if (++calls <= 2) return response({}, 401);
|
||||||
|
const id = new URL(url).searchParams.get("q");
|
||||||
|
return response({ items: [{ ServiceRequest_SRToInternalSR_Tgt: [{ Title: id }] }] });
|
||||||
|
} });
|
||||||
|
const results = await Promise.all([h.dispatch({ type, srId: "1" }), h.dispatch({ type, srId: "2" })]);
|
||||||
|
assert.equal(h.tabs.length, 0);
|
||||||
|
assert.deepEqual(h.removedTabs, []);
|
||||||
|
assert.ok(results.every(result => result.httpStatus === 401));
|
||||||
|
});
|
||||||
|
test("background never creates recovery tabs after an error", async () => {
|
||||||
|
for (const status of [401, 500]) {
|
||||||
|
let count = 0;
|
||||||
|
const h = loadBackground({ fetch: async () => response({}, ++count === 1 ? 401 : status) });
|
||||||
|
const result = await h.dispatch({ type, srId: "1" });
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.equal(count, 1);
|
||||||
|
assert.deepEqual(h.removedTabs, []);
|
||||||
|
}
|
||||||
|
const h = loadBackground({ fetch: async () => response({}, 403) });
|
||||||
|
assert.equal((await h.dispatch({ type, srId: "1" })).ok, false);
|
||||||
|
assert.equal(h.tabs.length, 0);
|
||||||
|
});
|
||||||
for (const family of ["chrome", "firefox"]) {
|
for (const family of ["chrome", "firefox"]) {
|
||||||
test(`${family}: SR detail GET uses exact scope and Session authentication`, async () => {
|
test(`${family}: SR detail GET uses exact scope and Session authentication`, async () => {
|
||||||
const calls = [];
|
const calls = [];
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const { test } = require("node:test");
|
|||||||
const { loadApplication } = require("./support/application.cjs");
|
const { loadApplication } = require("./support/application.cjs");
|
||||||
const flush = () => new Promise(resolve => setImmediate(resolve));
|
const flush = () => new Promise(resolve => setImmediate(resolve));
|
||||||
|
|
||||||
test("internal SR uses Session without TokenRelay or retries on 401", async () => {
|
test("internal SR uses Session without TokenRelay and reports unavailable iframe host", async () => {
|
||||||
const h = setup();
|
const h = setup();
|
||||||
let renewals = 0;
|
let renewals = 0;
|
||||||
const sent = [];
|
const sent = [];
|
||||||
@@ -24,7 +24,7 @@ test("internal SR uses Session without TokenRelay or retries on 401", async () =
|
|||||||
assert.equal(sent.length, 1);
|
assert.equal(sent.length, 1);
|
||||||
assert.equal(sent[0].token, undefined);
|
assert.equal(sent[0].token, undefined);
|
||||||
assert.ok(sent.every(item => item.type === "opportunitiesExtension.requestInternalServiceRequest"));
|
assert.ok(sent.every(item => item.type === "opportunitiesExtension.requestInternalServiceRequest"));
|
||||||
assert.ok(h.walk(h.panel()).some(node => node.textContent === "Unauthorized"));
|
assert.ok(h.walk(h.panel()).some(node => node.attributes.role === "alert"));
|
||||||
});
|
});
|
||||||
|
|
||||||
function setup() {
|
function setup() {
|
||||||
|
|||||||
145
tests/internalSrSession.test.cjs
Normal file
145
tests/internalSrSession.test.cjs
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
const assert = require("node:assert/strict");
|
||||||
|
const { test } = require("node:test");
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const vm = require("node:vm");
|
||||||
|
const path = require("node:path");
|
||||||
|
const factory = vm.runInNewContext(`${fs.readFileSync(path.join(__dirname, "../src/features/opportunities/internal-sr-session.js"), "utf8")}\ncreateInternalSrSession`);
|
||||||
|
function setup(query, options = {}) {
|
||||||
|
let elapsed = 0;
|
||||||
|
const frames = [], removed = [], delays = [], stopped = [];
|
||||||
|
const body = { append(frame) { frame.initialSrc = frame.src; frames.push(frame); } };
|
||||||
|
const document = { body, createElement(tag) {
|
||||||
|
assert.equal(tag, "iframe");
|
||||||
|
return { contentWindow: { stop() { stopped.push(true); } }, style: {}, attributes: {}, listeners: {}, setAttribute(k, v) { this.attributes[k] = v; },
|
||||||
|
addEventListener(k, fn) { this.listeners[k] = fn; }, remove() { removed.push(this); } };
|
||||||
|
} };
|
||||||
|
const api = factory({ ...options, query, environment: { document, performance: { now: () => elapsed }, setTimeout: (fn, ms) => { delays.push(ms); elapsed += ms; return setImmediate(fn); } } });
|
||||||
|
return { api, frames, removed, delays, stopped };
|
||||||
|
}
|
||||||
|
|
||||||
|
test("lookup requests recover together using the configured page and preserve each result", async () => {
|
||||||
|
let authenticated = false;
|
||||||
|
let calls = 0;
|
||||||
|
const h = setup(async message => {
|
||||||
|
if (++calls <= 4) return { ok: false, httpStatus: 401 };
|
||||||
|
authenticated = true;
|
||||||
|
return { ok: true, items: [message.type] };
|
||||||
|
}, { sessionUrl: "https://eeho.fa.us2.oraclecloud.com/?view=Show%20Service%20Request", sessionLabel: "Service Request" });
|
||||||
|
const types = ["pillar", "language", "platform", "services"];
|
||||||
|
const results = await Promise.all(types.map(type => h.api.request({ type })));
|
||||||
|
assert.deepEqual(results, types.map(type => [type]));
|
||||||
|
assert.equal(authenticated, true);
|
||||||
|
assert.equal(h.frames.length, 1);
|
||||||
|
assert.match(h.frames[0].initialSrc, /Show%20Service%20Request$/);
|
||||||
|
assert.equal(h.frames[0].title, "Service Request session initialization");
|
||||||
|
assert.equal(h.removed.length, 1);
|
||||||
|
assert.equal(h.stopped.length, 1);
|
||||||
|
});
|
||||||
|
test("401 initializes a hidden iframe in the calling document and removes it on success", async () => {
|
||||||
|
let calls = 0;
|
||||||
|
const h = setup(async () => ++calls < 3 ? { ok: false, httpStatus: 401 } : { ok: true, items: [{ SrId: 1 }] });
|
||||||
|
const items = await h.api.request("1");
|
||||||
|
assert.equal(items[0].SrId, 1);
|
||||||
|
assert.equal(h.frames.length, 1);
|
||||||
|
const frame = h.frames[0];
|
||||||
|
assert.equal(frame.tabIndex, -1);
|
||||||
|
assert.equal(frame.attributes["aria-hidden"], "true");
|
||||||
|
assert.match(frame.style.cssText, /pointer-events:none/);
|
||||||
|
assert.ok(!frame.attributes.sandbox.includes("allow-top-navigation"));
|
||||||
|
assert.ok(frame.initialSrc.endsWith("&view=showisr"));
|
||||||
|
assert.equal(frame.src, "about:blank");
|
||||||
|
assert.equal(h.stopped.length, 1);
|
||||||
|
assert.deepEqual(h.delays, [250]);
|
||||||
|
assert.equal(h.removed[0], frame);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an immediately available session stops the iframe without waiting for load or a timer", async () => {
|
||||||
|
let calls = 0;
|
||||||
|
const h = setup(async () => ++calls === 1 ? { ok: false, httpStatus: 401 } : { ok: true, items: [] });
|
||||||
|
await h.api.request("1");
|
||||||
|
assert.equal(calls, 2);
|
||||||
|
assert.equal(h.delays.length, 0);
|
||||||
|
assert.equal(h.stopped.length, 1);
|
||||||
|
assert.equal(h.removed.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("lookup 401 errors from older background responses still initialize the session", async () => {
|
||||||
|
for (const failure of [
|
||||||
|
{ ok: false, error: "requestSrLookupLanguage failed with status 401." },
|
||||||
|
{ ok: false, debugRequests: [{ httpStatus: 401 }] },
|
||||||
|
{ ok: false, httpStatus: "401" }
|
||||||
|
]) {
|
||||||
|
let calls = 0;
|
||||||
|
const h = setup(async () => ++calls <= 2 ? failure : { ok: true, items: ["ready"] });
|
||||||
|
assert.deepEqual(await h.api.request("language"), ["ready"]);
|
||||||
|
assert.equal(h.frames.length, 1);
|
||||||
|
assert.equal(h.removed.length, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a lookup that still returns 401 after shared recovery initializes its own session", async () => {
|
||||||
|
const calls = new Map();
|
||||||
|
const h = setup(async id => {
|
||||||
|
const count = (calls.get(id) || 0) + 1;
|
||||||
|
calls.set(id, count);
|
||||||
|
return count <= (id === "language" ? 1 : 2)
|
||||||
|
? { ok: false, httpStatus: 401 } : { ok: true, items: [id] };
|
||||||
|
});
|
||||||
|
assert.deepEqual(await Promise.all([h.api.request("language"), h.api.request("platform")]), [["language"], ["platform"]]);
|
||||||
|
assert.equal(h.frames.length, 2);
|
||||||
|
assert.equal(h.removed.length, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("cross-origin redirects still allow iframe removal when stop is blocked", async () => {
|
||||||
|
let calls = 0;
|
||||||
|
const h = setup(async () => {
|
||||||
|
if (++calls === 1) return { ok: false, httpStatus: 401 };
|
||||||
|
h.frames[0].contentWindow.stop = () => { throw new Error("Cross-origin"); };
|
||||||
|
return { ok: true, items: [] };
|
||||||
|
});
|
||||||
|
await h.api.request("1");
|
||||||
|
assert.equal(h.frames[0].src, "about:blank");
|
||||||
|
assert.equal(h.removed.length, 1);
|
||||||
|
});
|
||||||
|
test("concurrent queries share one iframe without mixing SR results", async () => {
|
||||||
|
let calls = 0;
|
||||||
|
const h = setup(async id => ++calls <= 2 ? { ok: false, httpStatus: 401 } : { ok: true, items: [id] });
|
||||||
|
const results = await Promise.all([h.api.request("1"), h.api.request("2")]);
|
||||||
|
assert.deepEqual(results, [["1"], ["2"]]);
|
||||||
|
assert.equal(h.frames.length, 1);
|
||||||
|
assert.equal(h.removed.length, 1);
|
||||||
|
});
|
||||||
|
test("normal success does not mount an iframe; errors and timeout always clean it up", async () => {
|
||||||
|
const success = setup(async () => ({ ok: true, items: [] }));
|
||||||
|
await success.api.request("1");
|
||||||
|
assert.equal(success.frames.length, 0);
|
||||||
|
for (const status of [401, 500]) {
|
||||||
|
let calls = 0;
|
||||||
|
const h = setup(async () => ({ ok: false, httpStatus: ++calls === 1 ? 401 : status }));
|
||||||
|
await assert.rejects(h.api.request("1"));
|
||||||
|
if (status === 401) {
|
||||||
|
assert.ok(calls > 31);
|
||||||
|
assert.equal(h.delays.reduce((sum, ms) => sum + ms, 0), 60000);
|
||||||
|
} else assert.equal(calls, 2);
|
||||||
|
assert.equal(h.removed.length, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("slow session initialization can succeed after the old thirty-attempt limit", async () => {
|
||||||
|
let calls = 0;
|
||||||
|
const h = setup(async () => ++calls < 45 ? { ok: false, httpStatus: 401 } : { ok: true, items: ["ready"] });
|
||||||
|
assert.deepEqual(await h.api.request("1"), ["ready"]);
|
||||||
|
assert.equal(calls, 45);
|
||||||
|
assert.equal(h.removed.length, 1);
|
||||||
|
assert.ok(h.delays.reduce((sum, ms) => sum + ms, 0) < 60000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("iframe load alone never confirms authentication", async () => {
|
||||||
|
let calls = 0;
|
||||||
|
const h = setup(async () => {
|
||||||
|
if (++calls > 1) h.frames[0].listeners.load();
|
||||||
|
return { ok: false, httpStatus: 401 };
|
||||||
|
});
|
||||||
|
await assert.rejects(h.api.request("1"), /HTTP 401.*load event, but authentication was not confirmed/);
|
||||||
|
assert.equal(h.removed.length, 1);
|
||||||
|
});
|
||||||
47
tests/srSessionPreparation.test.cjs
Normal file
47
tests/srSessionPreparation.test.cjs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
const assert = require("node:assert/strict");
|
||||||
|
const { test } = require("node:test");
|
||||||
|
const { loadApplication } = require("./support/application.cjs");
|
||||||
|
|
||||||
|
test("preparation runs once, shares pending work and reports loading and completion", async () => {
|
||||||
|
const app = loadApplication("content");
|
||||||
|
const progress = [];
|
||||||
|
let requests = 0;
|
||||||
|
let resolveUser, resolveSession;
|
||||||
|
app.currentUser.ensure = () => new Promise(resolve => { resolveUser = resolve; });
|
||||||
|
app.currentUser.allowsCrmActivities = () => true;
|
||||||
|
app.activityProgress.update = (...args) => progress.push(args);
|
||||||
|
app.debugContentLog.appendBackgroundRequestLog = () => {};
|
||||||
|
app.platformContentMessages.sendRuntimeMessage = message => {
|
||||||
|
assert.equal(message.type, "opportunitiesExtension.prepareSrSession");
|
||||||
|
requests += 1;
|
||||||
|
return new Promise(resolve => { resolveSession = resolve; });
|
||||||
|
};
|
||||||
|
const first = app.srSessionPreparation.ensure();
|
||||||
|
assert.equal(app.srSessionPreparation.ensure(), first);
|
||||||
|
assert.equal(requests, 0, "identity must load first");
|
||||||
|
resolveUser();
|
||||||
|
await new Promise(resolve => setImmediate(resolve));
|
||||||
|
assert.deepEqual(progress, [["session", "running"]]);
|
||||||
|
resolveSession({ ok: true });
|
||||||
|
await first;
|
||||||
|
await app.srSessionPreparation.ensure();
|
||||||
|
assert.equal(requests, 1);
|
||||||
|
assert.deepEqual(progress.at(-1), ["session", "complete"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("SALES_REP skips preparation; failed preparation does not auto-repeat or reject modal loading", async () => {
|
||||||
|
for (const allowed of [false, true]) {
|
||||||
|
const app = loadApplication("content");
|
||||||
|
let requests = 0;
|
||||||
|
const progress = [];
|
||||||
|
app.currentUser.ensure = async () => {};
|
||||||
|
app.currentUser.allowsCrmActivities = () => allowed;
|
||||||
|
app.activityProgress.update = (...args) => progress.push(args);
|
||||||
|
app.debugContentLog.appendBackgroundRequestLog = () => {};
|
||||||
|
app.platformContentMessages.sendRuntimeMessage = async () => { requests += 1; return { ok: false }; };
|
||||||
|
await app.srSessionPreparation.ensure();
|
||||||
|
await app.srSessionPreparation.ensure();
|
||||||
|
assert.equal(requests, allowed ? 1 : 0);
|
||||||
|
assert.deepEqual(progress, allowed ? [["session", "running"], ["session", "error"]] : []);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -57,7 +57,7 @@ function loadContent({ fetch = unexpectedFetch, storage = new Map(), xsrf = null
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadBackground({ family = "chrome", fetch = unexpectedFetch, storage = {}, cookies = () => [], storageError = false } = {}) {
|
function loadBackground({ family = "chrome", fetch = unexpectedFetch, storage = {}, cookies = () => [], storageError = false, messageTimeout = 2000 } = {}) {
|
||||||
const time = clock();
|
const time = clock();
|
||||||
const listeners = [];
|
const listeners = [];
|
||||||
const progress = [];
|
const progress = [];
|
||||||
@@ -123,7 +123,7 @@ function loadBackground({ family = "chrome", fetch = unexpectedFetch, storage =
|
|||||||
function dispatch(message) {
|
function dispatch(message) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let replies = 0;
|
let replies = 0;
|
||||||
const timer = setTimeout(() => reject(new Error("Message did not receive a response")), 2000);
|
const timer = setTimeout(() => reject(new Error("Message did not receive a response")), messageTimeout);
|
||||||
const pending = listeners[0](message, sender, result => {
|
const pending = listeners[0](message, sender, result => {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
assert.equal(++replies, 1, "Message must receive exactly one response");
|
assert.equal(++replies, 1, "Message must receive exactly one response");
|
||||||
|
|||||||
@@ -35,6 +35,89 @@ function setup() {
|
|||||||
return { app, host, document, nodes, form };
|
return { app, host, document, nodes, form };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("missing SR plus opens pillar form, handles retry, ignores closed requests and respects roles", async () => {
|
||||||
|
const h = setup();
|
||||||
|
h.app.opportunitiesTableState.opportunitiesTableState.items = [
|
||||||
|
{ OptyNumber: "A", Name: "<Opportunity A>", CustomerAccount: { PartyUniqueName: "Customer A" }, EffectiveDate: "2026-11-30" }
|
||||||
|
];
|
||||||
|
h.app.opportunitiesQueryState.periodRequestVersion = 1;
|
||||||
|
h.app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => [{ SrNumber: "SR1", OpportunityName_Id_c: { OptyNumber: "B" } }];
|
||||||
|
h.app.debugContentView.renderDebugPanel = () => {};
|
||||||
|
await h.app.opportunityCrmServiceRequests.load("test", 1);
|
||||||
|
const calls = [];
|
||||||
|
const extraCalls = [];
|
||||||
|
h.app.currentUser.getUserId = () => "user@example.invalid";
|
||||||
|
h.app.platformContentMessages.sendRuntimeMessage = message => {
|
||||||
|
if (message.type.endsWith("requestSrLookupPrimaryPillar")) return new Promise(resolve => calls.push({ message, resolve }));
|
||||||
|
extraCalls.push(message);
|
||||||
|
return Promise.resolve({ ok: true, items: message.type.endsWith("requestSrLookupOpportunityBusiness")
|
||||||
|
? [{ Id: "123", RecordName: "Service", Abstract_c: "<Description>" }]
|
||||||
|
: [{ code: "CODE", name: "Label" }] });
|
||||||
|
};
|
||||||
|
const badge = h.app.opportunityCrmServiceRequests.createBadge("A");
|
||||||
|
const plus = badge.children.find(node => node.tag === "button");
|
||||||
|
assert.ok(plus);
|
||||||
|
assert.equal(plus.textContent, "+");
|
||||||
|
assert.ok(!h.app.opportunityCrmServiceRequests.createBadge("B").children.some(node => node.className === "opportunities-extension-add-action"));
|
||||||
|
plus.click();
|
||||||
|
assert.deepEqual(h.nodes().filter(node => node.tag === "dt").map(node => node.textContent), ["Customer", "Name", "Close Date"]);
|
||||||
|
assert.deepEqual(h.nodes().filter(node => node.tag === "dd").map(node => node.textContent), ["Customer A", "<Opportunity A>", "30/11/2026"]);
|
||||||
|
assert.equal(calls.length, 1);
|
||||||
|
assert.equal(calls[0].message.type, "opportunitiesExtension.requestSrLookupPrimaryPillar");
|
||||||
|
const field = () => h.nodes().find(node => node.name === "primaryPillar");
|
||||||
|
assert.equal(field().disabled, true);
|
||||||
|
calls[0].resolve({ ok: false, error: "Unauthorized" });
|
||||||
|
await flush();
|
||||||
|
assert.equal(h.form().attributes["data-state"], "error");
|
||||||
|
assert.equal(extraCalls.length, 3);
|
||||||
|
assert.equal(extraCalls.find(call => call.type.endsWith("requestSrLookupOpportunityBusiness")).emailAddress, "user@example.invalid");
|
||||||
|
assert.equal(h.nodes().find(node => node.name === "customerDeadline").type, "date");
|
||||||
|
assert.equal(h.nodes().find(node => node.name === "description").tag, "textarea");
|
||||||
|
assert.equal(h.nodes().find(node => node.name === "language").children[1].value, "CODE");
|
||||||
|
assert.equal(h.nodes().find(node => node.name === "platform").children[1].textContent, "Label");
|
||||||
|
assert.equal(h.nodes().find(node => node.name === "service").value, "123");
|
||||||
|
assert.equal(h.nodes().find(node => node.name === "service").type, "radio");
|
||||||
|
assert.ok(h.nodes().some(node => node.textContent === "<Description>"));
|
||||||
|
h.nodes().find(node => node.textContent === "Retry" && !node.hidden).click();
|
||||||
|
calls[1].resolve({ ok: true, items: [{ code: "SaaS_HCM", name: "SaaS - HCM" }] });
|
||||||
|
await flush();
|
||||||
|
assert.equal(field().disabled, false);
|
||||||
|
assert.equal(field().children[1].value, "SaaS_HCM");
|
||||||
|
assert.equal(field().children[1].textContent, "SaaS - HCM");
|
||||||
|
const search = h.nodes().find(node => node.id === `${field().id}-search`);
|
||||||
|
assert.equal(field().hidden, true);
|
||||||
|
assert.equal(search.disabled, false);
|
||||||
|
search.value = "hcm";
|
||||||
|
search.listeners.input();
|
||||||
|
const options = h.nodes().find(node => node.id === search.attributes["aria-controls"]);
|
||||||
|
assert.equal(options.children.length, 1);
|
||||||
|
assert.equal(options.children[0].textContent, "SaaS - HCM");
|
||||||
|
search.listeners.keydown({ key: "ArrowDown", preventDefault() {} });
|
||||||
|
search.listeners.keydown({ key: "Enter", preventDefault() {} });
|
||||||
|
assert.equal(field().value, "SaaS_HCM");
|
||||||
|
assert.equal(search.value, "SaaS - HCM");
|
||||||
|
assert.equal(options.hidden, true);
|
||||||
|
search.value = "does not exist";
|
||||||
|
search.listeners.input();
|
||||||
|
assert.equal(options.textContent, "No results");
|
||||||
|
search.listeners.keydown({ key: "Escape", preventDefault() {}, stopPropagation() {} });
|
||||||
|
assert.equal(search.value, "SaaS - HCM");
|
||||||
|
assert.ok(h.form(), "Escape closes only the options");
|
||||||
|
assert.equal(calls.length, 2, "searching does not issue another request");
|
||||||
|
h.nodes().find(node => node.textContent === "Cancel").click();
|
||||||
|
assert.equal(h.host.children.length, 0);
|
||||||
|
h.app.opportunitiesTableState.opportunitiesTableState.items = [];
|
||||||
|
plus.click();
|
||||||
|
assert.deepEqual(h.nodes().filter(node => node.tag === "dd").map(node => node.textContent), ["-", "-", "-"]);
|
||||||
|
h.app.opportunityCrmServiceRequests.reset();
|
||||||
|
calls[2].resolve({ ok: true, items: [] });
|
||||||
|
await flush();
|
||||||
|
assert.equal(h.host.children.length, 0);
|
||||||
|
h.app.currentUser.allowsCrmActivities = () => false;
|
||||||
|
plus.click();
|
||||||
|
assert.equal(h.host.children.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
test("only plus opens the nine-field form; a missing workload stays disabled without a save operation", () => {
|
test("only plus opens the nine-field form; a missing workload stays disabled without a save operation", () => {
|
||||||
const h = setup();
|
const h = setup();
|
||||||
const badge = h.app.opportunityWorkloadActions.createBadge("A");
|
const badge = h.app.opportunityWorkloadActions.createBadge("A");
|
||||||
|
|||||||
Reference in New Issue
Block a user