Optimized requests to retrieve SRs and Actions

This commit is contained in:
2026-09-25 11:08:02 -03:00
parent 86973f7542
commit 6a3e9e6adf
14 changed files with 294 additions and 84 deletions

View File

@@ -27,7 +27,7 @@ If identity or roles cannot be validated, only these two features stay disabled;
other opportunity features continue. Reload the page to refresh identity/roles.
The unused identity token is discarded and redacted from debug responses.
Once per opening of the opportunities modal, `resquestCrmServiceRequest` fetches all pages of
After a completed product-loading cycle with fresh API results, `resquestCrmServiceRequest` fetches all pages of
`CRMServiceRequest` records with `SVC_PENDING_DELIVERY` and `ORA_SVC_MY_SR_TEAM_SRS`,
using the existing general tokenrelay Bearer. It uses the Fusion `searchResources/latest`
query endpoint, independently of the opportunity query version. Calls appear in
@@ -37,22 +37,25 @@ Records are linked by `OpportunityName_Id_c.OptyNumber`. A green SR badge indica
one or more matches; its tooltip lists each SR number, title, reported hours, owner,
email and contact flags. A red badge indicates no match within that query's scope.
A failed lookup shows `SR ?`, not a false no-match result, and opportunities still load.
Badges follow Ramp Months and do not wait for product downloads. SR results remain
in memory only. Reference changes share the in-flight lookup and reuse its result
(including failures) without another request. Closing resets this state; reopening
starts a fresh lookup, and responses from previous openings are ignored.
Badges follow Ramp Months. SR results remain in memory only. CRM SR and Actions
refresh only after actual successful product API requests, not cache reads.
Cache-only, empty and entirely failed cycles neither query nor clear activities.
In mixed cycles, Actions refreshes only workloads from fresh products and CRM SR
refreshes once. Closing and reopening the modal preserves results and does not
reload products or trigger these lookups. Already-running product requests may
finish in the background and update their related activities.
## Workload Actions
Workload Actions are queried in the background after product results arrive,
including cached products. Each distinct `RevnId` uses `requestWlActions` with
Workload Actions are queried in the background after the product cycle completes,
using only fresh API results. Each distinct `RevnId` uses `requestWlActions` with
the `WWB-TS` credential in `Wwb-Provider-Authorization`. The Act badge beside SR
aggregates all products: green means an action's `owner` matches the general
tokenrelay response's `principal`; red means no matching owner was found. `Act ?`
means ownership could not be confirmed (including a missing principal). Tooltips
retain all returned actions, regardless of owner. Principal stays in memory only.
Queries are shared per modal opening (up to four concurrently), reused
across Reference changes, and discarded on close. Reopening queries again.
Queries are shared per product cycle (up to four concurrently). Reference changes
that load products from cache do not refresh activities.
Only the first page is requested for now; `hasNext` is indicated in the tooltip.
The debug panel groups these requests under Actions, with credentials masked.

View File

