Implemented the customer service requests view
This commit is contained in:
18
README.md
18
README.md
@@ -32,12 +32,26 @@ Chromium service worker and Firefox background scripts. The build composes class
|
|||||||
factory modules from `scripts/sources.cjs` into one isolated IIFE per execution
|
factory modules from `scripts/sources.cjs` into one isolated IIFE per execution
|
||||||
context. It serializes `src/content/styles/redwood.css` followed by the scoped
|
context. It serializes `src/content/styles/redwood.css` followed by the scoped
|
||||||
`src/features/debug/styles.css`, `src/features/opportunities/styles.css` and
|
`src/features/debug/styles.css`, `src/features/opportunities/styles.css` and
|
||||||
`src/features/install-base/styles.css` at
|
`src/features/install-base/styles.css` and `src/features/service-requests/styles.css` at
|
||||||
build time. There is no
|
build time. There is no
|
||||||
runtime module loader, browser-side eval, ES-module requirement or new dependency.
|
runtime module loader, browser-side eval, ES-module requirement or new dependency.
|
||||||
|
|
||||||
## Tests and Maintenance
|
## Tests and Maintenance
|
||||||
|
|
||||||
|
The Service Requests action sits immediately after Installed Base below Customer.
|
||||||
|
Its dialog opens with skeleton rows and lists open SRs for the selected customer.
|
||||||
|
The background first calls `requestAccountSummary` with ACCP-TS, then uses its
|
||||||
|
`id` (not `registryId`) in the GET `requestCustomerServiceRequest` URL. The SR
|
||||||
|
credential is `uiaasHeader.value` from `SPA-ORANGE`, sent verbatim in
|
||||||
|
`custsr-auth`, without a Bearer prefix, regardless of `uiaasHeader.label`.
|
||||||
|
Pages of 40 are consolidated using `totalResults`, `count`
|
||||||
|
and `offset`. Concurrent clicks for the same customer share the in-flight query;
|
||||||
|
results are not cached, and closed/replaced dialogs ignore late responses.
|
||||||
|
The table maps Name/Registry ID from `customer`, Product Name from `component`,
|
||||||
|
and formats status as a badge and dates using the existing date formatter.
|
||||||
|
Debug contains a Service Requests category; credential headers are masked before
|
||||||
|
delivery to content. This feature adds no permissions or authentication retries.
|
||||||
|
|
||||||
The Installed Base action sits beside customer consumption below Customer. It
|
The Installed Base action sits beside customer consumption below Customer. It
|
||||||
opens a skeleton dialog, then calls `requestAccountSummary` with ACCP-TS and uses
|
opens a skeleton dialog, then calls `requestAccountSummary` with ACCP-TS and uses
|
||||||
the returned `registryId` in `requestInstallBase`. The latter uses the SAIB-TS
|
the returned `registryId` in `requestInstallBase`. The latter uses the SAIB-TS
|
||||||
@@ -65,7 +79,7 @@ duration or error. Cache hits do not create HTTP entries. Product diagnostics
|
|||||||
mask authorization and sensitive response headers before messaging the source
|
mask authorization and sensitive response headers before messaging the source
|
||||||
tab/frame; this change adds no timeout, retry or authentication behavior.
|
tab/frame; this change adds no timeout, retry or authentication behavior.
|
||||||
|
|
||||||
Debug tabs group Authentication, Opportunities, Products, AI, Consumption and Installed Base
|
Debug tabs group Authentication, Opportunities, Products, AI, Consumption, Installed Base and Service Requests
|
||||||
requests; unknown request types appear under Other. Only categories with entries
|
requests; unknown request types appear under Other. Only categories with entries
|
||||||
are shown. All preserves the complete timeline. Every tab sorts by request start
|
are shown. All preserves the complete timeline. Every tab sorts by request start
|
||||||
time, retaining original request IDs and showing start times in collapsed rows.
|
time, retaining original request IDs and showing start times in collapsed rows.
|
||||||
|
|||||||
85
dist/chromium/background.js
vendored
85
dist/chromium/background.js
vendored
@@ -12,6 +12,7 @@ function createMessageTypes() {
|
|||||||
requestConsumerTokenService: "opportunitiesExtension.requestConsumerTokenService",
|
requestConsumerTokenService: "opportunitiesExtension.requestConsumerTokenService",
|
||||||
requestAccountPlans: "opportunitiesExtension.requestAccountPlans",
|
requestAccountPlans: "opportunitiesExtension.requestAccountPlans",
|
||||||
requestInstallBase: "opportunitiesExtension.requestInstallBase",
|
requestInstallBase: "opportunitiesExtension.requestInstallBase",
|
||||||
|
requestCustomerServiceRequest: "opportunitiesExtension.requestCustomerServiceRequest",
|
||||||
requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend"
|
requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -158,7 +159,8 @@ function createAuthConsumerSession({
|
|||||||
authorizations: {
|
authorizations: {
|
||||||
accountSummary: accountSummaryAuthorization,
|
accountSummary: accountSummaryAuthorization,
|
||||||
accountPlans: accountPlansAuthorization,
|
accountPlans: accountPlansAuthorization,
|
||||||
installBase: extractTokenServiceAuthorization(tokenResponse, "SAIB-TS", "")
|
installBase: extractTokenServiceAuthorization(tokenResponse, "SAIB-TS", ""),
|
||||||
|
customerServiceRequests: extractTokenServiceAuthorization(tokenResponse, "SPA-ORANGE", "")
|
||||||
},
|
},
|
||||||
debugRequests
|
debugRequests
|
||||||
};
|
};
|
||||||
@@ -502,6 +504,68 @@ function createSpaInstallBase({ ACCOUNT_SUMMARY_URL, fetchJson, findNestedObject
|
|||||||
return { requestInstallBase };
|
return { requestInstallBase };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Source: src/services/spa/customer-service-requests.js
|
||||||
|
function createSpaCustomerServiceRequests({ ACCOUNT_SUMMARY_URL, fetchJson, findAccountPlanId, normalizeMessageAuthorization }) {
|
||||||
|
const endpoint = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-custsr-provider/service/oscSr/";
|
||||||
|
const filter = encodeURIComponent(JSON.stringify({ hierarchy: "single", status: "open" }));
|
||||||
|
|
||||||
|
async function requestCustomerServiceRequest(message) {
|
||||||
|
const debugRequests = [];
|
||||||
|
try {
|
||||||
|
const partyNumber = String(message.partyNumber || "").trim();
|
||||||
|
const summaryAuth = normalizeMessageAuthorization(message.authorizations?.accountSummary, "account-ti-authorization");
|
||||||
|
const srAuth = normalizeMessageAuthorization(message.authorizations?.customerServiceRequests, "");
|
||||||
|
if (!partyNumber) throw new Error("Customer Party Number is required.");
|
||||||
|
if (!summaryAuth.value) throw new Error("ACCP-TS authorization is unavailable.");
|
||||||
|
if (!srAuth.value) throw new Error("SPA-ORANGE uiaasHeader.value is unavailable.");
|
||||||
|
|
||||||
|
const summary = await fetchJson(ACCOUNT_SUMMARY_URL, {
|
||||||
|
method: "POST", credentials: "include",
|
||||||
|
headers: { Accept: "application/json", "Content-Type": "application/json", [summaryAuth.name]: summaryAuth.value },
|
||||||
|
body: JSON.stringify([{ key: "regId", value: partyNumber }])
|
||||||
|
}, "requestAccountSummary", debugRequests);
|
||||||
|
const accountSummaryId = findAccountPlanId(summary);
|
||||||
|
if ((typeof accountSummaryId !== "string" && typeof accountSummaryId !== "number") || !String(accountSummaryId).trim()) {
|
||||||
|
throw new Error("Account id was not found in requestAccountSummary.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = [];
|
||||||
|
let offset = 0;
|
||||||
|
// This endpoint uses totalResults/count rather than hasMore.
|
||||||
|
for (let page = 0; page < 1000; page += 1) {
|
||||||
|
const url = `${endpoint}${encodeURIComponent(String(accountSummaryId))}?customerSRFilter=${filter}&orderBy=creationDate&orderByMode=desc&limit=40&offset=${offset}`;
|
||||||
|
const result = await fetchJson(url, {
|
||||||
|
method: "GET", credentials: "include",
|
||||||
|
// SPA-ORANGE's label may be lead-auth; this provider requires custsr-auth.
|
||||||
|
headers: { Accept: "application/json", "custsr-auth": srAuth.value }
|
||||||
|
}, "requestCustomerServiceRequest", debugRequests);
|
||||||
|
if (!result || !Array.isArray(result.objects)
|
||||||
|
|| result.objects.some(item => !item || typeof item !== "object" || Array.isArray(item))
|
||||||
|
|| !Number.isSafeInteger(result.totalResults) || result.totalResults < 0
|
||||||
|
|| !Number.isSafeInteger(result.count) || result.count !== result.objects.length) {
|
||||||
|
throw new Error("requestCustomerServiceRequest returned an invalid collection.");
|
||||||
|
}
|
||||||
|
if (result.offset !== offset) throw new Error("requestCustomerServiceRequest returned an unexpected offset.");
|
||||||
|
items.push(...result.objects);
|
||||||
|
offset += result.count;
|
||||||
|
if (offset >= result.totalResults) return { ok: true, items, debugRequests };
|
||||||
|
if (!result.count) throw new Error("requestCustomerServiceRequest pagination made no progress.");
|
||||||
|
}
|
||||||
|
throw new Error("requestCustomerServiceRequest pagination limit reached.");
|
||||||
|
} catch (error) {
|
||||||
|
error.debugRequests = debugRequests;
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
for (const entry of debugRequests) {
|
||||||
|
for (const key of Object.keys(entry.requestHeaders || {})) {
|
||||||
|
if (!/^(accept|content-type)$/i.test(key)) entry.requestHeaders[key] = "(present)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { requestCustomerServiceRequest };
|
||||||
|
}
|
||||||
|
|
||||||
// Source: src/services/spa/consumption.js
|
// Source: src/services/spa/consumption.js
|
||||||
function createSpaConsumption({
|
function createSpaConsumption({
|
||||||
CONSUMPTION_TREND_REQUESTS,
|
CONSUMPTION_TREND_REQUESTS,
|
||||||
@@ -1513,6 +1577,7 @@ function registerBackgroundMessages({
|
|||||||
refreshXsrfCookie,
|
refreshXsrfCookie,
|
||||||
requestAccountPlans,
|
requestAccountPlans,
|
||||||
requestInstallBase,
|
requestInstallBase,
|
||||||
|
requestCustomerServiceRequest,
|
||||||
requestConsumerTokenService,
|
requestConsumerTokenService,
|
||||||
requestConsumptionTrend,
|
requestConsumptionTrend,
|
||||||
requestOpptyProducts,
|
requestOpptyProducts,
|
||||||
@@ -1592,6 +1657,14 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.type === messageTypes.requestCustomerServiceRequest) {
|
||||||
|
requestCustomerServiceRequest(message).then(sendResponse).catch(error => sendResponse({
|
||||||
|
ok: false, error: error.message || "Unable to load service requests.",
|
||||||
|
debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : []
|
||||||
|
}));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (message.type === messageTypes.requestInstallBase) {
|
if (message.type === messageTypes.requestInstallBase) {
|
||||||
requestInstallBase(message).then(sendResponse).catch(error => sendResponse({
|
requestInstallBase(message).then(sendResponse).catch(error => sendResponse({
|
||||||
ok: false, error: error.message || "Unable to load installed base.",
|
ok: false, error: error.message || "Unable to load installed base.",
|
||||||
@@ -1675,6 +1748,12 @@ function createBackgroundApplication(overrides = {}) {
|
|||||||
findNestedObject: (...args) => spaResponseShapes.findNestedObject(...args),
|
findNestedObject: (...args) => spaResponseShapes.findNestedObject(...args),
|
||||||
normalizeMessageAuthorization: (...args) => authSpaCredentials.normalizeMessageAuthorization(...args)
|
normalizeMessageAuthorization: (...args) => authSpaCredentials.normalizeMessageAuthorization(...args)
|
||||||
});
|
});
|
||||||
|
const spaCustomerServiceRequests = createSpaCustomerServiceRequests({
|
||||||
|
ACCOUNT_SUMMARY_URL: contracts.ACCOUNT_SUMMARY_URL,
|
||||||
|
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args),
|
||||||
|
findAccountPlanId: (...args) => spaResponseShapes.findAccountPlanId(...args),
|
||||||
|
normalizeMessageAuthorization: (...args) => authSpaCredentials.normalizeMessageAuthorization(...args)
|
||||||
|
});
|
||||||
const spaConsumption = createSpaConsumption({
|
const spaConsumption = createSpaConsumption({
|
||||||
CONSUMPTION_TREND_REQUESTS: contracts.CONSUMPTION_TREND_REQUESTS,
|
CONSUMPTION_TREND_REQUESTS: contracts.CONSUMPTION_TREND_REQUESTS,
|
||||||
CONSUMPTION_TREND_URL: contracts.CONSUMPTION_TREND_URL,
|
CONSUMPTION_TREND_URL: contracts.CONSUMPTION_TREND_URL,
|
||||||
@@ -1739,12 +1818,13 @@ function createBackgroundApplication(overrides = {}) {
|
|||||||
const refreshXsrfCookie = (...args) => authXsrfRecovery.refreshXsrfCookie(...args);
|
const refreshXsrfCookie = (...args) => authXsrfRecovery.refreshXsrfCookie(...args);
|
||||||
const requestAccountPlans = (...args) => spaAccounts.requestAccountPlans(...args);
|
const requestAccountPlans = (...args) => spaAccounts.requestAccountPlans(...args);
|
||||||
const requestInstallBase = (...args) => spaInstallBase.requestInstallBase(...args);
|
const requestInstallBase = (...args) => spaInstallBase.requestInstallBase(...args);
|
||||||
|
const requestCustomerServiceRequest = (...args) => spaCustomerServiceRequests.requestCustomerServiceRequest(...args);
|
||||||
const requestConsumerTokenService = (...args) => authConsumerSession.requestConsumerTokenService(...args);
|
const requestConsumerTokenService = (...args) => authConsumerSession.requestConsumerTokenService(...args);
|
||||||
const requestConsumptionTrend = (...args) => spaConsumption.requestConsumptionTrend(...args);
|
const requestConsumptionTrend = (...args) => spaConsumption.requestConsumptionTrend(...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;
|
||||||
function start() {
|
function start() {
|
||||||
startBackgroundApplication({ messageTypes, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestConsumerTokenService, requestConsumptionTrend, requestOpptyProducts, runtimeApi });
|
startBackgroundApplication({ messageTypes, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestConsumerTokenService, requestConsumptionTrend, requestOpptyProducts, runtimeApi });
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
start, contracts,
|
start, contracts,
|
||||||
@@ -1753,6 +1833,7 @@ function createBackgroundApplication(overrides = {}) {
|
|||||||
authSpaCredentials,
|
authSpaCredentials,
|
||||||
spaAccounts,
|
spaAccounts,
|
||||||
spaInstallBase,
|
spaInstallBase,
|
||||||
|
spaCustomerServiceRequests,
|
||||||
spaConsumption,
|
spaConsumption,
|
||||||
spaResponseShapes,
|
spaResponseShapes,
|
||||||
httpBackgroundJson,
|
httpBackgroundJson,
|
||||||
|
|||||||
211
dist/chromium/content.js
vendored
211
dist/chromium/content.js
vendored
File diff suppressed because one or more lines are too long
85
dist/firefox/background.js
vendored
85
dist/firefox/background.js
vendored
@@ -12,6 +12,7 @@ function createMessageTypes() {
|
|||||||
requestConsumerTokenService: "opportunitiesExtension.requestConsumerTokenService",
|
requestConsumerTokenService: "opportunitiesExtension.requestConsumerTokenService",
|
||||||
requestAccountPlans: "opportunitiesExtension.requestAccountPlans",
|
requestAccountPlans: "opportunitiesExtension.requestAccountPlans",
|
||||||
requestInstallBase: "opportunitiesExtension.requestInstallBase",
|
requestInstallBase: "opportunitiesExtension.requestInstallBase",
|
||||||
|
requestCustomerServiceRequest: "opportunitiesExtension.requestCustomerServiceRequest",
|
||||||
requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend"
|
requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -158,7 +159,8 @@ function createAuthConsumerSession({
|
|||||||
authorizations: {
|
authorizations: {
|
||||||
accountSummary: accountSummaryAuthorization,
|
accountSummary: accountSummaryAuthorization,
|
||||||
accountPlans: accountPlansAuthorization,
|
accountPlans: accountPlansAuthorization,
|
||||||
installBase: extractTokenServiceAuthorization(tokenResponse, "SAIB-TS", "")
|
installBase: extractTokenServiceAuthorization(tokenResponse, "SAIB-TS", ""),
|
||||||
|
customerServiceRequests: extractTokenServiceAuthorization(tokenResponse, "SPA-ORANGE", "")
|
||||||
},
|
},
|
||||||
debugRequests
|
debugRequests
|
||||||
};
|
};
|
||||||
@@ -502,6 +504,68 @@ function createSpaInstallBase({ ACCOUNT_SUMMARY_URL, fetchJson, findNestedObject
|
|||||||
return { requestInstallBase };
|
return { requestInstallBase };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Source: src/services/spa/customer-service-requests.js
|
||||||
|
function createSpaCustomerServiceRequests({ ACCOUNT_SUMMARY_URL, fetchJson, findAccountPlanId, normalizeMessageAuthorization }) {
|
||||||
|
const endpoint = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-custsr-provider/service/oscSr/";
|
||||||
|
const filter = encodeURIComponent(JSON.stringify({ hierarchy: "single", status: "open" }));
|
||||||
|
|
||||||
|
async function requestCustomerServiceRequest(message) {
|
||||||
|
const debugRequests = [];
|
||||||
|
try {
|
||||||
|
const partyNumber = String(message.partyNumber || "").trim();
|
||||||
|
const summaryAuth = normalizeMessageAuthorization(message.authorizations?.accountSummary, "account-ti-authorization");
|
||||||
|
const srAuth = normalizeMessageAuthorization(message.authorizations?.customerServiceRequests, "");
|
||||||
|
if (!partyNumber) throw new Error("Customer Party Number is required.");
|
||||||
|
if (!summaryAuth.value) throw new Error("ACCP-TS authorization is unavailable.");
|
||||||
|
if (!srAuth.value) throw new Error("SPA-ORANGE uiaasHeader.value is unavailable.");
|
||||||
|
|
||||||
|
const summary = await fetchJson(ACCOUNT_SUMMARY_URL, {
|
||||||
|
method: "POST", credentials: "include",
|
||||||
|
headers: { Accept: "application/json", "Content-Type": "application/json", [summaryAuth.name]: summaryAuth.value },
|
||||||
|
body: JSON.stringify([{ key: "regId", value: partyNumber }])
|
||||||
|
}, "requestAccountSummary", debugRequests);
|
||||||
|
const accountSummaryId = findAccountPlanId(summary);
|
||||||
|
if ((typeof accountSummaryId !== "string" && typeof accountSummaryId !== "number") || !String(accountSummaryId).trim()) {
|
||||||
|
throw new Error("Account id was not found in requestAccountSummary.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = [];
|
||||||
|
let offset = 0;
|
||||||
|
// This endpoint uses totalResults/count rather than hasMore.
|
||||||
|
for (let page = 0; page < 1000; page += 1) {
|
||||||
|
const url = `${endpoint}${encodeURIComponent(String(accountSummaryId))}?customerSRFilter=${filter}&orderBy=creationDate&orderByMode=desc&limit=40&offset=${offset}`;
|
||||||
|
const result = await fetchJson(url, {
|
||||||
|
method: "GET", credentials: "include",
|
||||||
|
// SPA-ORANGE's label may be lead-auth; this provider requires custsr-auth.
|
||||||
|
headers: { Accept: "application/json", "custsr-auth": srAuth.value }
|
||||||
|
}, "requestCustomerServiceRequest", debugRequests);
|
||||||
|
if (!result || !Array.isArray(result.objects)
|
||||||
|
|| result.objects.some(item => !item || typeof item !== "object" || Array.isArray(item))
|
||||||
|
|| !Number.isSafeInteger(result.totalResults) || result.totalResults < 0
|
||||||
|
|| !Number.isSafeInteger(result.count) || result.count !== result.objects.length) {
|
||||||
|
throw new Error("requestCustomerServiceRequest returned an invalid collection.");
|
||||||
|
}
|
||||||
|
if (result.offset !== offset) throw new Error("requestCustomerServiceRequest returned an unexpected offset.");
|
||||||
|
items.push(...result.objects);
|
||||||
|
offset += result.count;
|
||||||
|
if (offset >= result.totalResults) return { ok: true, items, debugRequests };
|
||||||
|
if (!result.count) throw new Error("requestCustomerServiceRequest pagination made no progress.");
|
||||||
|
}
|
||||||
|
throw new Error("requestCustomerServiceRequest pagination limit reached.");
|
||||||
|
} catch (error) {
|
||||||
|
error.debugRequests = debugRequests;
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
for (const entry of debugRequests) {
|
||||||
|
for (const key of Object.keys(entry.requestHeaders || {})) {
|
||||||
|
if (!/^(accept|content-type)$/i.test(key)) entry.requestHeaders[key] = "(present)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { requestCustomerServiceRequest };
|
||||||
|
}
|
||||||
|
|
||||||
// Source: src/services/spa/consumption.js
|
// Source: src/services/spa/consumption.js
|
||||||
function createSpaConsumption({
|
function createSpaConsumption({
|
||||||
CONSUMPTION_TREND_REQUESTS,
|
CONSUMPTION_TREND_REQUESTS,
|
||||||
@@ -1513,6 +1577,7 @@ function registerBackgroundMessages({
|
|||||||
refreshXsrfCookie,
|
refreshXsrfCookie,
|
||||||
requestAccountPlans,
|
requestAccountPlans,
|
||||||
requestInstallBase,
|
requestInstallBase,
|
||||||
|
requestCustomerServiceRequest,
|
||||||
requestConsumerTokenService,
|
requestConsumerTokenService,
|
||||||
requestConsumptionTrend,
|
requestConsumptionTrend,
|
||||||
requestOpptyProducts,
|
requestOpptyProducts,
|
||||||
@@ -1592,6 +1657,14 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.type === messageTypes.requestCustomerServiceRequest) {
|
||||||
|
requestCustomerServiceRequest(message).then(sendResponse).catch(error => sendResponse({
|
||||||
|
ok: false, error: error.message || "Unable to load service requests.",
|
||||||
|
debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : []
|
||||||
|
}));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (message.type === messageTypes.requestInstallBase) {
|
if (message.type === messageTypes.requestInstallBase) {
|
||||||
requestInstallBase(message).then(sendResponse).catch(error => sendResponse({
|
requestInstallBase(message).then(sendResponse).catch(error => sendResponse({
|
||||||
ok: false, error: error.message || "Unable to load installed base.",
|
ok: false, error: error.message || "Unable to load installed base.",
|
||||||
@@ -1675,6 +1748,12 @@ function createBackgroundApplication(overrides = {}) {
|
|||||||
findNestedObject: (...args) => spaResponseShapes.findNestedObject(...args),
|
findNestedObject: (...args) => spaResponseShapes.findNestedObject(...args),
|
||||||
normalizeMessageAuthorization: (...args) => authSpaCredentials.normalizeMessageAuthorization(...args)
|
normalizeMessageAuthorization: (...args) => authSpaCredentials.normalizeMessageAuthorization(...args)
|
||||||
});
|
});
|
||||||
|
const spaCustomerServiceRequests = createSpaCustomerServiceRequests({
|
||||||
|
ACCOUNT_SUMMARY_URL: contracts.ACCOUNT_SUMMARY_URL,
|
||||||
|
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args),
|
||||||
|
findAccountPlanId: (...args) => spaResponseShapes.findAccountPlanId(...args),
|
||||||
|
normalizeMessageAuthorization: (...args) => authSpaCredentials.normalizeMessageAuthorization(...args)
|
||||||
|
});
|
||||||
const spaConsumption = createSpaConsumption({
|
const spaConsumption = createSpaConsumption({
|
||||||
CONSUMPTION_TREND_REQUESTS: contracts.CONSUMPTION_TREND_REQUESTS,
|
CONSUMPTION_TREND_REQUESTS: contracts.CONSUMPTION_TREND_REQUESTS,
|
||||||
CONSUMPTION_TREND_URL: contracts.CONSUMPTION_TREND_URL,
|
CONSUMPTION_TREND_URL: contracts.CONSUMPTION_TREND_URL,
|
||||||
@@ -1739,12 +1818,13 @@ function createBackgroundApplication(overrides = {}) {
|
|||||||
const refreshXsrfCookie = (...args) => authXsrfRecovery.refreshXsrfCookie(...args);
|
const refreshXsrfCookie = (...args) => authXsrfRecovery.refreshXsrfCookie(...args);
|
||||||
const requestAccountPlans = (...args) => spaAccounts.requestAccountPlans(...args);
|
const requestAccountPlans = (...args) => spaAccounts.requestAccountPlans(...args);
|
||||||
const requestInstallBase = (...args) => spaInstallBase.requestInstallBase(...args);
|
const requestInstallBase = (...args) => spaInstallBase.requestInstallBase(...args);
|
||||||
|
const requestCustomerServiceRequest = (...args) => spaCustomerServiceRequests.requestCustomerServiceRequest(...args);
|
||||||
const requestConsumerTokenService = (...args) => authConsumerSession.requestConsumerTokenService(...args);
|
const requestConsumerTokenService = (...args) => authConsumerSession.requestConsumerTokenService(...args);
|
||||||
const requestConsumptionTrend = (...args) => spaConsumption.requestConsumptionTrend(...args);
|
const requestConsumptionTrend = (...args) => spaConsumption.requestConsumptionTrend(...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;
|
||||||
function start() {
|
function start() {
|
||||||
startBackgroundApplication({ messageTypes, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestConsumerTokenService, requestConsumptionTrend, requestOpptyProducts, runtimeApi });
|
startBackgroundApplication({ messageTypes, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestConsumerTokenService, requestConsumptionTrend, requestOpptyProducts, runtimeApi });
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
start, contracts,
|
start, contracts,
|
||||||
@@ -1753,6 +1833,7 @@ function createBackgroundApplication(overrides = {}) {
|
|||||||
authSpaCredentials,
|
authSpaCredentials,
|
||||||
spaAccounts,
|
spaAccounts,
|
||||||
spaInstallBase,
|
spaInstallBase,
|
||||||
|
spaCustomerServiceRequests,
|
||||||
spaConsumption,
|
spaConsumption,
|
||||||
spaResponseShapes,
|
spaResponseShapes,
|
||||||
httpBackgroundJson,
|
httpBackgroundJson,
|
||||||
|
|||||||
211
dist/firefox/content.js
vendored
211
dist/firefox/content.js
vendored
File diff suppressed because one or more lines are too long
@@ -32,6 +32,7 @@ different from extension storage and is not a safe place for credentials.
|
|||||||
| Content | Opportunity query and presentation | Filter sets, sorting, search, request versions, column formatters, dashboard animation |
|
| Content | Opportunity query and presentation | Filter sets, sorting, search, request versions, column formatters, dashboard animation |
|
||||||
| Content | Products UI | Current request ID, product map, expanded rows, progress, last update |
|
| Content | Products UI | Current request ID, product map, expanded rows, progress, last update |
|
||||||
| Content | Consumption UI | Plan state, per-plan/period trend cache, chart navigation and selected series |
|
| Content | Consumption UI | Plan state, per-plan/period trend cache, chart navigation and selected series |
|
||||||
|
| Content | Service Requests UI | Customer-specific in-flight queries, skeleton/table modal, stale-response guards; no persistent cache |
|
||||||
| Content | AI summary | Per-opportunity in-flight state and summary; fresh generation on expansion |
|
| Content | AI summary | Per-opportunity in-flight state and summary; fresh generation on expansion |
|
||||||
| Content | Authentication/debug | General and scoped token promises, SPA authorizations, request logs, visibility flag |
|
| Content | Authentication/debug | General and scoped token promises, SPA authorizations, request logs, visibility flag |
|
||||||
| Background | Message dispatcher | Synchronously registered listener, asynchronous replies using `return true` |
|
| Background | Message dispatcher | Synchronously registered listener, asynchronous replies using `return true` |
|
||||||
@@ -56,6 +57,7 @@ Production tokens are held in memory, not deliberately persisted by the extensio
|
|||||||
| Sales Common | GET `/fscmRestApi/tokenrelay?scopesfq=urn%3Aopc%3Aresource%3Afusion%3Aeeho%3Asales-common%2F` | AI summary Bearer | In-memory `expires_in`, shared in-flight acquisition; at most one summary retry after 401/403 |
|
| Sales Common | GET `/fscmRestApi/tokenrelay?scopesfq=urn%3Aopc%3Aresource%3Afusion%3Aeeho%3Asales-common%2F` | AI summary Bearer | In-memory `expires_in`, shared in-flight acquisition; at most one summary retry after 401/403 |
|
||||||
| ACCP-TS | SPA consumer service `uiaasHeader.value` | Account summary authorization | Retrieved with the consumer service response; expiry recorded, not automatically acted on |
|
| ACCP-TS | SPA consumer service `uiaasHeader.value` | Account summary authorization | Retrieved with the consumer service response; expiry recorded, not automatically acted on |
|
||||||
| OCICONS-TS | SPA consumer service `uiaasHeader.value` | Account plans and consumption trend authorization | Same service response, distinct credential from ACCP-TS |
|
| OCICONS-TS | SPA consumer service `uiaasHeader.value` | Account plans and consumption trend authorization | Same service response, distinct credential from ACCP-TS |
|
||||||
|
| SPA-ORANGE | SPA consumer service `uiaasHeader.value` | Customer service requests, fixed `custsr-auth` header with raw value, no Bearer prefix; ignores `uiaasHeader.label` | Optional at consumer acquisition; required only when opening Service Requests; existing in-memory session rules |
|
||||||
|
|
||||||
Fusion calls use `https://eeho.fa.us2.oraclecloud.com`; SPA calls use
|
Fusion calls use `https://eeho.fa.us2.oraclecloud.com`; SPA calls use
|
||||||
`https://spa.oracle.com/oalcrm/web/api/`. Preserve `credentials: include` and
|
`https://spa.oracle.com/oalcrm/web/api/`. Preserve `credentials: include` and
|
||||||
@@ -68,6 +70,7 @@ operation-specific headers rather than unifying all calls under a default token.
|
|||||||
| requestGenerateAiSummary | POST `/api/sales-common/gen-ai/v1/summarizations/generate` | `{path,promptCode,object,puid}`; prompt `cx.sales.generate_opportunity_summary`; only summaryText displayed |
|
| requestGenerateAiSummary | POST `/api/sales-common/gen-ai/v1/summarizations/generate` | `{path,promptCode,object,puid}`; prompt `cx.sales.generate_opportunity_summary`; only summaryText displayed |
|
||||||
| requestConsumerTokenService | GET `g2m-consumer-application/consumerTokenService` | No-store; parse JSON; HTML 200 recovery via inactive SPA UI tab; maximum five HTTP attempts |
|
| requestConsumerTokenService | GET `g2m-consumer-application/consumerTokenService` | No-store; parse JSON; HTML 200 recovery via inactive SPA UI tab; maximum five HTTP attempts |
|
||||||
| requestAccountSummary | POST `provider-proxy/g2m-account/service/accountsummary` | ACCP-TS; `[{key:"regId",value:partyNumber}]` |
|
| requestAccountSummary | POST `provider-proxy/g2m-account/service/accountsummary` | ACCP-TS; `[{key:"regId",value:partyNumber}]` |
|
||||||
|
| requestCustomerServiceRequest | GET `provider-proxy/g2m-custsr-provider/service/oscSr/{accountSummaryId}` | SPA-ORANGE; summary `id`, not `registryId`; encoded filter `{hierarchy:"single",status:"open"}`, creationDate descending, pages of 40 using totalResults/count/offset |
|
||||||
| requestAccountPlans | POST `provider-proxy/g2m-consumption/service/v1/account/accountPlans/{id}` | OCICONS-TS; original hierarchy/products/language/planStatus payload |
|
| requestAccountPlans | POST `provider-proxy/g2m-consumption/service/v1/account/accountPlans/{id}` | OCICONS-TS; original hierarchy/products/language/planStatus payload |
|
||||||
| requestConsumptionService{Period} | GET `provider-proxy/g2m-consumption/service/v1/plan/trend/{subPlanNum}?quarter=All&timePeriod={period}` | G2m-Authorization; quarterly/monthly/weekly/daily; weekly default |
|
| requestConsumptionService{Period} | GET `provider-proxy/g2m-consumption/service/v1/plan/trend/{subPlanNum}?quarter=All&timePeriod={period}` | G2m-Authorization; quarterly/monthly/weekly/daily; weekly default |
|
||||||
|
|
||||||
@@ -90,7 +93,8 @@ was silently changed during extraction.
|
|||||||
| refreshXsrfCookie | type; sender tab context | ok, refreshed, cookieName, durationMs, error |
|
| refreshXsrfCookie | type; sender tab context | ok, refreshed, cookieName, durationMs, error |
|
||||||
| requestOpptyProducts | requestId, accessToken, optyNumbers, cacheMaxAgeMs | ok, requestId, completed, total, failed, cacheHits; error envelope on dispatcher failure |
|
| requestOpptyProducts | requestId, accessToken, optyNumbers, cacheMaxAgeMs | ok, requestId, completed, total, failed, cacheHits; error envelope on dispatcher failure |
|
||||||
| opptyProductsProgress | Background to sender tab/frame | requestId, state, counters; per-item optyNumber/items/fromCache/cachedAt or error |
|
| opptyProductsProgress | Background to sender tab/frame | requestId, state, counters; per-item optyNumber/items/fromCache/cachedAt or error |
|
||||||
| requestConsumerTokenService | type; sender tab context | ok, authorizations.accountSummary/accountPlans, debugRequests |
|
| requestConsumerTokenService | type; sender tab context | ok, authorizations.accountSummary/accountPlans/installBase/customerServiceRequests, debugRequests |
|
||||||
|
| requestCustomerServiceRequest | partyNumber, authorizations.accountSummary/customerServiceRequests | ok, items (consolidated response.objects), debugRequests; error envelope on failure |
|
||||||
| requestAccountPlans | partyNumber, authorizations | ok, accountPlanId, plans, debugRequests |
|
| requestAccountPlans | partyNumber, authorizations | ok, accountPlanId, plans, debugRequests |
|
||||||
| requestConsumptionTrend | subPlanNum, period, authorization | ok, subPlanNum, period, items, debugRequests |
|
| requestConsumptionTrend | subPlanNum, period, authorization | ok, subPlanNum, period, items, debugRequests |
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ function composeScript(target, { bootstrap = true } = {}) {
|
|||||||
return `// Source: ${file}\n${source}`;
|
return `// Source: ${file}\n${source}`;
|
||||||
});
|
});
|
||||||
if (target === "content") {
|
if (target === "content") {
|
||||||
const css = ["src/content/styles/redwood.css", "src/features/debug/styles.css", "src/features/opportunities/styles.css", "src/features/install-base/styles.css"]
|
const css = ["src/content/styles/redwood.css", "src/features/debug/styles.css", "src/features/opportunities/styles.css", "src/features/install-base/styles.css", "src/features/service-requests/styles.css"]
|
||||||
.map(file => fs.readFileSync(path.join(root, file), "utf8")).join("\n");
|
.map(file => fs.readFileSync(path.join(root, file), "utf8")).join("\n");
|
||||||
parts.unshift(`const redwoodStyles = ${JSON.stringify(css)};`);
|
parts.unshift(`const redwoodStyles = ${JSON.stringify(css)};`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ module.exports = {
|
|||||||
"src/features/consumption/plans-action.js",
|
"src/features/consumption/plans-action.js",
|
||||||
"src/features/install-base/action.js",
|
"src/features/install-base/action.js",
|
||||||
"src/features/install-base/view.js",
|
"src/features/install-base/view.js",
|
||||||
|
"src/features/service-requests/action.js",
|
||||||
|
"src/features/service-requests/view.js",
|
||||||
"src/features/consumption/plans-view.js",
|
"src/features/consumption/plans-view.js",
|
||||||
"src/features/consumption/trend-controller.js",
|
"src/features/consumption/trend-controller.js",
|
||||||
"src/features/consumption/chart.js",
|
"src/features/consumption/chart.js",
|
||||||
@@ -59,6 +61,7 @@ module.exports = {
|
|||||||
"src/auth/spa-credentials.js",
|
"src/auth/spa-credentials.js",
|
||||||
"src/services/spa/accounts.js",
|
"src/services/spa/accounts.js",
|
||||||
"src/services/spa/install-base.js",
|
"src/services/spa/install-base.js",
|
||||||
|
"src/services/spa/customer-service-requests.js",
|
||||||
"src/services/spa/consumption.js",
|
"src/services/spa/consumption.js",
|
||||||
"src/services/spa/response-shapes.js",
|
"src/services/spa/response-shapes.js",
|
||||||
"src/services/http/background-json.js",
|
"src/services/http/background-json.js",
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ function createAuthConsumerSession({
|
|||||||
authorizations: {
|
authorizations: {
|
||||||
accountSummary: accountSummaryAuthorization,
|
accountSummary: accountSummaryAuthorization,
|
||||||
accountPlans: accountPlansAuthorization,
|
accountPlans: accountPlansAuthorization,
|
||||||
installBase: extractTokenServiceAuthorization(tokenResponse, "SAIB-TS", "")
|
installBase: extractTokenServiceAuthorization(tokenResponse, "SAIB-TS", ""),
|
||||||
|
customerServiceRequests: extractTokenServiceAuthorization(tokenResponse, "SPA-ORANGE", "")
|
||||||
},
|
},
|
||||||
debugRequests
|
debugRequests
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,6 +40,12 @@ function createBackgroundApplication(overrides = {}) {
|
|||||||
findNestedObject: (...args) => spaResponseShapes.findNestedObject(...args),
|
findNestedObject: (...args) => spaResponseShapes.findNestedObject(...args),
|
||||||
normalizeMessageAuthorization: (...args) => authSpaCredentials.normalizeMessageAuthorization(...args)
|
normalizeMessageAuthorization: (...args) => authSpaCredentials.normalizeMessageAuthorization(...args)
|
||||||
});
|
});
|
||||||
|
const spaCustomerServiceRequests = createSpaCustomerServiceRequests({
|
||||||
|
ACCOUNT_SUMMARY_URL: contracts.ACCOUNT_SUMMARY_URL,
|
||||||
|
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args),
|
||||||
|
findAccountPlanId: (...args) => spaResponseShapes.findAccountPlanId(...args),
|
||||||
|
normalizeMessageAuthorization: (...args) => authSpaCredentials.normalizeMessageAuthorization(...args)
|
||||||
|
});
|
||||||
const spaConsumption = createSpaConsumption({
|
const spaConsumption = createSpaConsumption({
|
||||||
CONSUMPTION_TREND_REQUESTS: contracts.CONSUMPTION_TREND_REQUESTS,
|
CONSUMPTION_TREND_REQUESTS: contracts.CONSUMPTION_TREND_REQUESTS,
|
||||||
CONSUMPTION_TREND_URL: contracts.CONSUMPTION_TREND_URL,
|
CONSUMPTION_TREND_URL: contracts.CONSUMPTION_TREND_URL,
|
||||||
@@ -104,12 +110,13 @@ function createBackgroundApplication(overrides = {}) {
|
|||||||
const refreshXsrfCookie = (...args) => authXsrfRecovery.refreshXsrfCookie(...args);
|
const refreshXsrfCookie = (...args) => authXsrfRecovery.refreshXsrfCookie(...args);
|
||||||
const requestAccountPlans = (...args) => spaAccounts.requestAccountPlans(...args);
|
const requestAccountPlans = (...args) => spaAccounts.requestAccountPlans(...args);
|
||||||
const requestInstallBase = (...args) => spaInstallBase.requestInstallBase(...args);
|
const requestInstallBase = (...args) => spaInstallBase.requestInstallBase(...args);
|
||||||
|
const requestCustomerServiceRequest = (...args) => spaCustomerServiceRequests.requestCustomerServiceRequest(...args);
|
||||||
const requestConsumerTokenService = (...args) => authConsumerSession.requestConsumerTokenService(...args);
|
const requestConsumerTokenService = (...args) => authConsumerSession.requestConsumerTokenService(...args);
|
||||||
const requestConsumptionTrend = (...args) => spaConsumption.requestConsumptionTrend(...args);
|
const requestConsumptionTrend = (...args) => spaConsumption.requestConsumptionTrend(...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;
|
||||||
function start() {
|
function start() {
|
||||||
startBackgroundApplication({ messageTypes, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestConsumerTokenService, requestConsumptionTrend, requestOpptyProducts, runtimeApi });
|
startBackgroundApplication({ messageTypes, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestConsumerTokenService, requestConsumptionTrend, requestOpptyProducts, runtimeApi });
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
start, contracts,
|
start, contracts,
|
||||||
@@ -118,6 +125,7 @@ function createBackgroundApplication(overrides = {}) {
|
|||||||
authSpaCredentials,
|
authSpaCredentials,
|
||||||
spaAccounts,
|
spaAccounts,
|
||||||
spaInstallBase,
|
spaInstallBase,
|
||||||
|
spaCustomerServiceRequests,
|
||||||
spaConsumption,
|
spaConsumption,
|
||||||
spaResponseShapes,
|
spaResponseShapes,
|
||||||
httpBackgroundJson,
|
httpBackgroundJson,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ function registerBackgroundMessages({
|
|||||||
refreshXsrfCookie,
|
refreshXsrfCookie,
|
||||||
requestAccountPlans,
|
requestAccountPlans,
|
||||||
requestInstallBase,
|
requestInstallBase,
|
||||||
|
requestCustomerServiceRequest,
|
||||||
requestConsumerTokenService,
|
requestConsumerTokenService,
|
||||||
requestConsumptionTrend,
|
requestConsumptionTrend,
|
||||||
requestOpptyProducts,
|
requestOpptyProducts,
|
||||||
@@ -84,6 +85,14 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.type === messageTypes.requestCustomerServiceRequest) {
|
||||||
|
requestCustomerServiceRequest(message).then(sendResponse).catch(error => sendResponse({
|
||||||
|
ok: false, error: error.message || "Unable to load service requests.",
|
||||||
|
debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : []
|
||||||
|
}));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (message.type === messageTypes.requestInstallBase) {
|
if (message.type === messageTypes.requestInstallBase) {
|
||||||
requestInstallBase(message).then(sendResponse).catch(error => sendResponse({
|
requestInstallBase(message).then(sendResponse).catch(error => sendResponse({
|
||||||
ok: false, error: error.message || "Unable to load installed base.",
|
ok: false, error: error.message || "Unable to load installed base.",
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ function createContentApplication(overrides = {}) {
|
|||||||
authSessionState,
|
authSessionState,
|
||||||
closeAccountPlansModal: (...args) => consumptionPlansView.closeAccountPlansModal(...args),
|
closeAccountPlansModal: (...args) => consumptionPlansView.closeAccountPlansModal(...args),
|
||||||
closeInstallBaseModal: (...args) => installBaseView.closeInstallBaseModal(...args),
|
closeInstallBaseModal: (...args) => installBaseView.closeInstallBaseModal(...args),
|
||||||
|
closeServiceRequestsModal: (...args) => serviceRequestsView.closeServiceRequestsModal(...args),
|
||||||
closeCustomerFilterPanel: (...args) => opportunitiesFilters.closeCustomerFilterPanel(...args),
|
closeCustomerFilterPanel: (...args) => opportunitiesFilters.closeCustomerFilterPanel(...args),
|
||||||
closeOwnerFilterPanel: (...args) => opportunitiesFilters.closeOwnerFilterPanel(...args),
|
closeOwnerFilterPanel: (...args) => opportunitiesFilters.closeOwnerFilterPanel(...args),
|
||||||
closePreferencesModal: (...args) => preferencesView.closePreferencesModal(...args),
|
closePreferencesModal: (...args) => preferencesView.closePreferencesModal(...args),
|
||||||
@@ -199,6 +200,7 @@ function createContentApplication(overrides = {}) {
|
|||||||
OPPORTUNITY_DETAIL_URL: contracts.OPPORTUNITY_DETAIL_URL,
|
OPPORTUNITY_DETAIL_URL: contracts.OPPORTUNITY_DETAIL_URL,
|
||||||
createCloudConsumptionButton: (...args) => consumptionPlansAction.createCloudConsumptionButton(...args),
|
createCloudConsumptionButton: (...args) => consumptionPlansAction.createCloudConsumptionButton(...args),
|
||||||
createInstallBaseButton: (...args) => installBaseAction.createInstallBaseButton(...args),
|
createInstallBaseButton: (...args) => installBaseAction.createInstallBaseButton(...args),
|
||||||
|
createServiceRequestsButton: (...args) => serviceRequestsAction.createServiceRequestsButton(...args),
|
||||||
createCopyOpportunityButton: (...args) => opportunitiesLinks.createCopyOpportunityButton(...args),
|
createCopyOpportunityButton: (...args) => opportunitiesLinks.createCopyOpportunityButton(...args),
|
||||||
createDetailLink: (...args) => opportunitiesLinks.createDetailLink(...args),
|
createDetailLink: (...args) => opportunitiesLinks.createDetailLink(...args),
|
||||||
createProductsCountButton: (...args) => productsDetails.createProductsCountButton(...args),
|
createProductsCountButton: (...args) => productsDetails.createProductsCountButton(...args),
|
||||||
@@ -333,6 +335,20 @@ function createContentApplication(overrides = {}) {
|
|||||||
openInstallBaseModal: (...args) => installBaseView.openInstallBaseModal(...args),
|
openInstallBaseModal: (...args) => installBaseView.openInstallBaseModal(...args),
|
||||||
environment
|
environment
|
||||||
});
|
});
|
||||||
|
const serviceRequestsView = createServiceRequestsView({
|
||||||
|
MODAL_ID: contracts.MODAL_ID,
|
||||||
|
createCloseIcon: (...args) => contentIcons.createCloseIcon(...args),
|
||||||
|
formatOracleResponseDate: (...args) => opportunitiesFormatters.formatOracleResponseDate(...args),
|
||||||
|
environment
|
||||||
|
});
|
||||||
|
const serviceRequestsAction = createServiceRequestsAction({
|
||||||
|
messageTypes,
|
||||||
|
requestConsumerTokenServiceOnce: (...args) => authContentSession.requestConsumerTokenServiceOnce(...args),
|
||||||
|
sendRuntimeMessage: (...args) => platformContentMessages.sendRuntimeMessage(...args),
|
||||||
|
appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args),
|
||||||
|
openServiceRequestsModal: (...args) => serviceRequestsView.openServiceRequestsModal(...args),
|
||||||
|
environment
|
||||||
|
});
|
||||||
const consumptionPlansAction = createConsumptionPlansAction({
|
const consumptionPlansAction = createConsumptionPlansAction({
|
||||||
messageTypes,
|
messageTypes,
|
||||||
appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args),
|
appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args),
|
||||||
@@ -443,6 +459,8 @@ function createContentApplication(overrides = {}) {
|
|||||||
consumptionPlansAction,
|
consumptionPlansAction,
|
||||||
installBaseAction,
|
installBaseAction,
|
||||||
installBaseView,
|
installBaseView,
|
||||||
|
serviceRequestsAction,
|
||||||
|
serviceRequestsView,
|
||||||
consumptionPlansView,
|
consumptionPlansView,
|
||||||
consumptionTrendController,
|
consumptionTrendController,
|
||||||
consumptionChart,
|
consumptionChart,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ function createContentModal({
|
|||||||
authSessionState,
|
authSessionState,
|
||||||
closeAccountPlansModal,
|
closeAccountPlansModal,
|
||||||
closeInstallBaseModal,
|
closeInstallBaseModal,
|
||||||
|
closeServiceRequestsModal,
|
||||||
closeCustomerFilterPanel,
|
closeCustomerFilterPanel,
|
||||||
closeOwnerFilterPanel,
|
closeOwnerFilterPanel,
|
||||||
closePreferencesModal,
|
closePreferencesModal,
|
||||||
@@ -434,6 +435,7 @@ function createContentModal({
|
|||||||
closePreferencesModal();
|
closePreferencesModal();
|
||||||
closeAccountPlansModal();
|
closeAccountPlansModal();
|
||||||
closeInstallBaseModal();
|
closeInstallBaseModal();
|
||||||
|
closeServiceRequestsModal();
|
||||||
modal.hidden = true;
|
modal.hidden = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,6 +464,10 @@ function createContentModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape") {
|
||||||
|
if (closeServiceRequestsModal()) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (closeInstallBaseModal()) {
|
if (closeInstallBaseModal()) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ function createDebugContentView({
|
|||||||
const categories = [
|
const categories = [
|
||||||
["all", "All"], ["authentication", "Authentication"],
|
["all", "All"], ["authentication", "Authentication"],
|
||||||
["opportunities", "Opportunities"], ["products", "Products"],
|
["opportunities", "Opportunities"], ["products", "Products"],
|
||||||
["ai", "AI"], ["consumption", "Consumption"], ["installBase", "Installed Base"], ["other", "Other"]
|
["ai", "AI"], ["consumption", "Consumption"], ["installBase", "Installed Base"],
|
||||||
|
["serviceRequests", "Service Requests"], ["other", "Other"]
|
||||||
];
|
];
|
||||||
let selectedCategory = "all";
|
let selectedCategory = "all";
|
||||||
const expandedIds = new Set();
|
const expandedIds = new Set();
|
||||||
@@ -22,6 +23,7 @@ function createDebugContentView({
|
|||||||
if (entry.name === "requestOpptyProducts") return "products";
|
if (entry.name === "requestOpptyProducts") return "products";
|
||||||
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 (entry.name === "requestCustomerServiceRequest") return "serviceRequests";
|
||||||
if (/^request(?:AccountSummary|AccountPlans|ConsumptionService)/.test(entry.name)) return "consumption";
|
if (/^request(?:AccountSummary|AccountPlans|ConsumptionService)/.test(entry.name)) return "consumption";
|
||||||
return "other";
|
return "other";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,11 +72,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.opportunities-extension-cell-actions .opportunities-extension-action-button {
|
.opportunities-extension-cell-actions .opportunities-extension-action-button {
|
||||||
|
flex: 0 0 24px;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.opportunities-extension-table > tbody > tr > td[data-column="customer"] .opportunities-extension-cell-actions {
|
||||||
|
min-width: 84px;
|
||||||
|
}
|
||||||
|
|
||||||
.opportunities-extension-cell-actions .opportunities-extension-action-button svg {
|
.opportunities-extension-cell-actions .opportunities-extension-action-button svg {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ function createOpportunitiesTable({
|
|||||||
OPPORTUNITY_DETAIL_URL,
|
OPPORTUNITY_DETAIL_URL,
|
||||||
createCloudConsumptionButton,
|
createCloudConsumptionButton,
|
||||||
createInstallBaseButton,
|
createInstallBaseButton,
|
||||||
|
createServiceRequestsButton,
|
||||||
createCopyOpportunityButton,
|
createCopyOpportunityButton,
|
||||||
createDetailLink,
|
createDetailLink,
|
||||||
createProductsCountButton,
|
createProductsCountButton,
|
||||||
@@ -149,7 +150,7 @@ function createOpportunitiesTable({
|
|||||||
);
|
);
|
||||||
const customerActions = document.createElement("span");
|
const customerActions = document.createElement("span");
|
||||||
customerActions.className = "opportunities-extension-cell-actions";
|
customerActions.className = "opportunities-extension-cell-actions";
|
||||||
customerActions.append(createCloudConsumptionButton(item), createInstallBaseButton(item));
|
customerActions.append(createCloudConsumptionButton(item), createInstallBaseButton(item), createServiceRequestsButton(item));
|
||||||
customerCellContent.append(customerLink, customerActions);
|
customerCellContent.append(customerLink, customerActions);
|
||||||
cell.append(customerCellContent);
|
cell.append(customerCellContent);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
52
src/features/service-requests/action.js
Normal file
52
src/features/service-requests/action.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
function createServiceRequestsAction({ messageTypes, requestConsumerTokenServiceOnce, sendRuntimeMessage,
|
||||||
|
appendBackgroundRequestLog, openServiceRequestsModal, environment }) {
|
||||||
|
const { document } = environment;
|
||||||
|
const pending = new Map();
|
||||||
|
|
||||||
|
async function load(partyNumber) {
|
||||||
|
const authorizations = await requestConsumerTokenServiceOnce();
|
||||||
|
const response = await sendRuntimeMessage({
|
||||||
|
type: messageTypes.requestCustomerServiceRequest, partyNumber,
|
||||||
|
authorizations: { accountSummary: authorizations.accountSummary, customerServiceRequests: authorizations.customerServiceRequests }
|
||||||
|
});
|
||||||
|
appendBackgroundRequestLog(response?.debugRequests);
|
||||||
|
if (!response?.ok) throw new Error(response?.error || "Unable to load service requests.");
|
||||||
|
return response.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createServiceRequestsButton(opportunity) {
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.className = "opportunities-extension-action-button";
|
||||||
|
button.title = "View customer service requests";
|
||||||
|
button.setAttribute("aria-label", `View service requests for ${opportunity.CustomerAccount?.PartyUniqueName || "customer"}`);
|
||||||
|
const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||||
|
icon.setAttribute("viewBox", "0 0 24 24");
|
||||||
|
icon.setAttribute("aria-hidden", "true");
|
||||||
|
icon.setAttribute("focusable", "false");
|
||||||
|
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||||
|
path.setAttribute("d", "M4 14v-3a8 8 0 0 1 16 0v3 M4 11H3v7h4v-7H4 M20 11h1v7h-4v-7h3 M20 18v1a2 2 0 0 1-2 2h-6");
|
||||||
|
path.setAttribute("fill", "none");
|
||||||
|
path.setAttribute("stroke", "currentColor");
|
||||||
|
path.setAttribute("stroke-width", "1.7");
|
||||||
|
path.setAttribute("stroke-linecap", "round");
|
||||||
|
path.setAttribute("stroke-linejoin", "round");
|
||||||
|
icon.append(path);
|
||||||
|
button.append(icon);
|
||||||
|
button.addEventListener("click", async () => {
|
||||||
|
const view = openServiceRequestsModal(opportunity, button);
|
||||||
|
const partyNumber = String(opportunity.CustomerAccount?.PartyNumber || "");
|
||||||
|
try {
|
||||||
|
if (!partyNumber) throw new Error("Customer Party Number is unavailable.");
|
||||||
|
if (!pending.has(partyNumber)) {
|
||||||
|
pending.set(partyNumber, load(partyNumber).finally(() => pending.delete(partyNumber)));
|
||||||
|
}
|
||||||
|
view.render(await pending.get(partyNumber));
|
||||||
|
} catch (error) {
|
||||||
|
view.render([], error.message || "Unable to load service requests.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
return { createServiceRequestsButton };
|
||||||
|
}
|
||||||
34
src/features/service-requests/styles.css
Normal file
34
src/features/service-requests/styles.css
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
.opportunities-extension-account-plans-dialog.opportunities-extension-service-requests-dialog { width: min(1700px, 100%); }
|
||||||
|
.opportunities-extension-service-requests-content { min-width: 0; min-height: 0; overflow: auto; scrollbar-gutter: stable; }
|
||||||
|
.opportunities-extension-service-requests-message { margin: 16px; font-size: 13px; }
|
||||||
|
.opportunities-extension-service-requests-table {
|
||||||
|
width: 100%; min-width: 1200px; table-layout: fixed; border-collapse: collapse; font-size: 12px;
|
||||||
|
}
|
||||||
|
.opportunities-extension-service-requests-table col { width: 7%; }
|
||||||
|
.opportunities-extension-service-requests-table .service-requests-col-name { width: 17%; }
|
||||||
|
.opportunities-extension-service-requests-table .service-requests-col-id { width: 10%; }
|
||||||
|
.opportunities-extension-service-requests-table .service-requests-col-severity { width: 5%; }
|
||||||
|
.opportunities-extension-service-requests-table .service-requests-col-subStatus { width: 14%; }
|
||||||
|
.opportunities-extension-service-requests-table .service-requests-col-productName { width: 12%; }
|
||||||
|
.opportunities-extension-service-requests-table th,
|
||||||
|
.opportunities-extension-service-requests-table td {
|
||||||
|
padding: 12px 8px; text-align: left; vertical-align: middle; overflow-wrap: anywhere;
|
||||||
|
border-bottom: 1px solid #e8e5e1; line-height: 1.4;
|
||||||
|
}
|
||||||
|
.opportunities-extension-service-requests-table th { position: sticky; top: 0; z-index: 1; background: #faf9f8; }
|
||||||
|
.opportunities-extension-service-requests-table td[data-field="created"],
|
||||||
|
.opportunities-extension-service-requests-table td[data-field="lastUpdated"],
|
||||||
|
.opportunities-extension-service-requests-table td[data-field="id"] { white-space: nowrap; font-variant-numeric: tabular-nums; }
|
||||||
|
.opportunities-extension-service-request-status {
|
||||||
|
display: inline-block; padding: 3px 6px; border-radius: 12px; font-size: 10px; font-weight: 600;
|
||||||
|
color: #5f5a55; background: #eceae8;
|
||||||
|
}
|
||||||
|
.opportunities-extension-service-request-status[data-status="OPEN"] { color: #17693a; background: #e1f1e5; }
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-service-requests-table th { background: #353331; }
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-service-requests-table th,
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-service-requests-table td { border-color: #484541; }
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-service-request-status { color: #dedbd7; background: #484541; }
|
||||||
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-service-request-status[data-status="OPEN"] { color: #b8e4c6; background: #214d34; }
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.opportunities-extension-service-requests-table .opportunities-extension-account-plans-skeleton-bar { animation: none; }
|
||||||
|
}
|
||||||
121
src/features/service-requests/view.js
Normal file
121
src/features/service-requests/view.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
function createServiceRequestsView({ MODAL_ID, createCloseIcon, formatOracleResponseDate, environment }) {
|
||||||
|
const { document } = environment;
|
||||||
|
let active = null;
|
||||||
|
const columns = [
|
||||||
|
["name", "Name", item => item.customer?.name],
|
||||||
|
["registryId", "Registry ID", item => item.customer?.registryId],
|
||||||
|
["id", "SR Number", item => item.id],
|
||||||
|
["severity", "Severity", item => item.severity],
|
||||||
|
["status", "Status", item => item.status],
|
||||||
|
["subStatus", "Substatus", item => item.subStatus],
|
||||||
|
["productName", "Product Name", item => item.component?.productName],
|
||||||
|
["os", "OS", item => item.os], ["csi", "CSI", item => item.csi],
|
||||||
|
["created", "Creation date", item => item.created],
|
||||||
|
["lastUpdated", "Modified", item => item.lastUpdated]
|
||||||
|
];
|
||||||
|
function element(tag, className, text) {
|
||||||
|
const node = document.createElement(tag);
|
||||||
|
if (className) node.className = className;
|
||||||
|
if (text !== undefined) node.textContent = text;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
function closeServiceRequestsModal() {
|
||||||
|
if (!active) return false;
|
||||||
|
const previous = active;
|
||||||
|
active = null;
|
||||||
|
previous.modal.remove();
|
||||||
|
if (previous.trigger?.isConnected) previous.trigger.focus();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function createTable(items, loading = false) {
|
||||||
|
const table = element("table", "opportunities-extension-service-requests-table");
|
||||||
|
table.setAttribute("aria-label", "Customer service requests");
|
||||||
|
const group = element("colgroup");
|
||||||
|
const head = element("thead");
|
||||||
|
const header = element("tr");
|
||||||
|
columns.forEach(([key, label]) => {
|
||||||
|
group.append(element("col", `service-requests-col-${key}`));
|
||||||
|
const th = element("th", "", label);
|
||||||
|
th.scope = "col";
|
||||||
|
header.append(th);
|
||||||
|
});
|
||||||
|
head.append(header);
|
||||||
|
const body = element("tbody");
|
||||||
|
(loading ? Array.from({ length: 8 }, () => ({})) : items).forEach(item => {
|
||||||
|
const row = element("tr");
|
||||||
|
columns.forEach(([key, , getValue]) => {
|
||||||
|
const cell = element("td");
|
||||||
|
cell.dataset.field = key;
|
||||||
|
if (loading) {
|
||||||
|
const bar = element("span", "opportunities-extension-account-plans-skeleton-bar");
|
||||||
|
bar.setAttribute("aria-hidden", "true");
|
||||||
|
cell.append(bar);
|
||||||
|
} else {
|
||||||
|
const value = getValue(item);
|
||||||
|
let text = value === null || value === undefined || value === "" ? "-" : String(value);
|
||||||
|
if (text !== "-" && (key === "created" || key === "lastUpdated")) text = formatOracleResponseDate(value);
|
||||||
|
if (key === "status") {
|
||||||
|
const badge = element("span", "opportunities-extension-service-request-status", text);
|
||||||
|
badge.dataset.status = text.trim().toUpperCase();
|
||||||
|
cell.append(badge);
|
||||||
|
} else cell.textContent = text;
|
||||||
|
}
|
||||||
|
row.append(cell);
|
||||||
|
});
|
||||||
|
body.append(row);
|
||||||
|
});
|
||||||
|
table.append(group, head, body);
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
function openServiceRequestsModal(opportunity, trigger) {
|
||||||
|
closeServiceRequestsModal();
|
||||||
|
const overlay = document.getElementById(MODAL_ID);
|
||||||
|
if (!overlay || overlay.hidden) return { render() {} };
|
||||||
|
const modal = element("section", "opportunities-extension-account-plans-modal");
|
||||||
|
modal.setAttribute("role", "dialog");
|
||||||
|
modal.setAttribute("aria-modal", "true");
|
||||||
|
modal.setAttribute("aria-busy", "true");
|
||||||
|
modal.setAttribute("aria-labelledby", "opportunities-extension-service-requests-title");
|
||||||
|
const dialog = element("div", "opportunities-extension-account-plans-dialog opportunities-extension-service-requests-dialog");
|
||||||
|
const header = element("header", "opportunities-extension-account-plans-header");
|
||||||
|
const heading = element("div");
|
||||||
|
const title = element("h2", "", "Service Requests");
|
||||||
|
title.id = "opportunities-extension-service-requests-title";
|
||||||
|
heading.append(title, element("p", "", opportunity.CustomerAccount?.PartyUniqueName || "Customer"));
|
||||||
|
const close = element("button", "opportunities-extension-icon-button");
|
||||||
|
close.type = "button";
|
||||||
|
close.title = "Close";
|
||||||
|
close.setAttribute("aria-label", "Close service requests");
|
||||||
|
close.append(createCloseIcon());
|
||||||
|
close.addEventListener("click", closeServiceRequestsModal);
|
||||||
|
header.append(heading, close);
|
||||||
|
const content = element("div", "opportunities-extension-service-requests-content");
|
||||||
|
content.tabIndex = 0;
|
||||||
|
content.setAttribute("role", "region");
|
||||||
|
content.setAttribute("aria-label", "Service requests results");
|
||||||
|
const status = element("p", "opportunities-extension-service-requests-message", "Loading service requests...");
|
||||||
|
status.setAttribute("role", "status");
|
||||||
|
content.append(status, createTable([], true));
|
||||||
|
dialog.append(header, content);
|
||||||
|
modal.append(dialog);
|
||||||
|
overlay.append(modal);
|
||||||
|
active = { modal, trigger };
|
||||||
|
modal.addEventListener("keydown", event => {
|
||||||
|
if (event.key === "Tab") {
|
||||||
|
event.preventDefault();
|
||||||
|
(document.activeElement === close ? content : close).focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
close.focus();
|
||||||
|
return { render(items, error = "") {
|
||||||
|
if (active?.modal !== modal || overlay.hidden) return;
|
||||||
|
modal.setAttribute("aria-busy", "false");
|
||||||
|
content.textContent = "";
|
||||||
|
status.textContent = error || (items.length ? `${items.length} service requests` : "No open service requests found for this customer.");
|
||||||
|
status.setAttribute("role", error ? "alert" : "status");
|
||||||
|
content.append(status);
|
||||||
|
if (!error && items.length) content.append(createTable(items));
|
||||||
|
} };
|
||||||
|
}
|
||||||
|
return { openServiceRequestsModal, closeServiceRequestsModal, createTable };
|
||||||
|
}
|
||||||
60
src/services/spa/customer-service-requests.js
Normal file
60
src/services/spa/customer-service-requests.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
function createSpaCustomerServiceRequests({ ACCOUNT_SUMMARY_URL, fetchJson, findAccountPlanId, normalizeMessageAuthorization }) {
|
||||||
|
const endpoint = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-custsr-provider/service/oscSr/";
|
||||||
|
const filter = encodeURIComponent(JSON.stringify({ hierarchy: "single", status: "open" }));
|
||||||
|
|
||||||
|
async function requestCustomerServiceRequest(message) {
|
||||||
|
const debugRequests = [];
|
||||||
|
try {
|
||||||
|
const partyNumber = String(message.partyNumber || "").trim();
|
||||||
|
const summaryAuth = normalizeMessageAuthorization(message.authorizations?.accountSummary, "account-ti-authorization");
|
||||||
|
const srAuth = normalizeMessageAuthorization(message.authorizations?.customerServiceRequests, "");
|
||||||
|
if (!partyNumber) throw new Error("Customer Party Number is required.");
|
||||||
|
if (!summaryAuth.value) throw new Error("ACCP-TS authorization is unavailable.");
|
||||||
|
if (!srAuth.value) throw new Error("SPA-ORANGE uiaasHeader.value is unavailable.");
|
||||||
|
|
||||||
|
const summary = await fetchJson(ACCOUNT_SUMMARY_URL, {
|
||||||
|
method: "POST", credentials: "include",
|
||||||
|
headers: { Accept: "application/json", "Content-Type": "application/json", [summaryAuth.name]: summaryAuth.value },
|
||||||
|
body: JSON.stringify([{ key: "regId", value: partyNumber }])
|
||||||
|
}, "requestAccountSummary", debugRequests);
|
||||||
|
const accountSummaryId = findAccountPlanId(summary);
|
||||||
|
if ((typeof accountSummaryId !== "string" && typeof accountSummaryId !== "number") || !String(accountSummaryId).trim()) {
|
||||||
|
throw new Error("Account id was not found in requestAccountSummary.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = [];
|
||||||
|
let offset = 0;
|
||||||
|
// This endpoint uses totalResults/count rather than hasMore.
|
||||||
|
for (let page = 0; page < 1000; page += 1) {
|
||||||
|
const url = `${endpoint}${encodeURIComponent(String(accountSummaryId))}?customerSRFilter=${filter}&orderBy=creationDate&orderByMode=desc&limit=40&offset=${offset}`;
|
||||||
|
const result = await fetchJson(url, {
|
||||||
|
method: "GET", credentials: "include",
|
||||||
|
// SPA-ORANGE's label may be lead-auth; this provider requires custsr-auth.
|
||||||
|
headers: { Accept: "application/json", "custsr-auth": srAuth.value }
|
||||||
|
}, "requestCustomerServiceRequest", debugRequests);
|
||||||
|
if (!result || !Array.isArray(result.objects)
|
||||||
|
|| result.objects.some(item => !item || typeof item !== "object" || Array.isArray(item))
|
||||||
|
|| !Number.isSafeInteger(result.totalResults) || result.totalResults < 0
|
||||||
|
|| !Number.isSafeInteger(result.count) || result.count !== result.objects.length) {
|
||||||
|
throw new Error("requestCustomerServiceRequest returned an invalid collection.");
|
||||||
|
}
|
||||||
|
if (result.offset !== offset) throw new Error("requestCustomerServiceRequest returned an unexpected offset.");
|
||||||
|
items.push(...result.objects);
|
||||||
|
offset += result.count;
|
||||||
|
if (offset >= result.totalResults) return { ok: true, items, debugRequests };
|
||||||
|
if (!result.count) throw new Error("requestCustomerServiceRequest pagination made no progress.");
|
||||||
|
}
|
||||||
|
throw new Error("requestCustomerServiceRequest pagination limit reached.");
|
||||||
|
} catch (error) {
|
||||||
|
error.debugRequests = debugRequests;
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
for (const entry of debugRequests) {
|
||||||
|
for (const key of Object.keys(entry.requestHeaders || {})) {
|
||||||
|
if (!/^(accept|content-type)$/i.test(key)) entry.requestHeaders[key] = "(present)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { requestCustomerServiceRequest };
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ function createMessageTypes() {
|
|||||||
requestConsumerTokenService: "opportunitiesExtension.requestConsumerTokenService",
|
requestConsumerTokenService: "opportunitiesExtension.requestConsumerTokenService",
|
||||||
requestAccountPlans: "opportunitiesExtension.requestAccountPlans",
|
requestAccountPlans: "opportunitiesExtension.requestAccountPlans",
|
||||||
requestInstallBase: "opportunitiesExtension.requestInstallBase",
|
requestInstallBase: "opportunitiesExtension.requestInstallBase",
|
||||||
|
requestCustomerServiceRequest: "opportunitiesExtension.requestCustomerServiceRequest",
|
||||||
requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend"
|
requestConsumptionTrend: "opportunitiesExtension.requestConsumptionTrend"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ test("request categories cover every current family and unknown requests stay vi
|
|||||||
requestConsumerTokenService: "authentication", requestCustomerTokenService: "authentication",
|
requestConsumerTokenService: "authentication", requestCustomerTokenService: "authentication",
|
||||||
requestOpportunities: "opportunities", requestOpptyProducts: "products", requestGenerateAiSummary: "ai",
|
requestOpportunities: "opportunities", requestOpptyProducts: "products", requestGenerateAiSummary: "ai",
|
||||||
requestAccountSummary: "consumption", requestAccountPlans: "consumption",
|
requestAccountSummary: "consumption", requestAccountPlans: "consumption",
|
||||||
|
requestInstallBase: "installBase", requestCustomerServiceRequest: "serviceRequests",
|
||||||
requestConsumptionServiceQuarterly: "consumption", requestConsumptionServiceMonthly: "consumption",
|
requestConsumptionServiceQuarterly: "consumption", requestConsumptionServiceMonthly: "consumption",
|
||||||
requestConsumptionServiceWeekly: "consumption", requestConsumptionServiceDaily: "consumption", futureRequest: "other"
|
requestConsumptionServiceWeekly: "consumption", requestConsumptionServiceDaily: "consumption", futureRequest: "other"
|
||||||
})) assert.equal(h.app.debugContentView.getRequestCategory({ name }), category, name);
|
})) assert.equal(h.app.debugContentView.getRequestCategory({ name }), category, name);
|
||||||
@@ -93,7 +94,7 @@ test("tabs support arrows, Home and End with a single selected tab and labelled
|
|||||||
|
|
||||||
test("scoped debug styles are packaged after the unchanged main stylesheet", () => {
|
test("scoped debug styles are packaged after the unchanged main stylesheet", () => {
|
||||||
const root = path.join(__dirname, "..");
|
const root = path.join(__dirname, "..");
|
||||||
const css = ["src/content/styles/redwood.css", "src/features/debug/styles.css", "src/features/opportunities/styles.css", "src/features/install-base/styles.css"]
|
const css = ["src/content/styles/redwood.css", "src/features/debug/styles.css", "src/features/opportunities/styles.css", "src/features/install-base/styles.css", "src/features/service-requests/styles.css"]
|
||||||
.map(file => fs.readFileSync(path.join(root, file), "utf8")).join("\n");
|
.map(file => fs.readFileSync(path.join(root, file), "utf8")).join("\n");
|
||||||
assert.ok(composeScript("content").includes(`const redwoodStyles = ${JSON.stringify(css)};`));
|
assert.ok(composeScript("content").includes(`const redwoodStyles = ${JSON.stringify(css)};`));
|
||||||
assert.ok(!composeScript("background").includes("opportunities-extension-debug-tabs"));
|
assert.ok(!composeScript("background").includes("opportunities-extension-debug-tabs"));
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ test("ten columns retain their links and put product/customer controls below the
|
|||||||
assert.ok(customer.children[0].href.endsWith("party%2Fid"));
|
assert.ok(customer.children[0].href.endsWith("party%2Fid"));
|
||||||
assert.equal(customer.children[1].children[0].title, "View customer consumption");
|
assert.equal(customer.children[1].children[0].title, "View customer consumption");
|
||||||
assert.equal(customer.children[1].children[1].title, "View customer installed base");
|
assert.equal(customer.children[1].children[1].title, "View customer installed base");
|
||||||
|
assert.equal(customer.children[1].children[2].title, "View customer service requests");
|
||||||
assert.equal(findClass(h.cell("optyNumber"), "opportunities-extension-copy-button").title, "Copy opportunity number");
|
assert.equal(findClass(h.cell("optyNumber"), "opportunities-extension-copy-button").title, "Copy opportunity number");
|
||||||
assert.equal(findClass(h.cell("name"), "opportunities-extension-action-button"), undefined);
|
assert.equal(findClass(h.cell("name"), "opportunities-extension-action-button"), undefined);
|
||||||
assert.equal(findClass(h.cell("customer"), "opportunities-extension-products-count"), undefined);
|
assert.equal(findClass(h.cell("customer"), "opportunities-extension-products-count"), undefined);
|
||||||
@@ -146,6 +147,8 @@ test("table styles reserve compact widths, prioritize Name/Customer and keep nes
|
|||||||
assert.match(actions, /position: absolute/);
|
assert.match(actions, /position: absolute/);
|
||||||
assert.match(actions, /bottom: 10px/);
|
assert.match(actions, /bottom: 10px/);
|
||||||
assert.match(actions, /height: 24px/);
|
assert.match(actions, /height: 24px/);
|
||||||
|
assert.match(css, /min-width: 84px/);
|
||||||
|
assert.match(css, /flex: 0 0 24px/);
|
||||||
assert.match(css, /td\[data-column="customer"\] \{\s*position: relative;/);
|
assert.match(css, /td\[data-column="customer"\] \{\s*position: relative;/);
|
||||||
assert.match(css, /grid-template-columns: minmax\(0, 1fr\) 12px/);
|
assert.match(css, /grid-template-columns: minmax\(0, 1fr\) 12px/);
|
||||||
assert.doesNotMatch(css, /text-overflow: ellipsis|overflow: hidden|th:nth-child|td:nth-child/);
|
assert.doesNotMatch(css, /text-overflow: ellipsis|overflow: hidden|th:nth-child|td:nth-child/);
|
||||||
@@ -153,7 +156,8 @@ test("table styles reserve compact widths, prioritize Name/Customer and keep nes
|
|||||||
const remaining = width - fixed;
|
const remaining = width - fixed;
|
||||||
assert.equal(fixed + remaining / 2 + remaining / 2, width);
|
assert.equal(fixed + remaining / 2 + remaining / 2, width);
|
||||||
assert.ok(remaining > 0);
|
assert.ok(remaining > 0);
|
||||||
|
assert.ok(remaining / 2 >= 12 + 84, "three customer controls fit after the left inset");
|
||||||
}
|
}
|
||||||
const allCss = ["src/content/styles/redwood.css", "src/features/debug/styles.css", "src/features/opportunities/styles.css", "src/features/install-base/styles.css"].map(read).join("\n");
|
const allCss = ["src/content/styles/redwood.css", "src/features/debug/styles.css", "src/features/opportunities/styles.css", "src/features/install-base/styles.css", "src/features/service-requests/styles.css"].map(read).join("\n");
|
||||||
assert.ok(composeScript("content").includes(`const redwoodStyles = ${JSON.stringify(allCss)};`));
|
assert.ok(composeScript("content").includes(`const redwoodStyles = ${JSON.stringify(allCss)};`));
|
||||||
});
|
});
|
||||||
|
|||||||
222
tests/serviceRequests.test.cjs
Normal file
222
tests/serviceRequests.test.cjs
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
const assert = require("node:assert/strict");
|
||||||
|
const { test } = require("node:test");
|
||||||
|
const { loadBackground, response, plain } = require("./support/characterization.cjs");
|
||||||
|
const { loadApplication } = require("./support/application.cjs");
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
|
||||||
|
const authorizations = {
|
||||||
|
accountSummary: { name: "Account-Ti-Authorization", value: "synthetic-summary-token" },
|
||||||
|
customerServiceRequests: { name: "lead-auth", value: "synthetic-sr-token" }
|
||||||
|
};
|
||||||
|
const message = { type: "opportunitiesExtension.requestCustomerServiceRequest", partyNumber: "customer-test", authorizations };
|
||||||
|
const tokens = [
|
||||||
|
{ tokenService: "ACCP-TS", uiaasHeader: { label: "Account-Ti-Authorization", value: "synthetic-summary-token" } },
|
||||||
|
{ tokenService: "OCICONS-TS", uiaasHeader: { value: "synthetic-consumption-token" } },
|
||||||
|
{ tokenService: "SAIB-TS", uiaasHeader: { value: "synthetic-install-token" } },
|
||||||
|
{ tokenService: "SPA-ORANGE", uiaasHeader: { label: "lead-auth", value: "synthetic-sr-token" } }
|
||||||
|
];
|
||||||
|
for (const family of ["chrome", "firefox"]) {
|
||||||
|
test(`${family}: SPA-ORANGE selection, summary id (not registryId), GET and pagination`, async () => {
|
||||||
|
const calls = [];
|
||||||
|
const h = loadBackground({ family, fetch: async (url, options) => {
|
||||||
|
calls.push({ url, options });
|
||||||
|
if (url.endsWith("consumerTokenService")) return response(tokens);
|
||||||
|
if (url.endsWith("accountsummary")) return response({ id: "account/id", registryId: "WRONG-ID" });
|
||||||
|
const offset = Number(new URL(url).searchParams.get("offset"));
|
||||||
|
const count = offset === 0 ? 40 : 1;
|
||||||
|
return response({ totalResults: 41, offset, count, limit: 40,
|
||||||
|
objects: Array.from({ length: count }, (_, i) => ({ id: `SR-${offset + i}` })) });
|
||||||
|
} });
|
||||||
|
const auth = await h.dispatch({ type: "opportunitiesExtension.requestConsumerTokenService" });
|
||||||
|
assert.deepEqual(auth.authorizations.customerServiceRequests, { ...authorizations.customerServiceRequests, expires: "" });
|
||||||
|
const result = await h.dispatch({ ...message, authorizations: auth.authorizations });
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(result.items.length, 41);
|
||||||
|
assert.equal(calls[1].options.method, "POST");
|
||||||
|
assert.equal(calls[1].options.headers[authorizations.accountSummary.name], authorizations.accountSummary.value);
|
||||||
|
assert.deepEqual(JSON.parse(calls[1].options.body), [{ key: "regId", value: "customer-test" }]);
|
||||||
|
for (const [index, call] of calls.slice(2).entries()) {
|
||||||
|
const url = new URL(call.url);
|
||||||
|
assert.equal(url.pathname, "/oalcrm/web/api/provider-proxy/g2m-custsr-provider/service/oscSr/account%2Fid");
|
||||||
|
assert.deepEqual(JSON.parse(url.searchParams.get("customerSRFilter")), { hierarchy: "single", status: "open" });
|
||||||
|
assert.equal(url.searchParams.get("orderBy"), "creationDate");
|
||||||
|
assert.equal(url.searchParams.get("orderByMode"), "desc");
|
||||||
|
assert.equal(url.searchParams.get("limit"), "40");
|
||||||
|
assert.equal(url.searchParams.get("offset"), String(index * 40));
|
||||||
|
assert.equal(call.options.method, "GET");
|
||||||
|
assert.equal(call.options.body, undefined);
|
||||||
|
assert.equal(call.options.credentials, "include");
|
||||||
|
assert.deepEqual(plain(call.options.headers), {
|
||||||
|
Accept: "application/json", "custsr-auth": authorizations.customerServiceRequests.value
|
||||||
|
}, "send the raw SPA-ORANGE value in custsr-auth, never lead-auth or Authorization/Bearer");
|
||||||
|
}
|
||||||
|
assert.deepEqual(result.debugRequests.map(entry => entry.name), ["requestAccountSummary", "requestCustomerServiceRequest", "requestCustomerServiceRequest"]);
|
||||||
|
assert.doesNotMatch(JSON.stringify(result.debugRequests), /synthetic-(sr|summary)-token/);
|
||||||
|
assert.ok(result.debugRequests.every(entry => entry.httpStatus === 200));
|
||||||
|
assert.equal(result.debugRequests[1].requestHeaders["custsr-auth"], "(present)");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("SR authentication uses SPA-ORANGE value even when its header label is absent", async () => {
|
||||||
|
let srCalls = 0;
|
||||||
|
const h = loadBackground({ fetch: async (url, options) => {
|
||||||
|
if (url.endsWith("consumerTokenService")) return response(tokens.map(token => token.tokenService === "SPA-ORANGE"
|
||||||
|
? { tokenService: "SPA-ORANGE", uiaasHeader: { value: "synthetic-sr-token" } } : token));
|
||||||
|
if (url.endsWith("accountsummary")) return response({ id: "account-test" });
|
||||||
|
srCalls += 1;
|
||||||
|
assert.deepEqual(plain(options.headers), { Accept: "application/json", "custsr-auth": "synthetic-sr-token" });
|
||||||
|
return response({ totalResults: 0, offset: 0, count: 0, objects: [] });
|
||||||
|
} });
|
||||||
|
const auth = await h.dispatch({ type: "opportunitiesExtension.requestConsumerTokenService" });
|
||||||
|
const result = await h.dispatch({ ...message, authorizations: auth.authorizations });
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(srCalls, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("zero and exact page-size totals stop without an extra request", async () => {
|
||||||
|
for (const total of [0, 40]) {
|
||||||
|
let calls = 0;
|
||||||
|
const h = loadBackground({ fetch: async url => {
|
||||||
|
calls += 1;
|
||||||
|
if (url.endsWith("accountsummary")) return response([{ id: "summary-id" }]);
|
||||||
|
return response({ totalResults: total, offset: 0, count: total, objects: Array.from({ length: total }, (_, i) => ({ id: `SR-${i}` })) });
|
||||||
|
} });
|
||||||
|
const result = await h.dispatch(message);
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(result.items.length, total);
|
||||||
|
assert.equal(calls, 2);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("missing SPA-ORANGE stays optional for existing features and fails only the new action", async () => {
|
||||||
|
const h = loadBackground({ fetch: async () => response(tokens.filter(token => token.tokenService !== "SPA-ORANGE")) });
|
||||||
|
const auth = await h.dispatch({ type: "opportunitiesExtension.requestConsumerTokenService" });
|
||||||
|
assert.equal(auth.ok, true);
|
||||||
|
const result = await h.dispatch({ ...message, authorizations: auth.authorizations });
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.match(result.error, /SPA-ORANGE/);
|
||||||
|
assert.equal(result.debugRequests.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("missing identifiers, stalled pages, malformed collections, network and HTTP failures are visible", async () => {
|
||||||
|
for (const kind of ["id", "stalled", "offset", "count", "total", "objects", "http", "json", "network"]) {
|
||||||
|
const h = loadBackground({ fetch: async url => {
|
||||||
|
if (url.endsWith("accountsummary")) return response(kind === "id" ? { registryId: "wrong" } : { id: "account-id" });
|
||||||
|
if (kind === "http") return response({}, 401);
|
||||||
|
if (kind === "json") return new Response("<html>fallback</html>");
|
||||||
|
if (kind === "network") throw new Error("Synthetic network failure");
|
||||||
|
const page = { totalResults: 1, offset: 0, count: 0, objects: [] };
|
||||||
|
if (kind === "offset") page.offset = 40;
|
||||||
|
if (kind === "count") page.count = 1;
|
||||||
|
if (kind === "total") delete page.totalResults;
|
||||||
|
if (kind === "objects") page.objects = null;
|
||||||
|
return response(page);
|
||||||
|
} });
|
||||||
|
const result = await h.dispatch(message);
|
||||||
|
assert.equal(result.ok, false, kind);
|
||||||
|
assert.ok(result.error, kind);
|
||||||
|
assert.equal(result.debugRequests.length, kind === "id" ? 1 : 2);
|
||||||
|
assert.doesNotMatch(JSON.stringify(result.debugRequests), /synthetic-(sr|summary)-token/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
class Element {
|
||||||
|
constructor(tag, document) { this.tag = tag; this.document = document; this.children = []; this.dataset = {}; this.attributes = {}; this.listeners = {}; }
|
||||||
|
append(...nodes) { nodes.forEach(node => { node.parent = this; }); this.children.push(...nodes); }
|
||||||
|
set textContent(value) { this.text = value; this.children = []; }
|
||||||
|
get textContent() { return this.text || ""; }
|
||||||
|
setAttribute(key, value) { this.attributes[key] = value; }
|
||||||
|
addEventListener(key, fn) { this.listeners[key] = fn; }
|
||||||
|
focus() { this.document.activeElement = this; }
|
||||||
|
remove() { if (this.parent) this.parent.children = this.parent.children.filter(node => node !== this); this.parent = null; }
|
||||||
|
get isConnected() { return Boolean(this.parent); }
|
||||||
|
click() { return this.listeners.click(); }
|
||||||
|
}
|
||||||
|
const walk = node => [node, ...node.children.flatMap(walk)];
|
||||||
|
function setup() {
|
||||||
|
const document = { createElement: tag => new Element(tag, document), createElementNS: (_, tag) => new Element(tag, document),
|
||||||
|
getElementById: id => id === app.contracts.MODAL_ID ? overlay : null };
|
||||||
|
const overlay = new Element("main", document);
|
||||||
|
const app = loadApplication("content", { document, Intl, Date });
|
||||||
|
return { app, overlay, document };
|
||||||
|
}
|
||||||
|
const customer = { CustomerAccount: { PartyNumber: "customer-test", PartyUniqueName: "Synthetic customer" } };
|
||||||
|
|
||||||
|
test("skeleton, exact field mapping, safe text, dates, badge, empty/error and focus states", () => {
|
||||||
|
const { app, overlay, document } = setup();
|
||||||
|
const trigger = new Element("button", document);
|
||||||
|
overlay.append(trigger);
|
||||||
|
const view = app.serviceRequestsView.openServiceRequestsModal(customer, trigger);
|
||||||
|
const modal = overlay.children[1];
|
||||||
|
assert.equal(modal.attributes["aria-busy"], "true");
|
||||||
|
assert.equal(walk(modal).filter(node => node.tag === "td").length, 88);
|
||||||
|
assert.deepEqual(walk(modal).filter(node => node.tag === "th").map(node => node.textContent), [
|
||||||
|
"Name", "Registry ID", "SR Number", "Severity", "Status", "Substatus", "Product Name", "OS", "CSI", "Creation date", "Modified"
|
||||||
|
]);
|
||||||
|
view.render([{ customer: { name: "<script>not HTML</script>", registryId: "reg-1" }, id: "SR-1", severity: "3", status: "Open",
|
||||||
|
subStatus: "Work In Progress", component: { productName: null }, os: ": ", csi: "Unspecified", created: "2026-03-04", lastUpdated: "2026-09-17" }]);
|
||||||
|
assert.equal(modal.attributes["aria-busy"], "false");
|
||||||
|
const cells = walk(modal).filter(node => node.tag === "td");
|
||||||
|
assert.equal(cells.length, 11);
|
||||||
|
assert.equal(cells[0].textContent, "<script>not HTML</script>");
|
||||||
|
assert.equal(cells[0].children.length, 0);
|
||||||
|
assert.equal(cells[1].textContent, "reg-1");
|
||||||
|
assert.equal(cells[2].textContent, "SR-1");
|
||||||
|
assert.equal(cells[4].children[0].dataset.status, "OPEN");
|
||||||
|
assert.equal(cells[5].textContent, "Work In Progress");
|
||||||
|
assert.equal(cells[6].textContent, "-");
|
||||||
|
assert.equal(cells[9].textContent, app.opportunitiesFormatters.formatOracleResponseDate("2026-03-04"));
|
||||||
|
assert.equal(cells[10].textContent, app.opportunitiesFormatters.formatOracleResponseDate("2026-09-17"));
|
||||||
|
modal.listeners.keydown({ key: "Tab", preventDefault() {} });
|
||||||
|
assert.equal(document.activeElement.attributes.role, "region");
|
||||||
|
view.render([]);
|
||||||
|
assert.ok(walk(modal).some(node => node.textContent === "No open service requests found for this customer."));
|
||||||
|
view.render([], "Synthetic failure");
|
||||||
|
assert.ok(walk(modal).some(node => node.attributes.role === "alert" && node.textContent === "Synthetic failure"));
|
||||||
|
assert.equal(app.serviceRequestsView.closeServiceRequestsModal(), true);
|
||||||
|
assert.equal(document.activeElement, trigger);
|
||||||
|
view.render([{ id: "Late" }]);
|
||||||
|
assert.equal(overlay.children.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rapid clicks share the query; different customers and late responses stay isolated", async () => {
|
||||||
|
const { app, overlay } = setup();
|
||||||
|
const requests = [];
|
||||||
|
const logs = [];
|
||||||
|
app.authContentSession.requestConsumerTokenServiceOnce = async () => authorizations;
|
||||||
|
app.platformContentMessages.sendRuntimeMessage = request => new Promise(resolve => requests.push({ request, resolve }));
|
||||||
|
app.debugContentLog.appendBackgroundRequestLog = entries => logs.push(entries);
|
||||||
|
const button = app.serviceRequestsAction.createServiceRequestsButton(customer);
|
||||||
|
const first = button.click();
|
||||||
|
assert.equal(overlay.children[0].attributes["aria-busy"], "true");
|
||||||
|
const repeated = button.click();
|
||||||
|
const otherButton = app.serviceRequestsAction.createServiceRequestsButton({ CustomerAccount: { PartyNumber: "other-customer" } });
|
||||||
|
const second = otherButton.click();
|
||||||
|
await new Promise(resolve => setImmediate(resolve));
|
||||||
|
assert.equal(requests.length, 2);
|
||||||
|
assert.deepEqual(plain(requests[0].request), message);
|
||||||
|
requests[1].resolve({ ok: true, items: [{ id: "Other SR" }], debugRequests: [] });
|
||||||
|
await second;
|
||||||
|
requests[0].resolve({ ok: true, items: [{ id: "Wrong customer" }], debugRequests: [] });
|
||||||
|
await Promise.all([first, repeated]);
|
||||||
|
assert.ok(walk(overlay.children[0]).some(node => node.textContent === "Other SR"));
|
||||||
|
assert.equal(walk(overlay.children[0]).some(node => node.textContent === "Wrong customer"), false);
|
||||||
|
assert.equal(logs.length, 2);
|
||||||
|
const retry = button.click();
|
||||||
|
await new Promise(resolve => setImmediate(resolve));
|
||||||
|
assert.equal(requests.length, 3, "results are not cached after completion");
|
||||||
|
requests[2].resolve({ ok: false, error: "Synthetic request failure", debugRequests: [] });
|
||||||
|
await retry;
|
||||||
|
assert.ok(walk(overlay.children[0]).some(node => node.textContent === "Synthetic request failure"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("styles allocate 100 percent, support dark mode and reduced motion, without hiding values", () => {
|
||||||
|
const css = fs.readFileSync(path.join(__dirname, "../src/features/service-requests/styles.css"), "utf8");
|
||||||
|
const overrides = [...css.matchAll(/\.service-requests-col-\w+ \{ width: (\d+)%; \}/g)].map(match => Number(match[1]));
|
||||||
|
assert.equal(overrides.reduce((a, b) => a + b, 0) + (11 - overrides.length) * 7, 100);
|
||||||
|
assert.match(css, /min-width: 1200px/);
|
||||||
|
assert.match(css, /data-theme="dark"/);
|
||||||
|
assert.match(css, /prefers-reduced-motion/);
|
||||||
|
assert.doesNotMatch(css, /text-overflow: ellipsis/);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user