Optimized requests to retrieve SRs and Actions
This commit is contained in:
21
README.md
21
README.md
@@ -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.
|
||||||
|
|
||||||
|
|||||||
76
dist/chromium/content.js
vendored
76
dist/chromium/content.js
vendored
@@ -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,
|
||||||
|
|||||||
76
dist/firefox/content.js
vendored
76
dist/firefox/content.js
vendored
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
30
src/features/opportunities/product-activities.js
Normal file
30
src/features/opportunities/product-activities.js
Normal 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 };
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
109
tests/productActivities.test.cjs
Normal file
109
tests/productActivities.test.cjs
Normal 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, []);
|
||||||
|
});
|
||||||
@@ -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");
|
||||||
|
|||||||
Reference in New Issue
Block a user