@@ -466,10 +466,7 @@ function createContentModal({
renderStageFilterButtons,
renderStatusFilterButtons,
requestOpportunitiesForPeriod,
loadCrmServiceRequests,
resetCrmServiceRequests,
resetWorkloadActions,
reopenWorkloadActions,
closeCrmSrPopover,
requestTokenRelayOnce,
ensureCurrentUser,
resetCustomerAndOwnerFiltersForPeriod,
@@ -503,10 +500,7 @@ function createContentModal({
ensureEscapeKeyHandler();
ensureCurrentUser().then(() => {
if (existingModal.hidden) return;
reopenWorkloadActions();
return requestTokenRelayOnce().then(token => {
if (!existingModal.hidden) return loadCrmServiceRequests(token, opportunitiesQueryState.periodRequestVersion);
});
return requestTokenRelayOnce();
})
.then(() => renderOpportunitiesTable()).catch(() => {});
return;
@@ -884,8 +878,7 @@ function createContentModal({
function closeOpportunitiesModal() {
resetAuthCompletion();
resetWorkloadActions(false);
resetCrmServiceRequests();
closeCrmSrPopover();
const modal = document.getElementById(MODAL_ID);
if (modal) {
@@ -1589,7 +1582,6 @@ function createOpportunitiesController({
productsContentState,
renderProductsProgress,
requestOpportunitiesPage,
loadCrmServiceRequests,
resetCrmServiceRequests,
resetWorkloadActions,
requestOpptyProductsInBackground,
@@ -1660,7 +1652,6 @@ function createOpportunitiesController({
}
try {
await loadCrmServiceRequests(token, requestVersion);
if (!isCurrentPeriodRequest(requestVersion)) return;
const allItems = await requestOpportunitiesPage(token, period, dateRange, 0, [], requestVersion, 1);
@@ -2027,6 +2018,38 @@ function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurre
return { reset, load, createBadge };
}
// Source: src/features/opportunities/product-activities.js
function createProductActivities({ productsContentState, opportunitiesQueryState, resetCrm,
loadCrm, loadActions, render }) {
let cycle = null;
function begin(requestId, token) {
cycle = { requestId, token, version: opportunitiesQueryState.periodRequestVersion, completed: false };
}
function complete(requestId) {
const current = cycle;
if (!current || current.completed || current.requestId !== requestId
|| productsContentState.currentProductsRequestId !== requestId) return;
current.completed = true;
const products = [...productsContentState.opptyProductsByNumber.values()].filter(result => !result.error && !result.fromCache);
if (!products.length) return;
resetCrm();
const seen = new Set();
for (const result of products) {
const items = result.items.filter(item => {
const id = String(item?.RevnId ?? "");
if (seen.has(id)) return false;
seen.add(id);
return true;
});
loadActions({ ...result, items }, true);
}
Promise.resolve(loadCrm(current.token, current.version)).finally(() => {
if (cycle === current && productsContentState.currentProductsRequestId === requestId) render();
}).catch(() => {});
}
return { begin, complete };
}
// Source: src/features/opportunities/crm-sr-popover.js
function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, environment }) {
const { document, window, setTimeout, clearTimeout } = environment;
@@ -2171,10 +2194,10 @@ function createOpportunityWorkloadActions({ authSessionState, allowsCrmActivitie
function idsFor(products) {
return [...new Set((products?.items || []).map(item => String(item?.RevnId ?? "").trim()).filter(Boolean))];
}
function loadProducts(products) {
function loadProducts(products, refresh = false) {
if (!session.enabled || !allowsCrmActivities()) return;
for (const revnId of idsFor(products)) {
if (session.records.has(revnId)) continue;
if (!refresh && session.records.has(revnId)) continue;
const record = { status: "loading", actions: [], hasNext: false };
session.records.set(revnId, record);
session.queue.push({ revnId, record });
@@ -2196,7 +2219,7 @@ function createOpportunityWorkloadActions({ authSessionState, allowsCrmActivitie
async function run(current, { revnId, record }) {
try {
const authorizations = await requestConsumerTokenServiceOnce();
if (!current.enabled || current !== session || !allowsCrmActivities()) return;
if (!current.enabled || current !== session || current.records.get(revnId) !== record || !allowsCrmActivities()) return;
const result = await sendRuntimeMessage({ type: messageTypes.requestWlActions, revnId, authorization: authorizations?.workloadActions });
if (!current.enabled || current !== session || !allowsCrmActivities()) return;
appendBackgroundRequestLog(result?.debugRequests);
@@ -3626,7 +3649,8 @@ function createProductsContentJobs({
preferencesState,
productsContentState,
renderOpportunitiesTable,
loadWorkloadActions,
beginProductActivities,
completeProductActivities,
sendRuntimeMessage,
environment
}) {
@@ -3671,6 +3695,7 @@ function createProductsContentJobs({
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
productsContentState.currentProductsRequestId = requestId;
beginProductActivities(requestId, token);
productsContentState.opptyProductsByNumber = new Map();
productsContentState.expandedProductOptyNumbers = new Set();
productsContentState.productsProgress = {
@@ -3738,7 +3763,6 @@ function createProductsContentJobs({
cachedAt: Number(message.cachedAt) || 0,
error: message.error || ""
});
loadWorkloadActions(productsContentState.opptyProductsByNumber.get(String(message.optyNumber)));
}
if (Number(message.cachedAt) > productsContentState.productsLastCachedAt) {
@@ -3753,6 +3777,7 @@ function createProductsContentJobs({
cacheHits: Number(message.cacheHits) || 0
};
renderProductsProgress();
if (message.state === "complete" || message.state === "complete-with-errors") completeProductActivities(message.requestId);
queueProductsTableRender();
return false;
});
@@ -6897,6 +6922,7 @@ function createContentApplication(overrides = {}) {
environment
});
const contentModal = createContentModal({
closeCrmSrPopover: () => crmSrPopover.close(),
ensureCurrentUser: () => currentUser.ensure(),
ACCOUNT_PLANS_MODAL_ID: contracts.ACCOUNT_PLANS_MODAL_ID,
AUTH_STATUS: contracts.AUTH_STATUS,
@@ -6953,10 +6979,6 @@ function createContentApplication(overrides = {}) {
requestTokenRelayOnce: (...args) => authContentSession.requestTokenRelayOnce(...args),
resetCustomerAndOwnerFiltersForPeriod: (...args) => opportunitiesFilters.resetCustomerAndOwnerFiltersForPeriod(...args),
resetOpportunitiesTable: (...args) => opportunitiesController.resetOpportunitiesTable(...args),
resetWorkloadActions: (...args) => opportunityWorkloadActions.reset(...args),
reopenWorkloadActions: () => opportunityWorkloadActions.reopen(),
loadCrmServiceRequests: (...args) => opportunityCrmServiceRequests.load(...args),
resetCrmServiceRequests: () => opportunityCrmServiceRequests.reset(),
restoreFilterPreferences: (...args) => preferencesStorage.restoreFilterPreferences(...args),
saveFilterPreferences: (...args) => preferencesStorage.saveFilterPreferences(...args),
toggleCustomerFilterPanel: (...args) => opportunitiesFilters.toggleCustomerFilterPanel(...args),
@@ -7032,7 +7054,6 @@ function createContentApplication(overrides = {}) {
productsContentState,
renderProductsProgress: (...args) => productsContentJobs.renderProductsProgress(...args),
requestOpportunitiesPage: (...args) => fusionOpportunities.requestOpportunitiesPage(...args),
loadCrmServiceRequests: (...args) => opportunityCrmServiceRequests.load(...args),
resetCrmServiceRequests: () => opportunityCrmServiceRequests.reset(),
requestOpptyProductsInBackground: (...args) => productsContentJobs.requestOpptyProductsInBackground(...args),
requestTokenRelayOnce: (...args) => authContentSession.requestTokenRelayOnce(...args),
@@ -7172,8 +7193,16 @@ function createContentApplication(overrides = {}) {
requestOpportunitiesForPeriod: (...args) => opportunitiesController.requestOpportunitiesForPeriod(...args),
environment
});
const productActivities = createProductActivities({
productsContentState, opportunitiesQueryState,
resetCrm: () => opportunityCrmServiceRequests.reset(),
loadCrm: (...args) => opportunityCrmServiceRequests.load(...args),
loadActions: (...args) => opportunityWorkloadActions.loadProducts(...args),
render: () => productsContentJobs.queueProductsTableRender()
});
const productsContentJobs = createProductsContentJobs({
loadWorkloadActions: (...args) => opportunityWorkloadActions.loadProducts(...args),
beginProductActivities: (...args) => productActivities.begin(...args),
completeProductActivities: (...args) => productActivities.complete(...args),
appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args),
messageTypes,
PRODUCTS_CACHE_TIMESTAMP_ID: contracts.PRODUCTS_CACHE_TIMESTAMP_ID,
@@ -7395,6 +7424,7 @@ function createContentApplication(overrides = {}) {
preferencesStorage,
preferencesView,
productsContentJobs,
productActivities,
productsDetails,
aiSummaryContent,
consumptionPlansAction,

View File

@@ -466,10 +466,7 @@ function createContentModal({
renderStageFilterButtons,
renderStatusFilterButtons,
requestOpportunitiesForPeriod,
loadCrmServiceRequests,
resetCrmServiceRequests,
resetWorkloadActions,
reopenWorkloadActions,
closeCrmSrPopover,
requestTokenRelayOnce,
ensureCurrentUser,
resetCustomerAndOwnerFiltersForPeriod,
@@ -503,10 +500,7 @@ function createContentModal({
ensureEscapeKeyHandler();
ensureCurrentUser().then(() => {
if (existingModal.hidden) return;
reopenWorkloadActions();
return requestTokenRelayOnce().then(token => {
if (!existingModal.hidden) return loadCrmServiceRequests(token, opportunitiesQueryState.periodRequestVersion);
});
return requestTokenRelayOnce();
})
.then(() => renderOpportunitiesTable()).catch(() => {});
return;
@@ -884,8 +878,7 @@ function createContentModal({
function closeOpportunitiesModal() {
resetAuthCompletion();
resetWorkloadActions(false);
resetCrmServiceRequests();
closeCrmSrPopover();
const modal = document.getElementById(MODAL_ID);
if (modal) {
@@ -1589,7 +1582,6 @@ function createOpportunitiesController({
productsContentState,
renderProductsProgress,
requestOpportunitiesPage,
loadCrmServiceRequests,
resetCrmServiceRequests,
resetWorkloadActions,
requestOpptyProductsInBackground,
@@ -1660,7 +1652,6 @@ function createOpportunitiesController({
}
try {
await loadCrmServiceRequests(token, requestVersion);
if (!isCurrentPeriodRequest(requestVersion)) return;
const allItems = await requestOpportunitiesPage(token, period, dateRange, 0, [], requestVersion, 1);
@@ -2027,6 +2018,38 @@ function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurre
return { reset, load, createBadge };
}
// Source: src/features/opportunities/product-activities.js
function createProductActivities({ productsContentState, opportunitiesQueryState, resetCrm,
loadCrm, loadActions, render }) {
let cycle = null;
function begin(requestId, token) {
cycle = { requestId, token, version: opportunitiesQueryState.periodRequestVersion, completed: false };
}
function complete(requestId) {
const current = cycle;
if (!current || current.completed || current.requestId !== requestId
|| productsContentState.currentProductsRequestId !== requestId) return;
current.completed = true;
const products = [...productsContentState.opptyProductsByNumber.values()].filter(result => !result.error && !result.fromCache);
if (!products.length) return;
resetCrm();
const seen = new Set();
for (const result of products) {
const items = result.items.filter(item => {
const id = String(item?.RevnId ?? "");
if (seen.has(id)) return false;
seen.add(id);
return true;
});
loadActions({ ...result, items }, true);
}
Promise.resolve(loadCrm(current.token, current.version)).finally(() => {
if (cycle === current && productsContentState.currentProductsRequestId === requestId) render();
}).catch(() => {});
}
return { begin, complete };
}
// Source: src/features/opportunities/crm-sr-popover.js
function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, environment }) {
const { document, window, setTimeout, clearTimeout } = environment;
@@ -2171,10 +2194,10 @@ function createOpportunityWorkloadActions({ authSessionState, allowsCrmActivitie
function idsFor(products) {
return [...new Set((products?.items || []).map(item => String(item?.RevnId ?? "").trim()).filter(Boolean))];
}
function loadProducts(products) {
function loadProducts(products, refresh = false) {
if (!session.enabled || !allowsCrmActivities()) return;
for (const revnId of idsFor(products)) {
if (session.records.has(revnId)) continue;
if (!refresh && session.records.has(revnId)) continue;
const record = { status: "loading", actions: [], hasNext: false };
session.records.set(revnId, record);
session.queue.push({ revnId, record });
@@ -2196,7 +2219,7 @@ function createOpportunityWorkloadActions({ authSessionState, allowsCrmActivitie
async function run(current, { revnId, record }) {
try {
const authorizations = await requestConsumerTokenServiceOnce();
if (!current.enabled || current !== session || !allowsCrmActivities()) return;
if (!current.enabled || current !== session || current.records.get(revnId) !== record || !allowsCrmActivities()) return;
const result = await sendRuntimeMessage({ type: messageTypes.requestWlActions, revnId, authorization: authorizations?.workloadActions });
if (!current.enabled || current !== session || !allowsCrmActivities()) return;
appendBackgroundRequestLog(result?.debugRequests);
@@ -3626,7 +3649,8 @@ function createProductsContentJobs({
preferencesState,
productsContentState,
renderOpportunitiesTable,
loadWorkloadActions,
beginProductActivities,
completeProductActivities,
sendRuntimeMessage,
environment
}) {
@@ -3671,6 +3695,7 @@ function createProductsContentJobs({
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
productsContentState.currentProductsRequestId = requestId;
beginProductActivities(requestId, token);
productsContentState.opptyProductsByNumber = new Map();
productsContentState.expandedProductOptyNumbers = new Set();
productsContentState.productsProgress = {
@@ -3738,7 +3763,6 @@ function createProductsContentJobs({
cachedAt: Number(message.cachedAt) || 0,
error: message.error || ""
});
loadWorkloadActions(productsContentState.opptyProductsByNumber.get(String(message.optyNumber)));
}
if (Number(message.cachedAt) > productsContentState.productsLastCachedAt) {
@@ -3753,6 +3777,7 @@ function createProductsContentJobs({
cacheHits: Number(message.cacheHits) || 0
};
renderProductsProgress();
if (message.state === "complete" || message.state === "complete-with-errors") completeProductActivities(message.requestId);
queueProductsTableRender();
return false;
});
@@ -6897,6 +6922,7 @@ function createContentApplication(overrides = {}) {
environment
});
const contentModal = createContentModal({
closeCrmSrPopover: () => crmSrPopover.close(),
ensureCurrentUser: () => currentUser.ensure(),
ACCOUNT_PLANS_MODAL_ID: contracts.ACCOUNT_PLANS_MODAL_ID,
AUTH_STATUS: contracts.AUTH_STATUS,
@@ -6953,10 +6979,6 @@ function createContentApplication(overrides = {}) {
requestTokenRelayOnce: (...args) => authContentSession.requestTokenRelayOnce(...args),
resetCustomerAndOwnerFiltersForPeriod: (...args) => opportunitiesFilters.resetCustomerAndOwnerFiltersForPeriod(...args),
resetOpportunitiesTable: (...args) => opportunitiesController.resetOpportunitiesTable(...args),
resetWorkloadActions: (...args) => opportunityWorkloadActions.reset(...args),
reopenWorkloadActions: () => opportunityWorkloadActions.reopen(),
loadCrmServiceRequests: (...args) => opportunityCrmServiceRequests.load(...args),
resetCrmServiceRequests: () => opportunityCrmServiceRequests.reset(),
restoreFilterPreferences: (...args) => preferencesStorage.restoreFilterPreferences(...args),
saveFilterPreferences: (...args) => preferencesStorage.saveFilterPreferences(...args),
toggleCustomerFilterPanel: (...args) => opportunitiesFilters.toggleCustomerFilterPanel(...args),
@@ -7032,7 +7054,6 @@ function createContentApplication(overrides = {}) {
productsContentState,
renderProductsProgress: (...args) => productsContentJobs.renderProductsProgress(...args),
requestOpportunitiesPage: (...args) => fusionOpportunities.requestOpportunitiesPage(...args),
loadCrmServiceRequests: (...args) => opportunityCrmServiceRequests.load(...args),
resetCrmServiceRequests: () => opportunityCrmServiceRequests.reset(),
requestOpptyProductsInBackground: (...args) => productsContentJobs.requestOpptyProductsInBackground(...args),
requestTokenRelayOnce: (...args) => authContentSession.requestTokenRelayOnce(...args),
@@ -7172,8 +7193,16 @@ function createContentApplication(overrides = {}) {
requestOpportunitiesForPeriod: (...args) => opportunitiesController.requestOpportunitiesForPeriod(...args),
environment
});
const productActivities = createProductActivities({
productsContentState, opportunitiesQueryState,
resetCrm: () => opportunityCrmServiceRequests.reset(),
loadCrm: (...args) => opportunityCrmServiceRequests.load(...args),
loadActions: (...args) => opportunityWorkloadActions.loadProducts(...args),
render: () => productsContentJobs.queueProductsTableRender()
});
const productsContentJobs = createProductsContentJobs({
loadWorkloadActions: (...args) => opportunityWorkloadActions.loadProducts(...args),
beginProductActivities: (...args) => productActivities.begin(...args),
completeProductActivities: (...args) => productActivities.complete(...args),
appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args),
messageTypes,
PRODUCTS_CACHE_TIMESTAMP_ID: contracts.PRODUCTS_CACHE_TIMESTAMP_ID,
@@ -7395,6 +7424,7 @@ function createContentApplication(overrides = {}) {
preferencesStorage,
preferencesView,
productsContentJobs,
productActivities,
productsDetails,
aiSummaryContent,
consumptionPlansAction,

View File

@@ -32,6 +32,7 @@ module.exports = {
"src/services/fusion/opportunities.js",
"src/services/fusion/crm-service-requests.js",
"src/features/opportunities/crm-service-requests.js",
"src/features/opportunities/product-activities.js",
"src/features/opportunities/crm-sr-popover.js",
"src/features/opportunities/workload-actions.js",
"src/features/opportunities/fiscal-periods.js",

View File

@@ -54,6 +54,7 @@ function createContentApplication(overrides = {}) {
environment
});
const contentModal = createContentModal({
closeCrmSrPopover: () => crmSrPopover.close(),
ensureCurrentUser: () => currentUser.ensure(),
ACCOUNT_PLANS_MODAL_ID: contracts.ACCOUNT_PLANS_MODAL_ID,
AUTH_STATUS: contracts.AUTH_STATUS,
@@ -110,10 +111,6 @@ function createContentApplication(overrides = {}) {
requestTokenRelayOnce: (...args) => authContentSession.requestTokenRelayOnce(...args),
resetCustomerAndOwnerFiltersForPeriod: (...args) => opportunitiesFilters.resetCustomerAndOwnerFiltersForPeriod(...args),
resetOpportunitiesTable: (...args) => opportunitiesController.resetOpportunitiesTable(...args),
resetWorkloadActions: (...args) => opportunityWorkloadActions.reset(...args),
reopenWorkloadActions: () => opportunityWorkloadActions.reopen(),
loadCrmServiceRequests: (...args) => opportunityCrmServiceRequests.load(...args),
resetCrmServiceRequests: () => opportunityCrmServiceRequests.reset(),
restoreFilterPreferences: (...args) => preferencesStorage.restoreFilterPreferences(...args),
saveFilterPreferences: (...args) => preferencesStorage.saveFilterPreferences(...args),
toggleCustomerFilterPanel: (...args) => opportunitiesFilters.toggleCustomerFilterPanel(...args),
@@ -189,7 +186,6 @@ function createContentApplication(overrides = {}) {
productsContentState,
renderProductsProgress: (...args) => productsContentJobs.renderProductsProgress(...args),
requestOpportunitiesPage: (...args) => fusionOpportunities.requestOpportunitiesPage(...args),
loadCrmServiceRequests: (...args) => opportunityCrmServiceRequests.load(...args),
resetCrmServiceRequests: () => opportunityCrmServiceRequests.reset(),
requestOpptyProductsInBackground: (...args) => productsContentJobs.requestOpptyProductsInBackground(...args),
requestTokenRelayOnce: (...args) => authContentSession.requestTokenRelayOnce(...args),
@@ -329,8 +325,16 @@ function createContentApplication(overrides = {}) {
requestOpportunitiesForPeriod: (...args) => opportunitiesController.requestOpportunitiesForPeriod(...args),
environment
});
const productActivities = createProductActivities({
productsContentState, opportunitiesQueryState,
resetCrm: () => opportunityCrmServiceRequests.reset(),
loadCrm: (...args) => opportunityCrmServiceRequests.load(...args),
loadActions: (...args) => opportunityWorkloadActions.loadProducts(...args),
render: () => productsContentJobs.queueProductsTableRender()
});
const productsContentJobs = createProductsContentJobs({
loadWorkloadActions: (...args) => opportunityWorkloadActions.loadProducts(...args),
beginProductActivities: (...args) => productActivities.begin(...args),
completeProductActivities: (...args) => productActivities.complete(...args),
appendBackgroundRequestLog: (...args) => debugContentLog.appendBackgroundRequestLog(...args),
messageTypes,
PRODUCTS_CACHE_TIMESTAMP_ID: contracts.PRODUCTS_CACHE_TIMESTAMP_ID,
@@ -552,6 +556,7 @@ function createContentApplication(overrides = {}) {
preferencesStorage,
preferencesView,
productsContentJobs,
productActivities,
productsDetails,
aiSummaryContent,
consumptionPlansAction,

View File

@@ -51,10 +51,7 @@ function createContentModal({
renderStageFilterButtons,
renderStatusFilterButtons,
requestOpportunitiesForPeriod,
loadCrmServiceRequests,
resetCrmServiceRequests,
resetWorkloadActions,
reopenWorkloadActions,
closeCrmSrPopover,
requestTokenRelayOnce,
ensureCurrentUser,
resetCustomerAndOwnerFiltersForPeriod,
@@ -88,10 +85,7 @@ function createContentModal({
ensureEscapeKeyHandler();
ensureCurrentUser().then(() => {
if (existingModal.hidden) return;
reopenWorkloadActions();
return requestTokenRelayOnce().then(token => {
if (!existingModal.hidden) return loadCrmServiceRequests(token, opportunitiesQueryState.periodRequestVersion);
});
return requestTokenRelayOnce();
})
.then(() => renderOpportunitiesTable()).catch(() => {});
return;
@@ -469,8 +463,7 @@ function createContentModal({
function closeOpportunitiesModal() {
resetAuthCompletion();
resetWorkloadActions(false);
resetCrmServiceRequests();
closeCrmSrPopover();
const modal = document.getElementById(MODAL_ID);
if (modal) {

View File

@@ -13,7 +13,6 @@ function createOpportunitiesController({
productsContentState,
renderProductsProgress,
requestOpportunitiesPage,
loadCrmServiceRequests,
resetCrmServiceRequests,
resetWorkloadActions,
requestOpptyProductsInBackground,
@@ -84,7 +83,6 @@ function createOpportunitiesController({
}
try {
await loadCrmServiceRequests(token, requestVersion);
if (!isCurrentPeriodRequest(requestVersion)) return;
const allItems = await requestOpportunitiesPage(token, period, dateRange, 0, [], requestVersion, 1);

View File

@@ -0,0 +1,30 @@
function createProductActivities({ productsContentState, opportunitiesQueryState, resetCrm,
loadCrm, loadActions, render }) {
let cycle = null;
function begin(requestId, token) {
cycle = { requestId, token, version: opportunitiesQueryState.periodRequestVersion, completed: false };
}
function complete(requestId) {
const current = cycle;
if (!current || current.completed || current.requestId !== requestId
|| productsContentState.currentProductsRequestId !== requestId) return;
current.completed = true;
const products = [...productsContentState.opptyProductsByNumber.values()].filter(result => !result.error && !result.fromCache);
if (!products.length) return;
resetCrm();
const seen = new Set();
for (const result of products) {
const items = result.items.filter(item => {
const id = String(item?.RevnId ?? "");
if (seen.has(id)) return false;
seen.add(id);
return true;
});
loadActions({ ...result, items }, true);
}
Promise.resolve(loadCrm(current.token, current.version)).finally(() => {
if (cycle === current && productsContentState.currentProductsRequestId === requestId) render();
}).catch(() => {});
}
return { begin, complete };
}

View File

@@ -11,10 +11,10 @@ function createOpportunityWorkloadActions({ authSessionState, allowsCrmActivitie
function idsFor(products) {
return [...new Set((products?.items || []).map(item => String(item?.RevnId ?? "").trim()).filter(Boolean))];
}
function loadProducts(products) {
function loadProducts(products, refresh = false) {
if (!session.enabled || !allowsCrmActivities()) return;
for (const revnId of idsFor(products)) {
if (session.records.has(revnId)) continue;
if (!refresh && session.records.has(revnId)) continue;
const record = { status: "loading", actions: [], hasNext: false };
session.records.set(revnId, record);
session.queue.push({ revnId, record });
@@ -36,7 +36,7 @@ function createOpportunityWorkloadActions({ authSessionState, allowsCrmActivitie
async function run(current, { revnId, record }) {
try {
const authorizations = await requestConsumerTokenServiceOnce();
if (!current.enabled || current !== session || !allowsCrmActivities()) return;
if (!current.enabled || current !== session || current.records.get(revnId) !== record || !allowsCrmActivities()) return;
const result = await sendRuntimeMessage({ type: messageTypes.requestWlActions, revnId, authorization: authorizations?.workloadActions });
if (!current.enabled || current !== session || !allowsCrmActivities()) return;
appendBackgroundRequestLog(result?.debugRequests);

View File

@@ -7,7 +7,8 @@ function createProductsContentJobs({
preferencesState,
productsContentState,
renderOpportunitiesTable,
loadWorkloadActions,
beginProductActivities,
completeProductActivities,
sendRuntimeMessage,
environment
}) {
@@ -52,6 +53,7 @@ function createProductsContentJobs({
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
productsContentState.currentProductsRequestId = requestId;
beginProductActivities(requestId, token);
productsContentState.opptyProductsByNumber = new Map();
productsContentState.expandedProductOptyNumbers = new Set();
productsContentState.productsProgress = {
@@ -119,7 +121,6 @@ function createProductsContentJobs({
cachedAt: Number(message.cachedAt) || 0,
error: message.error || ""
});
loadWorkloadActions(productsContentState.opptyProductsByNumber.get(String(message.optyNumber)));
}
if (Number(message.cachedAt) > productsContentState.productsLastCachedAt) {
@@ -134,6 +135,7 @@ function createProductsContentJobs({
cacheHits: Number(message.cacheHits) || 0
};
renderProductsProgress();
if (message.state === "complete" || message.state === "complete-with-errors") completeProductActivities(message.requestId);
queueProductsTableRender();
return false;
});

View File

@@ -37,7 +37,7 @@ test("CRM fetch is called without the environment object as its receiver", async
assert.equal(items.length, 2);
});
test("CRM query preserves payload, paginates before opportunities, uses tokenrelay Bearer and logs each page", async () => {
test("CRM query preserves payload and pagination after opportunities, uses tokenrelay Bearer and logs each page", async () => {
const calls = [];
const app = setup(async (url, options) => {
const payload = JSON.parse(options.body);
@@ -49,11 +49,12 @@ test("CRM query preserves payload, paginates before opportunities, uses tokenrel
app.productsContentJobs.renderProductsProgress = () => {};
app.productsContentJobs.requestOpptyProductsInBackground = () => {};
app.fusionOpportunities.requestOpportunitiesPage = async () => {
assert.equal(calls.length, 2, "all CRM pages must finish first");
assert.equal(calls.length, 0, "CRM must not run before product loading");
return [{ OptyNumber: "TEST-1" }];
};
await app.opportunitiesController.requestOpportunities("synthetic-token", "Current Quarter", {}, 1);
assert.equal(states.at(-1).status, "ready");
await app.opportunityCrmServiceRequests.load("synthetic-token", 1);
assert.deepEqual(calls.map(call => call.payload.offset), [0, 1]);
const { payload, options, url } = calls[0];
assert.equal(url, "https://eeho.fa.us2.oraclecloud.com/crmRestApi/searchResources/latest/custom-actions/queries");
@@ -116,6 +117,7 @@ test("CRM failures do not mislabel missing SRs or block the opportunity list", a
app.fusionOpportunities.requestOpportunitiesPage = async () => { loaded = true; return []; };
await app.opportunitiesController.requestOpportunities("token", "Current Quarter", {}, 1);
assert.equal(loaded, true);
await app.opportunityCrmServiceRequests.load("token", 1);
assert.equal(app.opportunityCrmServiceRequests.createBadge("TEST").textContent, "SR ?");
assert.equal(app.debugState.requestLog.at(-1).status, "failed");
}

View File

@@ -73,10 +73,12 @@ for (const [roles, allowed] of [[ ["SALES_REP", "SIMPLE_USER"], false ], [ ["SOL
assert.equal(same, app.currentUser.ensure());
finish({ ok: true, user: { roles } });
await first;
assert.deepEqual(calls, allowed ? [type, "tokenrelay", "crm", "opportunities"] : [type, "tokenrelay", "opportunities"]);
assert.deepEqual(calls, [type, "tokenrelay", "opportunities"]);
app.productsContentState.currentProductsRequestId = "products-cycle";
app.productActivities.begin("products-cycle", "synthetic");
const products = { items: [{ RevnId: "123" }] };
app.productsContentState.opptyProductsByNumber.set("OP", products);
app.opportunityWorkloadActions.loadProducts(products);
app.productActivities.complete("products-cycle");
await new Promise(resolve => setImmediate(resolve));
assert.equal(calls.includes("opportunitiesExtension.requestWlActions"), allowed);
if (!allowed) {

View File

@@ -0,0 +1,109 @@
const assert = require("node:assert/strict");
const { test } = require("node:test");
const { loadApplication } = require("./support/application.cjs");
const flush = () => new Promise(resolve => setImmediate(resolve));
function setup() {
let listener;
const app = loadApplication("content", {
document: { getElementById: () => null }, window: { requestAnimationFrame() {} },
chrome: { runtime: { onMessage: { addListener(fn) { listener = fn; } } } }
});
const calls = [];
app.opportunityCrmServiceRequests.reset = () => calls.push("reset SR");
app.opportunityWorkloadActions.reset = () => calls.push("reset Act");
app.opportunityCrmServiceRequests.load = async token => calls.push(`SR ${token}`);
app.opportunityWorkloadActions.loadProducts = products => calls.push(products.items[0]?.RevnId || "empty products");
app.platformContentMessages.sendRuntimeMessage = async () => ({ ok: true });
app.productsContentJobs.ensureProductsBackgroundMessageHandler();
function start(token = "token") {
app.productsContentJobs.requestOpptyProductsInBackground([{ OptyNumber: "A" }], token);
return app.productsContentState.currentProductsRequestId;
}
function progress(requestId, data) {
listener({ type: "opportunitiesExtension.opptyProductsProgress", requestId, ...data });
}
return { app, calls, start, progress };
}
test("follow-up calls wait for completion, deduplicate terminal messages and refresh on every new cycle", async () => {
const h = setup();
const first = h.start();
h.progress(first, { state: "running", optyNumber: "A", items: [{ RevnId: "123" }], fromCache: false });
assert.deepEqual(h.calls, []);
h.progress(first, { state: "complete" });
h.progress(first, { state: "complete" });
await flush();
assert.deepEqual(h.calls, ["reset SR", "123", "SR token"]);
const second = h.start("new-token");
h.progress(first, { state: "complete", optyNumber: "B", items: [{ RevnId: "old" }] });
h.progress(second, { state: "running", optyNumber: "A", items: [{ RevnId: "123" }] });
assert.equal(h.calls.filter(value => value === "123").length, 1);
h.progress(second, { state: "complete" });
await flush();
assert.equal(h.calls.filter(value => value === "123").length, 2);
assert.ok(h.calls.includes("SR new-token"));
assert.equal(h.calls.includes("old"), false);
});
test("partial failures use successful products only; closed, failed and empty cycles have no follow-up", async () => {
const h = setup();
const first = h.start();
h.progress(first, { optyNumber: "A", items: [{ RevnId: "123" }] });
h.progress(first, { optyNumber: "B", items: [{ RevnId: "failed" }], error: "Synthetic failure" });
h.progress(first, { state: "complete-with-errors" });
await flush();
assert.equal(h.calls.includes("failed"), false);
assert.equal(h.calls.filter(value => value === "SR token").length, 1);
const second = h.start();
h.progress(second, { optyNumber: "A", error: "Synthetic failure" });
h.progress(second, { state: "complete-with-errors" });
const third = h.start();
h.progress(third, { state: "error" });
const fourth = h.start();
h.app.productsContentState.currentProductsRequestId = "";
h.progress(fourth, { state: "complete", optyNumber: "A", items: [{ RevnId: "closed" }] });
h.app.productsContentJobs.requestOpptyProductsInBackground([], "empty");
await flush();
assert.equal(h.calls.filter(value => value.startsWith("SR ")).length, 1);
assert.equal(h.calls.includes("closed"), false);
});
test("cached products never trigger or reset activities; mixed cycles use only fresh products", async () => {
const h = setup();
const cached = h.start();
h.progress(cached, { optyNumber: "A", items: [{ RevnId: "cached" }], fromCache: true });
h.progress(cached, { state: "complete" });
await flush();
assert.deepEqual(h.calls, []);
const mixed = h.start();
h.progress(mixed, { optyNumber: "A", items: [{ RevnId: "cached" }], fromCache: true });
h.progress(mixed, { optyNumber: "B", items: [{ RevnId: "fresh" }], fromCache: false });
h.progress(mixed, { state: "complete" });
await flush();
assert.deepEqual(h.calls, ["reset SR", "fresh", "SR token"]);
});
test("reopening an existing modal does not reload products or reset/query activities", async () => {
const modal = { hidden: true, querySelector: () => ({ focus() {} }) };
const app = loadApplication("content", {
window: {}, document: {
getElementById: id => id === "opportunities-extension-modal" ? modal : null,
documentElement: { classList: { add() {} } }, addEventListener() {}
}
});
app.contentStyles.ensureExtensionStyles = () => {};
app.currentUser.ensure = async () => true;
app.authContentSession.requestTokenRelayOnce = async () => "synthetic";
app.opportunitiesTable.renderOpportunitiesTable = () => {};
const calls = [];
app.productsContentJobs.requestOpptyProductsInBackground = () => calls.push("products");
app.opportunityCrmServiceRequests.load = () => calls.push("SR");
app.opportunityCrmServiceRequests.reset = () => calls.push("reset SR");
app.opportunityWorkloadActions.loadProducts = () => calls.push("Actions");
app.opportunityWorkloadActions.reset = () => calls.push("reset Actions");
app.contentModal.openOpportunitiesModal();
await flush();
assert.equal(modal.hidden, false);
assert.deepEqual(calls, []);
});

View File

@@ -158,7 +158,7 @@ test("actions queue caps concurrency and discards results/queued jobs from close
assert.equal(app.opportunityWorkloadActions.createBadge("A"), null);
});
test("product progress triggers Actions for cached products and ignores stale product jobs", async () => {
test("product progress triggers Actions only for fresh products and ignores stale product jobs", async () => {
let listener;
const calls = [];
const app = loadApplication("content", {
@@ -173,14 +173,19 @@ test("product progress triggers Actions for cached products and ignores stale pr
return { ok: true, actions: [], hasNext: false };
};
app.productsContentState.currentProductsRequestId = "current";
app.productActivities.begin("current", "synthetic");
app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => [];
app.productsContentJobs.ensureProductsBackgroundMessageHandler();
const message = { type: app.messageTypes?.opptyProductsProgress || "opportunitiesExtension.opptyProductsProgress",
requestId: "current", optyNumber: "A", items: [{ RevnId: "123" }], fromCache: true };
requestId: "current", optyNumber: "A", items: [{ RevnId: "123" }], fromCache: false };
listener({ ...message, requestId: "old" });
await flush();
assert.equal(calls.length, 0);
listener(message);
await flush();
assert.equal(calls.length, 0);
listener({ ...message, optyNumber: undefined, state: "complete" });
await flush();
assert.equal(calls.length, 1);
assert.equal(calls[0].type, "opportunitiesExtension.requestWlActions");
assert.equal(calls[0].revnId, "123");