diff --git a/dist/chromium/content.js b/dist/chromium/content.js index 03c4f46..7c2424c 100644 --- a/dist/chromium/content.js +++ b/dist/chromium/content.js @@ -3684,6 +3684,7 @@ function createFusionOpportunities({ getOpportunityTypeValues, readResponseBodyForDebug, resetDebugRequests = () => {}, + loadServiceRequests = async () => {}, environment }) { const { fetch } = environment; @@ -3712,7 +3713,13 @@ function createFusionOpportunities({ const payload = createOpportunitiesQueryPayload(dateRange, offset, savedSearchNumber); const requestBody = JSON.stringify(payload); // One history per query, preserving all pages of the same result set. - if (page === 1) resetDebugRequests(); + if (page === 1) { + resetDebugRequests(); + // SR queries do not depend on opportunity or product IDs. Keep their + // diagnostics in this query's history and reuse the loaded SR state later. + try { await loadServiceRequests(token, requestVersion); } catch { /* SR failure must not block opportunities. */ } + if (!isCurrentPeriodRequest(requestVersion)) return null; + } const requestEntry = logRequestStart({ name: "requestOpportunities", method: "POST", @@ -3953,7 +3960,7 @@ function createFusionCrmServiceRequests({ isCurrentPeriodRequest, } // Source: src/features/opportunities/crm-service-requests.js -function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurrentPeriodRequest, bindPopover, closePopover, createSrButton, closeSrForm, allowsCrmActivities, reportProgress, activityCache, setBadgeContent, formatDate, environment }) { +function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurrentPeriodRequest, bindPopover, closePopover, createSrButton, closeSrForm, allowsCrmActivities, reportProgress, activityCache, setBadgeContent, formatDate, environment, onStateChange = () => {} }) { let status = "idle"; let byOpportunity = new Map(); let pending = null; @@ -3975,6 +3982,7 @@ function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurre async function run(token, version, currentGeneration, cacheOptions) { const isActive = () => currentGeneration === generation; status = "loading"; + onStateChange(); try { const cached = cacheOptions ? await activityCache.read("sr", "all", cacheOptions.cachedAt) : null; if (!isActive()) return false; @@ -4000,9 +4008,11 @@ function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurre loadedItems = items; status = "ready"; if (!cached) reportProgress("complete", items.length, items.length); + // Refresh SR badges, filters and totals independently of product/action jobs. + onStateChange(); return true; } catch { - if (isActive()) { status = "error"; reportProgress("error"); } + if (isActive()) { status = "error"; reportProgress("error"); onStateChange(); } return false; } } @@ -11714,6 +11724,7 @@ function createContentApplication(overrides = {}) { setOpportunitiesTableState: (...args) => opportunitiesTable.setOpportunitiesTableState(...args) }); const fusionOpportunities = createFusionOpportunities({ + loadServiceRequests: (token, version) => opportunityCrmServiceRequests.load(token, version), resetDebugRequests: () => debugState.reset(), OPPORTUNITIES_PAGE_LIMIT: contracts.OPPORTUNITIES_PAGE_LIMIT, OPPORTUNITIES_QUERY_URL: contracts.OPPORTUNITIES_QUERY_URL, @@ -11793,6 +11804,9 @@ function createContentApplication(overrides = {}) { requestPillars: () => requestSrFormLookup(messageTypes.requestSrLookupPrimaryPillar) }); const opportunityCrmServiceRequests = createOpportunityCrmServiceRequests({ + onStateChange: () => { + if (environment.document?.getElementById?.(contracts.OPPORTUNITIES_TABLE_ID)) opportunitiesTable.renderOpportunitiesTable(); + }, formatDate: value => opportunitiesFormatters.formatOracleResponseDate(value), createSrButton: number => crmSrForm.createButton(number), closeSrForm: () => { internalSrForm.close(false); crmSrForm.close(false); }, diff --git a/dist/firefox/content.js b/dist/firefox/content.js index 03c4f46..7c2424c 100644 --- a/dist/firefox/content.js +++ b/dist/firefox/content.js @@ -3684,6 +3684,7 @@ function createFusionOpportunities({ getOpportunityTypeValues, readResponseBodyForDebug, resetDebugRequests = () => {}, + loadServiceRequests = async () => {}, environment }) { const { fetch } = environment; @@ -3712,7 +3713,13 @@ function createFusionOpportunities({ const payload = createOpportunitiesQueryPayload(dateRange, offset, savedSearchNumber); const requestBody = JSON.stringify(payload); // One history per query, preserving all pages of the same result set. - if (page === 1) resetDebugRequests(); + if (page === 1) { + resetDebugRequests(); + // SR queries do not depend on opportunity or product IDs. Keep their + // diagnostics in this query's history and reuse the loaded SR state later. + try { await loadServiceRequests(token, requestVersion); } catch { /* SR failure must not block opportunities. */ } + if (!isCurrentPeriodRequest(requestVersion)) return null; + } const requestEntry = logRequestStart({ name: "requestOpportunities", method: "POST", @@ -3953,7 +3960,7 @@ function createFusionCrmServiceRequests({ isCurrentPeriodRequest, } // Source: src/features/opportunities/crm-service-requests.js -function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurrentPeriodRequest, bindPopover, closePopover, createSrButton, closeSrForm, allowsCrmActivities, reportProgress, activityCache, setBadgeContent, formatDate, environment }) { +function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurrentPeriodRequest, bindPopover, closePopover, createSrButton, closeSrForm, allowsCrmActivities, reportProgress, activityCache, setBadgeContent, formatDate, environment, onStateChange = () => {} }) { let status = "idle"; let byOpportunity = new Map(); let pending = null; @@ -3975,6 +3982,7 @@ function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurre async function run(token, version, currentGeneration, cacheOptions) { const isActive = () => currentGeneration === generation; status = "loading"; + onStateChange(); try { const cached = cacheOptions ? await activityCache.read("sr", "all", cacheOptions.cachedAt) : null; if (!isActive()) return false; @@ -4000,9 +4008,11 @@ function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurre loadedItems = items; status = "ready"; if (!cached) reportProgress("complete", items.length, items.length); + // Refresh SR badges, filters and totals independently of product/action jobs. + onStateChange(); return true; } catch { - if (isActive()) { status = "error"; reportProgress("error"); } + if (isActive()) { status = "error"; reportProgress("error"); onStateChange(); } return false; } } @@ -11714,6 +11724,7 @@ function createContentApplication(overrides = {}) { setOpportunitiesTableState: (...args) => opportunitiesTable.setOpportunitiesTableState(...args) }); const fusionOpportunities = createFusionOpportunities({ + loadServiceRequests: (token, version) => opportunityCrmServiceRequests.load(token, version), resetDebugRequests: () => debugState.reset(), OPPORTUNITIES_PAGE_LIMIT: contracts.OPPORTUNITIES_PAGE_LIMIT, OPPORTUNITIES_QUERY_URL: contracts.OPPORTUNITIES_QUERY_URL, @@ -11793,6 +11804,9 @@ function createContentApplication(overrides = {}) { requestPillars: () => requestSrFormLookup(messageTypes.requestSrLookupPrimaryPillar) }); const opportunityCrmServiceRequests = createOpportunityCrmServiceRequests({ + onStateChange: () => { + if (environment.document?.getElementById?.(contracts.OPPORTUNITIES_TABLE_ID)) opportunitiesTable.renderOpportunitiesTable(); + }, formatDate: value => opportunitiesFormatters.formatOracleResponseDate(value), createSrButton: number => crmSrForm.createButton(number), closeSrForm: () => { internalSrForm.close(false); crmSrForm.close(false); }, diff --git a/src/content/application.js b/src/content/application.js index c923b7c..8739d5c 100644 --- a/src/content/application.js +++ b/src/content/application.js @@ -273,6 +273,7 @@ function createContentApplication(overrides = {}) { setOpportunitiesTableState: (...args) => opportunitiesTable.setOpportunitiesTableState(...args) }); const fusionOpportunities = createFusionOpportunities({ + loadServiceRequests: (token, version) => opportunityCrmServiceRequests.load(token, version), resetDebugRequests: () => debugState.reset(), OPPORTUNITIES_PAGE_LIMIT: contracts.OPPORTUNITIES_PAGE_LIMIT, OPPORTUNITIES_QUERY_URL: contracts.OPPORTUNITIES_QUERY_URL, @@ -352,6 +353,9 @@ function createContentApplication(overrides = {}) { requestPillars: () => requestSrFormLookup(messageTypes.requestSrLookupPrimaryPillar) }); const opportunityCrmServiceRequests = createOpportunityCrmServiceRequests({ + onStateChange: () => { + if (environment.document?.getElementById?.(contracts.OPPORTUNITIES_TABLE_ID)) opportunitiesTable.renderOpportunitiesTable(); + }, formatDate: value => opportunitiesFormatters.formatOracleResponseDate(value), createSrButton: number => crmSrForm.createButton(number), closeSrForm: () => { internalSrForm.close(false); crmSrForm.close(false); }, diff --git a/src/features/opportunities/crm-service-requests.js b/src/features/opportunities/crm-service-requests.js index 2471e57..998069a 100644 --- a/src/features/opportunities/crm-service-requests.js +++ b/src/features/opportunities/crm-service-requests.js @@ -1,4 +1,4 @@ -function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurrentPeriodRequest, bindPopover, closePopover, createSrButton, closeSrForm, allowsCrmActivities, reportProgress, activityCache, setBadgeContent, formatDate, environment }) { +function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurrentPeriodRequest, bindPopover, closePopover, createSrButton, closeSrForm, allowsCrmActivities, reportProgress, activityCache, setBadgeContent, formatDate, environment, onStateChange = () => {} }) { let status = "idle"; let byOpportunity = new Map(); let pending = null; @@ -20,6 +20,7 @@ function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurre async function run(token, version, currentGeneration, cacheOptions) { const isActive = () => currentGeneration === generation; status = "loading"; + onStateChange(); try { const cached = cacheOptions ? await activityCache.read("sr", "all", cacheOptions.cachedAt) : null; if (!isActive()) return false; @@ -45,9 +46,11 @@ function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurre loadedItems = items; status = "ready"; if (!cached) reportProgress("complete", items.length, items.length); + // Refresh SR badges, filters and totals independently of product/action jobs. + onStateChange(); return true; } catch { - if (isActive()) { status = "error"; reportProgress("error"); } + if (isActive()) { status = "error"; reportProgress("error"); onStateChange(); } return false; } } diff --git a/src/services/fusion/opportunities.js b/src/services/fusion/opportunities.js index 6288e52..5f1df21 100644 --- a/src/services/fusion/opportunities.js +++ b/src/services/fusion/opportunities.js @@ -9,6 +9,7 @@ function createFusionOpportunities({ getOpportunityTypeValues, readResponseBodyForDebug, resetDebugRequests = () => {}, + loadServiceRequests = async () => {}, environment }) { const { fetch } = environment; @@ -37,7 +38,13 @@ function createFusionOpportunities({ const payload = createOpportunitiesQueryPayload(dateRange, offset, savedSearchNumber); const requestBody = JSON.stringify(payload); // One history per query, preserving all pages of the same result set. - if (page === 1) resetDebugRequests(); + if (page === 1) { + resetDebugRequests(); + // SR queries do not depend on opportunity or product IDs. Keep their + // diagnostics in this query's history and reuse the loaded SR state later. + try { await loadServiceRequests(token, requestVersion); } catch { /* SR failure must not block opportunities. */ } + if (!isCurrentPeriodRequest(requestVersion)) return null; + } const requestEntry = logRequestStart({ name: "requestOpportunities", method: "POST", diff --git a/tests/srRequestPriority.test.cjs b/tests/srRequestPriority.test.cjs new file mode 100644 index 0000000..998220c --- /dev/null +++ b/tests/srRequestPriority.test.cjs @@ -0,0 +1,8 @@ +const {test}=require('node:test');const assert=require('node:assert/strict');const fs=require('node:fs');const vm=require('node:vm'); +const factory=vm.runInNewContext(fs.readFileSync('src/services/fusion/opportunities.js','utf8')+'\ncreateFusionOpportunities'); +for(const mode of ['success','failure','stale'])test(`SR precedes opportunities: ${mode}`,async()=>{ + const order=[];let active=true;let page=0; + const api=factory({OPPORTUNITIES_PAGE_LIMIT:1,OPPORTUNITIES_QUERY_URL:'https://example.invalid',isCurrentPeriodRequest:()=>active,getOpportunityTypeValues:()=>new Set(['ALL']),logRequestStart:()=>({}),logRequestSuccess(){},logRequestFailure(){},readResponseBodyForDebug:async()=>'',resetDebugRequests:()=>order.push('reset'),loadServiceRequests:async()=>{order.push('sr');if(mode==='stale')active=false;if(mode==='failure')throw Error('SR failed');},environment:{fetch:async()=>{order.push('opportunities');page++;return {ok:true,json:async()=>({items:[{OptyNumber:String(page)}],hasMore:page===1,count:1,offset:page-1})};}}}); + await api.requestOpportunitiesPage('token','CQ',{startCloseDate:'2026-09-01',endCloseDate:'2026-12-01'},0,[],1,1,'saved-search'); + assert.deepEqual(order,mode==='stale'?['reset','sr']:['reset','sr','opportunities','opportunities']); +});