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. other opportunity features continue. Reload the page to refresh identity/roles.
The unused identity token is discarded and redacted from debug responses. 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`, `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` using the existing general tokenrelay Bearer. It uses the Fusion `searchResources/latest`
query endpoint, independently of the opportunity query version. Calls appear in 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, 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. 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. 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 Badges follow Ramp Months. SR results remain in memory only. CRM SR and Actions
in memory only. Reference changes share the in-flight lookup and reuse its result refresh only after actual successful product API requests, not cache reads.
(including failures) without another request. Closing resets this state; reopening Cache-only, empty and entirely failed cycles neither query nor clear activities.
starts a fresh lookup, and responses from previous openings are ignored. 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
Workload Actions are queried in the background after product results arrive, Workload Actions are queried in the background after the product cycle completes,
including cached products. Each distinct `RevnId` uses `requestWlActions` with using only fresh API results. Each distinct `RevnId` uses `requestWlActions` with
the `WWB-TS` credential in `Wwb-Provider-Authorization`. The Act badge beside SR 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 aggregates all products: green means an action's `owner` matches the general
tokenrelay response's `principal`; red means no matching owner was found. `Act ?` tokenrelay response's `principal`; red means no matching owner was found. `Act ?`
means ownership could not be confirmed (including a missing principal). Tooltips means ownership could not be confirmed (including a missing principal). Tooltips
retain all returned actions, regardless of owner. Principal stays in memory only. retain all returned actions, regardless of owner. Principal stays in memory only.
Queries are shared per modal opening (up to four concurrently), reused Queries are shared per product cycle (up to four concurrently). Reference changes
across Reference changes, and discarded on close. Reopening queries again. that load products from cache do not refresh activities.
Only the first page is requested for now; `hasNext` is indicated in the tooltip. 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. The debug panel groups these requests under Actions, with credentials masked.

View File

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

View File

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

View File

@@ -32,6 +32,7 @@ module.exports = {
"src/services/fusion/opportunities.js", "src/services/fusion/opportunities.js",
"src/services/fusion/crm-service-requests.js", "src/services/fusion/crm-service-requests.js",
"src/features/opportunities/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/crm-sr-popover.js",
"src/features/opportunities/workload-actions.js", "src/features/opportunities/workload-actions.js",
"src/features/opportunities/fiscal-periods.js", "src/features/opportunities/fiscal-periods.js",

View File

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

View File

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

View File

@@ -13,7 +13,6 @@ function createOpportunitiesController({
productsContentState, productsContentState,
renderProductsProgress, renderProductsProgress,
requestOpportunitiesPage, requestOpportunitiesPage,
loadCrmServiceRequests,
resetCrmServiceRequests, resetCrmServiceRequests,
resetWorkloadActions, resetWorkloadActions,
requestOpptyProductsInBackground, requestOpptyProductsInBackground,
@@ -84,7 +83,6 @@ function createOpportunitiesController({
} }
try { try {
await loadCrmServiceRequests(token, requestVersion);
if (!isCurrentPeriodRequest(requestVersion)) return; if (!isCurrentPeriodRequest(requestVersion)) return;
const allItems = await requestOpportunitiesPage(token, period, dateRange, 0, [], requestVersion, 1); 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) { function idsFor(products) {
return [...new Set((products?.items || []).map(item => String(item?.RevnId ?? "").trim()).filter(Boolean))]; 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; if (!session.enabled || !allowsCrmActivities()) return;
for (const revnId of idsFor(products)) { 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 }; const record = { status: "loading", actions: [], hasNext: false };
session.records.set(revnId, record); session.records.set(revnId, record);
session.queue.push({ revnId, record }); session.queue.push({ revnId, record });
@@ -36,7 +36,7 @@ function createOpportunityWorkloadActions({ authSessionState, allowsCrmActivitie
async function run(current, { revnId, record }) { async function run(current, { revnId, record }) {
try { try {
const authorizations = await requestConsumerTokenServiceOnce(); 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 }); const result = await sendRuntimeMessage({ type: messageTypes.requestWlActions, revnId, authorization: authorizations?.workloadActions });
if (!current.enabled || current !== session || !allowsCrmActivities()) return; if (!current.enabled || current !== session || !allowsCrmActivities()) return;
appendBackgroundRequestLog(result?.debugRequests); appendBackgroundRequestLog(result?.debugRequests);

View File

@@ -7,7 +7,8 @@ function createProductsContentJobs({
preferencesState, preferencesState,
productsContentState, productsContentState,
renderOpportunitiesTable, renderOpportunitiesTable,
loadWorkloadActions, beginProductActivities,
completeProductActivities,
sendRuntimeMessage, sendRuntimeMessage,
environment environment
}) { }) {
@@ -52,6 +53,7 @@ function createProductsContentJobs({
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2)}`; const requestId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
productsContentState.currentProductsRequestId = requestId; productsContentState.currentProductsRequestId = requestId;
beginProductActivities(requestId, token);
productsContentState.opptyProductsByNumber = new Map(); productsContentState.opptyProductsByNumber = new Map();
productsContentState.expandedProductOptyNumbers = new Set(); productsContentState.expandedProductOptyNumbers = new Set();
productsContentState.productsProgress = { productsContentState.productsProgress = {
@@ -119,7 +121,6 @@ function createProductsContentJobs({
cachedAt: Number(message.cachedAt) || 0, cachedAt: Number(message.cachedAt) || 0,
error: message.error || "" error: message.error || ""
}); });
loadWorkloadActions(productsContentState.opptyProductsByNumber.get(String(message.optyNumber)));
} }
if (Number(message.cachedAt) > productsContentState.productsLastCachedAt) { if (Number(message.cachedAt) > productsContentState.productsLastCachedAt) {
@@ -134,6 +135,7 @@ function createProductsContentJobs({
cacheHits: Number(message.cacheHits) || 0 cacheHits: Number(message.cacheHits) || 0
}; };
renderProductsProgress(); renderProductsProgress();
if (message.state === "complete" || message.state === "complete-with-errors") completeProductActivities(message.requestId);
queueProductsTableRender(); queueProductsTableRender();
return false; 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); 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 calls = [];
const app = setup(async (url, options) => { const app = setup(async (url, options) => {
const payload = JSON.parse(options.body); 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.renderProductsProgress = () => {};
app.productsContentJobs.requestOpptyProductsInBackground = () => {}; app.productsContentJobs.requestOpptyProductsInBackground = () => {};
app.fusionOpportunities.requestOpportunitiesPage = async () => { 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" }]; return [{ OptyNumber: "TEST-1" }];
}; };
await app.opportunitiesController.requestOpportunities("synthetic-token", "Current Quarter", {}, 1); await app.opportunitiesController.requestOpportunities("synthetic-token", "Current Quarter", {}, 1);
assert.equal(states.at(-1).status, "ready"); assert.equal(states.at(-1).status, "ready");
await app.opportunityCrmServiceRequests.load("synthetic-token", 1);
assert.deepEqual(calls.map(call => call.payload.offset), [0, 1]); assert.deepEqual(calls.map(call => call.payload.offset), [0, 1]);
const { payload, options, url } = calls[0]; const { payload, options, url } = calls[0];
assert.equal(url, "https://eeho.fa.us2.oraclecloud.com/crmRestApi/searchResources/latest/custom-actions/queries"); 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 []; }; app.fusionOpportunities.requestOpportunitiesPage = async () => { loaded = true; return []; };
await app.opportunitiesController.requestOpportunities("token", "Current Quarter", {}, 1); await app.opportunitiesController.requestOpportunities("token", "Current Quarter", {}, 1);
assert.equal(loaded, true); assert.equal(loaded, true);
await app.opportunityCrmServiceRequests.load("token", 1);
assert.equal(app.opportunityCrmServiceRequests.createBadge("TEST").textContent, "SR ?"); assert.equal(app.opportunityCrmServiceRequests.createBadge("TEST").textContent, "SR ?");
assert.equal(app.debugState.requestLog.at(-1).status, "failed"); 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()); assert.equal(same, app.currentUser.ensure());
finish({ ok: true, user: { roles } }); finish({ ok: true, user: { roles } });
await first; 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" }] }; const products = { items: [{ RevnId: "123" }] };
app.productsContentState.opptyProductsByNumber.set("OP", products); app.productsContentState.opptyProductsByNumber.set("OP", products);
app.opportunityWorkloadActions.loadProducts(products); app.productActivities.complete("products-cycle");
await new Promise(resolve => setImmediate(resolve)); await new Promise(resolve => setImmediate(resolve));
assert.equal(calls.includes("opportunitiesExtension.requestWlActions"), allowed); assert.equal(calls.includes("opportunitiesExtension.requestWlActions"), allowed);
if (!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); 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; let listener;
const calls = []; const calls = [];
const app = loadApplication("content", { 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 }; return { ok: true, actions: [], hasNext: false };
}; };
app.productsContentState.currentProductsRequestId = "current"; app.productsContentState.currentProductsRequestId = "current";
app.productActivities.begin("current", "synthetic");
app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => [];
app.productsContentJobs.ensureProductsBackgroundMessageHandler(); app.productsContentJobs.ensureProductsBackgroundMessageHandler();
const message = { type: app.messageTypes?.opptyProductsProgress || "opportunitiesExtension.opptyProductsProgress", 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" }); listener({ ...message, requestId: "old" });
await flush(); await flush();
assert.equal(calls.length, 0); assert.equal(calls.length, 0);
listener(message); listener(message);
await flush(); await flush();
assert.equal(calls.length, 0);
listener({ ...message, optyNumber: undefined, state: "complete" });
await flush();
assert.equal(calls.length, 1); assert.equal(calls.length, 1);
assert.equal(calls[0].type, "opportunitiesExtension.requestWlActions"); assert.equal(calls[0].type, "opportunitiesExtension.requestWlActions");
assert.equal(calls[0].revnId, "123"); assert.equal(calls[0].revnId, "123");