Some visual adjustments to the badges
This commit is contained in:
22
dist/chromium/content.js
vendored
22
dist/chromium/content.js
vendored
@@ -1692,6 +1692,7 @@ function createOpportunitiesController({
|
|||||||
async function requestOpportunitiesForPeriod(period) {
|
async function requestOpportunitiesForPeriod(period) {
|
||||||
const dateRange = getFiscalPeriodRange(period, new Date());
|
const dateRange = getFiscalPeriodRange(period, new Date());
|
||||||
const requestVersion = ++opportunitiesQueryState.periodRequestVersion;
|
const requestVersion = ++opportunitiesQueryState.periodRequestVersion;
|
||||||
|
resetWorkloadActions();
|
||||||
await ensureCurrentUser();
|
await ensureCurrentUser();
|
||||||
if (!isCurrentPeriodRequest(requestVersion)) return;
|
if (!isCurrentPeriodRequest(requestVersion)) return;
|
||||||
|
|
||||||
@@ -2158,20 +2159,22 @@ function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurre
|
|||||||
function createProductActivities({ productsContentState, opportunitiesQueryState,
|
function createProductActivities({ productsContentState, opportunitiesQueryState,
|
||||||
loadCrm, loadActions, blockActions, render }) {
|
loadCrm, loadActions, blockActions, render }) {
|
||||||
let cycle = null;
|
let cycle = null;
|
||||||
function begin(requestId, token) {
|
function begin(requestId, token, opportunityNumbers = null) {
|
||||||
cycle = { requestId, token, version: opportunitiesQueryState.periodRequestVersion, completed: false };
|
cycle = { requestId, token, numbers: opportunityNumbers && new Set(opportunityNumbers.map(String)), version: opportunitiesQueryState.periodRequestVersion, completed: false };
|
||||||
}
|
}
|
||||||
function complete(requestId) {
|
function complete(requestId) {
|
||||||
const current = cycle;
|
const current = cycle;
|
||||||
if (!current || current.completed || current.requestId !== requestId
|
if (!current || current.completed || current.requestId !== requestId
|
||||||
|| productsContentState.currentProductsRequestId !== requestId) return;
|
|| productsContentState.currentProductsRequestId !== requestId) return;
|
||||||
current.completed = true;
|
current.completed = true;
|
||||||
const products = [...productsContentState.opptyProductsByNumber.values()].filter(result => !result.error);
|
const entries = [...productsContentState.opptyProductsByNumber].filter(([number]) => !current.numbers || current.numbers.has(String(number)));
|
||||||
|
const products = entries.map(([, result]) => result).filter(result => !result.error);
|
||||||
if (!products.length) return;
|
if (!products.length) return;
|
||||||
const cachedAt = Math.max(...products.map(result => Number(result.cachedAt) || 0));
|
const cachedAt = Math.max(...products.map(result => Number(result.cachedAt) || 0));
|
||||||
const isActive = () => cycle === current && productsContentState.currentProductsRequestId === requestId
|
const isActive = () => cycle === current && productsContentState.currentProductsRequestId === requestId
|
||||||
&& opportunitiesQueryState.periodRequestVersion === current.version;
|
&& opportunitiesQueryState.periodRequestVersion === current.version;
|
||||||
return Promise.resolve(loadCrm(current.token, current.version, { cachedAt, cacheOnly: products.every(result => result.fromCache) })).then(async success => {
|
// Product cache hits do not guarantee that the corresponding activity cache exists.
|
||||||
|
return Promise.resolve(loadCrm(current.token, current.version, { cachedAt, cacheOnly: false })).then(async success => {
|
||||||
if (!isActive()) return;
|
if (!isActive()) return;
|
||||||
if (success === false) {
|
if (success === false) {
|
||||||
products.forEach(blockActions);
|
products.forEach(blockActions);
|
||||||
@@ -2187,7 +2190,7 @@ function createProductActivities({ productsContentState, opportunitiesQueryState
|
|||||||
seen.add(id);
|
seen.add(id);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
pending.push(loadActions({ ...result, items }, true, { cachedAt: result.cachedAt, cacheOnly: result.fromCache === true }));
|
pending.push(loadActions({ ...result, items }, true, { cachedAt: result.cachedAt, cacheOnly: false }));
|
||||||
}
|
}
|
||||||
await Promise.allSettled(pending);
|
await Promise.allSettled(pending);
|
||||||
if (!isActive()) return;
|
if (!isActive()) return;
|
||||||
@@ -2349,7 +2352,7 @@ function createActivityProgress({ MODAL_ID, environment }) {
|
|||||||
: state === "complete-with-errors" ? `${label} ${count} (errors)`
|
: state === "complete-with-errors" ? `${label} ${count} (errors)`
|
||||||
: state === "complete" ? `${label} ready ${completed}` : `${label} ${count}`;
|
: state === "complete" ? `${label} ready ${completed}` : `${label} ${count}`;
|
||||||
if (key === "session") entry.badge.textContent = state === "complete" ? "SR session ready" : state === "error" ? "SR session unavailable" : "SR session loading";
|
if (key === "session") entry.badge.textContent = state === "complete" ? "SR session ready" : state === "error" ? "SR session unavailable" : "SR session loading";
|
||||||
entry.badge.title = key === "session" ? "Service Request session initialization" : key === "sr" ? "CRM service request records loaded" : "Workload action requests completed";
|
entry.badge.title = key === "session" ? "Service Request session initialization" : key === "sr" ? "CRM service request records loaded" : "Product workloads checked for Actions in the selected Reference, regardless of display filters";
|
||||||
if (state === "complete") entry.timer = setTimeout(() => {
|
if (state === "complete") entry.timer = setTimeout(() => {
|
||||||
entry.badge.setAttribute("data-fading", "true");
|
entry.badge.setAttribute("data-fading", "true");
|
||||||
const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||||
@@ -3124,6 +3127,7 @@ function createOpportunityWorkloadActions({ authSessionState, allowsCrmActivitie
|
|||||||
for (const id of idsFor(products)) {
|
for (const id of idsFor(products)) {
|
||||||
session.records.set(id, { status: "error", actions: [], hasNext: false, error: "CRM service requests unavailable." });
|
session.records.set(id, { status: "error", actions: [], hasNext: false, error: "CRM service requests unavailable." });
|
||||||
}
|
}
|
||||||
|
updateProgress(session);
|
||||||
}
|
}
|
||||||
function pump(current) {
|
function pump(current) {
|
||||||
while (current.enabled && current === session && current.running < 4 && current.queue.length) {
|
while (current.enabled && current === session && current.running < 4 && current.queue.length) {
|
||||||
@@ -3232,6 +3236,8 @@ function createOpportunityWorkloadActions({ authSessionState, allowsCrmActivitie
|
|||||||
].join("\n")).join("\n\n") : state === "missing" ? "No actions found for this opportunity's products."
|
].join("\n")).join("\n\n") : state === "missing" ? "No actions found for this opportunity's products."
|
||||||
: state === "loading" ? "Loading workload actions..." : "Unable to confirm workload actions. Check product RevnId and Debug Requests.";
|
: state === "loading" ? "Loading workload actions..." : "Unable to confirm workload actions. Check product RevnId and Debug Requests.";
|
||||||
if (records.some(record => record?.hasNext)) badge.title += "\nAdditional actions exist; pagination is not enabled.";
|
if (records.some(record => record?.hasNext)) badge.title += "\nAdditional actions exist; pagination is not enabled.";
|
||||||
|
const errors = [...new Set(records.filter(record => record?.error).map(record => record.error))];
|
||||||
|
if (errors.length) badge.title += `\n${errors.join("\n")}`;
|
||||||
if (!principal) badge.title += "\nTokenrelay principal is unavailable; ownership cannot be verified.";
|
if (!principal) badge.title += "\nTokenrelay principal is unavailable; ownership cannot be verified.";
|
||||||
else if (actions.length && state === "missing") badge.title += "\nNone of the returned actions belong to the signed-in principal.";
|
else if (actions.length && state === "missing") badge.title += "\nNone of the returned actions belong to the signed-in principal.";
|
||||||
if (actions.length && (pending || uncertain)) badge.title += "\nSome workloads are still loading or could not be fully checked.";
|
if (actions.length && (pending || uncertain)) badge.title += "\nSome workloads are still loading or could not be fully checked.";
|
||||||
@@ -5004,7 +5010,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);
|
beginProductActivities(requestId, token, optyNumbers);
|
||||||
productsContentState.opptyProductsByNumber = new Map();
|
productsContentState.opptyProductsByNumber = new Map();
|
||||||
productsContentState.expandedProductOptyNumbers = new Set();
|
productsContentState.expandedProductOptyNumbers = new Set();
|
||||||
productsContentState.productsProgress = {
|
productsContentState.productsProgress = {
|
||||||
@@ -5175,7 +5181,7 @@ function createProductsDetails({
|
|||||||
const badge = document.createElement("span");
|
const badge = document.createElement("span");
|
||||||
badge.className = "opportunities-extension-products-ramp";
|
badge.className = "opportunities-extension-products-ramp";
|
||||||
badge.setAttribute("data-missing", String(values.includes("N/D")));
|
badge.setAttribute("data-missing", String(values.includes("N/D")));
|
||||||
setBadgeContent(badge, "ramp", values.join(","));
|
setBadgeContent(badge, "ramp", values.join(" | "));
|
||||||
badge.title = `Ramp Months: ${values.join(", ")}`;
|
badge.title = `Ramp Months: ${values.join(", ")}`;
|
||||||
badge.setAttribute("aria-label", badge.title);
|
badge.setAttribute("aria-label", badge.title);
|
||||||
return badge;
|
return badge;
|
||||||
|
|||||||
22
dist/firefox/content.js
vendored
22
dist/firefox/content.js
vendored
@@ -1692,6 +1692,7 @@ function createOpportunitiesController({
|
|||||||
async function requestOpportunitiesForPeriod(period) {
|
async function requestOpportunitiesForPeriod(period) {
|
||||||
const dateRange = getFiscalPeriodRange(period, new Date());
|
const dateRange = getFiscalPeriodRange(period, new Date());
|
||||||
const requestVersion = ++opportunitiesQueryState.periodRequestVersion;
|
const requestVersion = ++opportunitiesQueryState.periodRequestVersion;
|
||||||
|
resetWorkloadActions();
|
||||||
await ensureCurrentUser();
|
await ensureCurrentUser();
|
||||||
if (!isCurrentPeriodRequest(requestVersion)) return;
|
if (!isCurrentPeriodRequest(requestVersion)) return;
|
||||||
|
|
||||||
@@ -2158,20 +2159,22 @@ function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurre
|
|||||||
function createProductActivities({ productsContentState, opportunitiesQueryState,
|
function createProductActivities({ productsContentState, opportunitiesQueryState,
|
||||||
loadCrm, loadActions, blockActions, render }) {
|
loadCrm, loadActions, blockActions, render }) {
|
||||||
let cycle = null;
|
let cycle = null;
|
||||||
function begin(requestId, token) {
|
function begin(requestId, token, opportunityNumbers = null) {
|
||||||
cycle = { requestId, token, version: opportunitiesQueryState.periodRequestVersion, completed: false };
|
cycle = { requestId, token, numbers: opportunityNumbers && new Set(opportunityNumbers.map(String)), version: opportunitiesQueryState.periodRequestVersion, completed: false };
|
||||||
}
|
}
|
||||||
function complete(requestId) {
|
function complete(requestId) {
|
||||||
const current = cycle;
|
const current = cycle;
|
||||||
if (!current || current.completed || current.requestId !== requestId
|
if (!current || current.completed || current.requestId !== requestId
|
||||||
|| productsContentState.currentProductsRequestId !== requestId) return;
|
|| productsContentState.currentProductsRequestId !== requestId) return;
|
||||||
current.completed = true;
|
current.completed = true;
|
||||||
const products = [...productsContentState.opptyProductsByNumber.values()].filter(result => !result.error);
|
const entries = [...productsContentState.opptyProductsByNumber].filter(([number]) => !current.numbers || current.numbers.has(String(number)));
|
||||||
|
const products = entries.map(([, result]) => result).filter(result => !result.error);
|
||||||
if (!products.length) return;
|
if (!products.length) return;
|
||||||
const cachedAt = Math.max(...products.map(result => Number(result.cachedAt) || 0));
|
const cachedAt = Math.max(...products.map(result => Number(result.cachedAt) || 0));
|
||||||
const isActive = () => cycle === current && productsContentState.currentProductsRequestId === requestId
|
const isActive = () => cycle === current && productsContentState.currentProductsRequestId === requestId
|
||||||
&& opportunitiesQueryState.periodRequestVersion === current.version;
|
&& opportunitiesQueryState.periodRequestVersion === current.version;
|
||||||
return Promise.resolve(loadCrm(current.token, current.version, { cachedAt, cacheOnly: products.every(result => result.fromCache) })).then(async success => {
|
// Product cache hits do not guarantee that the corresponding activity cache exists.
|
||||||
|
return Promise.resolve(loadCrm(current.token, current.version, { cachedAt, cacheOnly: false })).then(async success => {
|
||||||
if (!isActive()) return;
|
if (!isActive()) return;
|
||||||
if (success === false) {
|
if (success === false) {
|
||||||
products.forEach(blockActions);
|
products.forEach(blockActions);
|
||||||
@@ -2187,7 +2190,7 @@ function createProductActivities({ productsContentState, opportunitiesQueryState
|
|||||||
seen.add(id);
|
seen.add(id);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
pending.push(loadActions({ ...result, items }, true, { cachedAt: result.cachedAt, cacheOnly: result.fromCache === true }));
|
pending.push(loadActions({ ...result, items }, true, { cachedAt: result.cachedAt, cacheOnly: false }));
|
||||||
}
|
}
|
||||||
await Promise.allSettled(pending);
|
await Promise.allSettled(pending);
|
||||||
if (!isActive()) return;
|
if (!isActive()) return;
|
||||||
@@ -2349,7 +2352,7 @@ function createActivityProgress({ MODAL_ID, environment }) {
|
|||||||
: state === "complete-with-errors" ? `${label} ${count} (errors)`
|
: state === "complete-with-errors" ? `${label} ${count} (errors)`
|
||||||
: state === "complete" ? `${label} ready ${completed}` : `${label} ${count}`;
|
: state === "complete" ? `${label} ready ${completed}` : `${label} ${count}`;
|
||||||
if (key === "session") entry.badge.textContent = state === "complete" ? "SR session ready" : state === "error" ? "SR session unavailable" : "SR session loading";
|
if (key === "session") entry.badge.textContent = state === "complete" ? "SR session ready" : state === "error" ? "SR session unavailable" : "SR session loading";
|
||||||
entry.badge.title = key === "session" ? "Service Request session initialization" : key === "sr" ? "CRM service request records loaded" : "Workload action requests completed";
|
entry.badge.title = key === "session" ? "Service Request session initialization" : key === "sr" ? "CRM service request records loaded" : "Product workloads checked for Actions in the selected Reference, regardless of display filters";
|
||||||
if (state === "complete") entry.timer = setTimeout(() => {
|
if (state === "complete") entry.timer = setTimeout(() => {
|
||||||
entry.badge.setAttribute("data-fading", "true");
|
entry.badge.setAttribute("data-fading", "true");
|
||||||
const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||||
@@ -3124,6 +3127,7 @@ function createOpportunityWorkloadActions({ authSessionState, allowsCrmActivitie
|
|||||||
for (const id of idsFor(products)) {
|
for (const id of idsFor(products)) {
|
||||||
session.records.set(id, { status: "error", actions: [], hasNext: false, error: "CRM service requests unavailable." });
|
session.records.set(id, { status: "error", actions: [], hasNext: false, error: "CRM service requests unavailable." });
|
||||||
}
|
}
|
||||||
|
updateProgress(session);
|
||||||
}
|
}
|
||||||
function pump(current) {
|
function pump(current) {
|
||||||
while (current.enabled && current === session && current.running < 4 && current.queue.length) {
|
while (current.enabled && current === session && current.running < 4 && current.queue.length) {
|
||||||
@@ -3232,6 +3236,8 @@ function createOpportunityWorkloadActions({ authSessionState, allowsCrmActivitie
|
|||||||
].join("\n")).join("\n\n") : state === "missing" ? "No actions found for this opportunity's products."
|
].join("\n")).join("\n\n") : state === "missing" ? "No actions found for this opportunity's products."
|
||||||
: state === "loading" ? "Loading workload actions..." : "Unable to confirm workload actions. Check product RevnId and Debug Requests.";
|
: state === "loading" ? "Loading workload actions..." : "Unable to confirm workload actions. Check product RevnId and Debug Requests.";
|
||||||
if (records.some(record => record?.hasNext)) badge.title += "\nAdditional actions exist; pagination is not enabled.";
|
if (records.some(record => record?.hasNext)) badge.title += "\nAdditional actions exist; pagination is not enabled.";
|
||||||
|
const errors = [...new Set(records.filter(record => record?.error).map(record => record.error))];
|
||||||
|
if (errors.length) badge.title += `\n${errors.join("\n")}`;
|
||||||
if (!principal) badge.title += "\nTokenrelay principal is unavailable; ownership cannot be verified.";
|
if (!principal) badge.title += "\nTokenrelay principal is unavailable; ownership cannot be verified.";
|
||||||
else if (actions.length && state === "missing") badge.title += "\nNone of the returned actions belong to the signed-in principal.";
|
else if (actions.length && state === "missing") badge.title += "\nNone of the returned actions belong to the signed-in principal.";
|
||||||
if (actions.length && (pending || uncertain)) badge.title += "\nSome workloads are still loading or could not be fully checked.";
|
if (actions.length && (pending || uncertain)) badge.title += "\nSome workloads are still loading or could not be fully checked.";
|
||||||
@@ -5004,7 +5010,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);
|
beginProductActivities(requestId, token, optyNumbers);
|
||||||
productsContentState.opptyProductsByNumber = new Map();
|
productsContentState.opptyProductsByNumber = new Map();
|
||||||
productsContentState.expandedProductOptyNumbers = new Set();
|
productsContentState.expandedProductOptyNumbers = new Set();
|
||||||
productsContentState.productsProgress = {
|
productsContentState.productsProgress = {
|
||||||
@@ -5175,7 +5181,7 @@ function createProductsDetails({
|
|||||||
const badge = document.createElement("span");
|
const badge = document.createElement("span");
|
||||||
badge.className = "opportunities-extension-products-ramp";
|
badge.className = "opportunities-extension-products-ramp";
|
||||||
badge.setAttribute("data-missing", String(values.includes("N/D")));
|
badge.setAttribute("data-missing", String(values.includes("N/D")));
|
||||||
setBadgeContent(badge, "ramp", values.join(","));
|
setBadgeContent(badge, "ramp", values.join(" | "));
|
||||||
badge.title = `Ramp Months: ${values.join(", ")}`;
|
badge.title = `Ramp Months: ${values.join(", ")}`;
|
||||||
badge.setAttribute("aria-label", badge.title);
|
badge.setAttribute("aria-label", badge.title);
|
||||||
return badge;
|
return badge;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function createActivityProgress({ MODAL_ID, environment }) {
|
|||||||
: state === "complete-with-errors" ? `${label} ${count} (errors)`
|
: state === "complete-with-errors" ? `${label} ${count} (errors)`
|
||||||
: state === "complete" ? `${label} ready ${completed}` : `${label} ${count}`;
|
: state === "complete" ? `${label} ready ${completed}` : `${label} ${count}`;
|
||||||
if (key === "session") entry.badge.textContent = state === "complete" ? "SR session ready" : state === "error" ? "SR session unavailable" : "SR session loading";
|
if (key === "session") entry.badge.textContent = state === "complete" ? "SR session ready" : state === "error" ? "SR session unavailable" : "SR session loading";
|
||||||
entry.badge.title = key === "session" ? "Service Request session initialization" : key === "sr" ? "CRM service request records loaded" : "Workload action requests completed";
|
entry.badge.title = key === "session" ? "Service Request session initialization" : key === "sr" ? "CRM service request records loaded" : "Product workloads checked for Actions in the selected Reference, regardless of display filters";
|
||||||
if (state === "complete") entry.timer = setTimeout(() => {
|
if (state === "complete") entry.timer = setTimeout(() => {
|
||||||
entry.badge.setAttribute("data-fading", "true");
|
entry.badge.setAttribute("data-fading", "true");
|
||||||
const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ function createOpportunitiesController({
|
|||||||
async function requestOpportunitiesForPeriod(period) {
|
async function requestOpportunitiesForPeriod(period) {
|
||||||
const dateRange = getFiscalPeriodRange(period, new Date());
|
const dateRange = getFiscalPeriodRange(period, new Date());
|
||||||
const requestVersion = ++opportunitiesQueryState.periodRequestVersion;
|
const requestVersion = ++opportunitiesQueryState.periodRequestVersion;
|
||||||
|
resetWorkloadActions();
|
||||||
await ensureCurrentUser();
|
await ensureCurrentUser();
|
||||||
if (!isCurrentPeriodRequest(requestVersion)) return;
|
if (!isCurrentPeriodRequest(requestVersion)) return;
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
function createProductActivities({ productsContentState, opportunitiesQueryState,
|
function createProductActivities({ productsContentState, opportunitiesQueryState,
|
||||||
loadCrm, loadActions, blockActions, render }) {
|
loadCrm, loadActions, blockActions, render }) {
|
||||||
let cycle = null;
|
let cycle = null;
|
||||||
function begin(requestId, token) {
|
function begin(requestId, token, opportunityNumbers = null) {
|
||||||
cycle = { requestId, token, version: opportunitiesQueryState.periodRequestVersion, completed: false };
|
cycle = { requestId, token, numbers: opportunityNumbers && new Set(opportunityNumbers.map(String)), version: opportunitiesQueryState.periodRequestVersion, completed: false };
|
||||||
}
|
}
|
||||||
function complete(requestId) {
|
function complete(requestId) {
|
||||||
const current = cycle;
|
const current = cycle;
|
||||||
if (!current || current.completed || current.requestId !== requestId
|
if (!current || current.completed || current.requestId !== requestId
|
||||||
|| productsContentState.currentProductsRequestId !== requestId) return;
|
|| productsContentState.currentProductsRequestId !== requestId) return;
|
||||||
current.completed = true;
|
current.completed = true;
|
||||||
const products = [...productsContentState.opptyProductsByNumber.values()].filter(result => !result.error);
|
const entries = [...productsContentState.opptyProductsByNumber].filter(([number]) => !current.numbers || current.numbers.has(String(number)));
|
||||||
|
const products = entries.map(([, result]) => result).filter(result => !result.error);
|
||||||
if (!products.length) return;
|
if (!products.length) return;
|
||||||
const cachedAt = Math.max(...products.map(result => Number(result.cachedAt) || 0));
|
const cachedAt = Math.max(...products.map(result => Number(result.cachedAt) || 0));
|
||||||
const isActive = () => cycle === current && productsContentState.currentProductsRequestId === requestId
|
const isActive = () => cycle === current && productsContentState.currentProductsRequestId === requestId
|
||||||
&& opportunitiesQueryState.periodRequestVersion === current.version;
|
&& opportunitiesQueryState.periodRequestVersion === current.version;
|
||||||
return Promise.resolve(loadCrm(current.token, current.version, { cachedAt, cacheOnly: products.every(result => result.fromCache) })).then(async success => {
|
// Product cache hits do not guarantee that the corresponding activity cache exists.
|
||||||
|
return Promise.resolve(loadCrm(current.token, current.version, { cachedAt, cacheOnly: false })).then(async success => {
|
||||||
if (!isActive()) return;
|
if (!isActive()) return;
|
||||||
if (success === false) {
|
if (success === false) {
|
||||||
products.forEach(blockActions);
|
products.forEach(blockActions);
|
||||||
@@ -30,7 +32,7 @@ function createProductActivities({ productsContentState, opportunitiesQueryState
|
|||||||
seen.add(id);
|
seen.add(id);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
pending.push(loadActions({ ...result, items }, true, { cachedAt: result.cachedAt, cacheOnly: result.fromCache === true }));
|
pending.push(loadActions({ ...result, items }, true, { cachedAt: result.cachedAt, cacheOnly: false }));
|
||||||
}
|
}
|
||||||
await Promise.allSettled(pending);
|
await Promise.allSettled(pending);
|
||||||
if (!isActive()) return;
|
if (!isActive()) return;
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ function createOpportunityWorkloadActions({ authSessionState, allowsCrmActivitie
|
|||||||
for (const id of idsFor(products)) {
|
for (const id of idsFor(products)) {
|
||||||
session.records.set(id, { status: "error", actions: [], hasNext: false, error: "CRM service requests unavailable." });
|
session.records.set(id, { status: "error", actions: [], hasNext: false, error: "CRM service requests unavailable." });
|
||||||
}
|
}
|
||||||
|
updateProgress(session);
|
||||||
}
|
}
|
||||||
function pump(current) {
|
function pump(current) {
|
||||||
while (current.enabled && current === session && current.running < 4 && current.queue.length) {
|
while (current.enabled && current === session && current.running < 4 && current.queue.length) {
|
||||||
@@ -160,6 +161,8 @@ function createOpportunityWorkloadActions({ authSessionState, allowsCrmActivitie
|
|||||||
].join("\n")).join("\n\n") : state === "missing" ? "No actions found for this opportunity's products."
|
].join("\n")).join("\n\n") : state === "missing" ? "No actions found for this opportunity's products."
|
||||||
: state === "loading" ? "Loading workload actions..." : "Unable to confirm workload actions. Check product RevnId and Debug Requests.";
|
: state === "loading" ? "Loading workload actions..." : "Unable to confirm workload actions. Check product RevnId and Debug Requests.";
|
||||||
if (records.some(record => record?.hasNext)) badge.title += "\nAdditional actions exist; pagination is not enabled.";
|
if (records.some(record => record?.hasNext)) badge.title += "\nAdditional actions exist; pagination is not enabled.";
|
||||||
|
const errors = [...new Set(records.filter(record => record?.error).map(record => record.error))];
|
||||||
|
if (errors.length) badge.title += `\n${errors.join("\n")}`;
|
||||||
if (!principal) badge.title += "\nTokenrelay principal is unavailable; ownership cannot be verified.";
|
if (!principal) badge.title += "\nTokenrelay principal is unavailable; ownership cannot be verified.";
|
||||||
else if (actions.length && state === "missing") badge.title += "\nNone of the returned actions belong to the signed-in principal.";
|
else if (actions.length && state === "missing") badge.title += "\nNone of the returned actions belong to the signed-in principal.";
|
||||||
if (actions.length && (pending || uncertain)) badge.title += "\nSome workloads are still loading or could not be fully checked.";
|
if (actions.length && (pending || uncertain)) badge.title += "\nSome workloads are still loading or could not be fully checked.";
|
||||||
|
|||||||
@@ -53,7 +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);
|
beginProductActivities(requestId, token, optyNumbers);
|
||||||
productsContentState.opptyProductsByNumber = new Map();
|
productsContentState.opptyProductsByNumber = new Map();
|
||||||
productsContentState.expandedProductOptyNumbers = new Set();
|
productsContentState.expandedProductOptyNumbers = new Set();
|
||||||
productsContentState.productsProgress = {
|
productsContentState.productsProgress = {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function createProductsDetails({
|
|||||||
const badge = document.createElement("span");
|
const badge = document.createElement("span");
|
||||||
badge.className = "opportunities-extension-products-ramp";
|
badge.className = "opportunities-extension-products-ramp";
|
||||||
badge.setAttribute("data-missing", String(values.includes("N/D")));
|
badge.setAttribute("data-missing", String(values.includes("N/D")));
|
||||||
setBadgeContent(badge, "ramp", values.join(","));
|
setBadgeContent(badge, "ramp", values.join(" | "));
|
||||||
badge.title = `Ramp Months: ${values.join(", ")}`;
|
badge.title = `Ramp Months: ${values.join(", ")}`;
|
||||||
badge.setAttribute("aria-label", badge.title);
|
badge.setAttribute("aria-label", badge.title);
|
||||||
return badge;
|
return badge;
|
||||||
|
|||||||
@@ -44,6 +44,41 @@ function setup(family, saved = {}, principal = "user@example.invalid") {
|
|||||||
}
|
}
|
||||||
return { app, calls, load, saved, local };
|
return { app, calls, load, saved, local };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("all cached opportunities load missing Actions despite filters, then reuse their completed cache", async () => {
|
||||||
|
const h = setup("firefox");
|
||||||
|
const stamp = Date.now() - 1000;
|
||||||
|
const app = h.app;
|
||||||
|
app.opportunitiesFilterState.tableSearch = "hide every opportunity";
|
||||||
|
app.productsContentJobs.renderProductsProgress = () => {};
|
||||||
|
const products = Array.from({ length: 7 }, (_, index) => [String(index + 1), {
|
||||||
|
items: [{ RevnId: String(index + 1) }], cachedAt: stamp, fromCache: true
|
||||||
|
}]);
|
||||||
|
app.productsContentState.opptyProductsByNumber = new Map(products);
|
||||||
|
await app.activityCache.write("actions", "1", stamp, []);
|
||||||
|
await app.activityCache.write("actions", "2", stamp - 1, []);
|
||||||
|
const queried = [];
|
||||||
|
app.platformContentMessages.sendRuntimeMessage = async message => {
|
||||||
|
queried.push(message.revnId);
|
||||||
|
return { ok: true, actions: message.revnId === "3" ? [{ owner: app.authSessionState.principal }] : [], hasNext: false };
|
||||||
|
};
|
||||||
|
async function cycle(id) {
|
||||||
|
app.productsContentState.currentProductsRequestId = id;
|
||||||
|
app.productActivities.begin(id, "synthetic-token");
|
||||||
|
await app.productActivities.complete(id);
|
||||||
|
}
|
||||||
|
await cycle("first");
|
||||||
|
assert.deepEqual(queried.sort(), ["2", "3", "4", "5", "6", "7"]);
|
||||||
|
assert.deepEqual(h.calls, ["sr"], "a missing SR cache must not block the Actions queue");
|
||||||
|
for (const [number] of products) {
|
||||||
|
assert.equal(app.opportunityWorkloadActions.getSummary(number).state, number === "3" ? "matched" : "missing");
|
||||||
|
}
|
||||||
|
app.opportunityWorkloadActions.reset();
|
||||||
|
app.opportunityCrmServiceRequests.reset();
|
||||||
|
await cycle("second");
|
||||||
|
assert.equal(queried.length, 6, "valid matching action cache avoids all repeat requests");
|
||||||
|
assert.deepEqual(h.calls, ["sr"]);
|
||||||
|
});
|
||||||
for (const family of ["chrome", "firefox"]) {
|
for (const family of ["chrome", "firefox"]) {
|
||||||
test(`${family}: Current Quarter -> Next Quarter -> Current Quarter restores SR hours and filters without requests`, async () => {
|
test(`${family}: Current Quarter -> Next Quarter -> Current Quarter restores SR hours and filters without requests`, async () => {
|
||||||
const h = setup(family);
|
const h = setup(family);
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ test("consistent inline vectors replace raster icons without parentheses or lost
|
|||||||
assert.equal(count.title, "3 products");
|
assert.equal(count.title, "3 products");
|
||||||
assert.equal(typeof count.listeners.click, "function");
|
assert.equal(typeof count.listeners.click, "function");
|
||||||
const ramp = app.productsDetails.createProductsRampBadge(products);
|
const ramp = app.productsDetails.createProductsRampBadge(products);
|
||||||
checkIcon(ramp, "ramp", "1,2,3");
|
checkIcon(ramp, "ramp", "1 | 2 | 3");
|
||||||
assert.equal(ramp.title, "Ramp Months: 1, 2, 3");
|
assert.equal(ramp.title, "Ramp Months: 1, 2, 3");
|
||||||
const missingRamp = app.productsDetails.createProductsRampBadge({ items: [{ RampMonths_c: null }] });
|
const missingRamp = app.productsDetails.createProductsRampBadge({ items: [{ RampMonths_c: null }] });
|
||||||
checkIcon(missingRamp, "ramp", "N/D");
|
checkIcon(missingRamp, "ramp", "N/D");
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ function setup() {
|
|||||||
app.opportunityWorkloadActions.loadProducts = (products, refresh, options) => { if (!options?.cacheOnly) calls.push(products.items[0]?.RevnId || "empty products"); };
|
app.opportunityWorkloadActions.loadProducts = (products, refresh, options) => { if (!options?.cacheOnly) calls.push(products.items[0]?.RevnId || "empty products"); };
|
||||||
app.platformContentMessages.sendRuntimeMessage = async () => ({ ok: true });
|
app.platformContentMessages.sendRuntimeMessage = async () => ({ ok: true });
|
||||||
app.productsContentJobs.ensureProductsBackgroundMessageHandler();
|
app.productsContentJobs.ensureProductsBackgroundMessageHandler();
|
||||||
function start(token = "token") {
|
function start(token = "token", numbers = ["A"]) {
|
||||||
app.productsContentJobs.requestOpptyProductsInBackground([{ OptyNumber: "A" }], token);
|
app.productsContentJobs.requestOpptyProductsInBackground(numbers.map(OptyNumber => ({ OptyNumber })), token);
|
||||||
return app.productsContentState.currentProductsRequestId;
|
return app.productsContentState.currentProductsRequestId;
|
||||||
}
|
}
|
||||||
function progress(requestId, data) {
|
function progress(requestId, data) {
|
||||||
@@ -147,19 +147,33 @@ test("partial failures use successful products only; closed, failed and empty cy
|
|||||||
assert.equal(h.calls.includes("closed"), false);
|
assert.equal(h.calls.includes("closed"), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("cached products restore activities without network; mixed cycles fetch only fresh products", async () => {
|
test("cached and mixed product cycles both allow activity cache misses to be fetched", async () => {
|
||||||
const h = setup();
|
const h = setup();
|
||||||
const cached = h.start();
|
const cached = h.start();
|
||||||
h.progress(cached, { optyNumber: "A", items: [{ RevnId: "cached" }], fromCache: true });
|
h.progress(cached, { optyNumber: "A", items: [{ RevnId: "cached" }], fromCache: true });
|
||||||
h.progress(cached, { state: "complete" });
|
h.progress(cached, { state: "complete" });
|
||||||
await flush();
|
await flush();
|
||||||
assert.deepEqual(h.calls, []);
|
assert.deepEqual(h.calls, ["SR token", "cached"]);
|
||||||
const mixed = h.start();
|
h.calls.length = 0;
|
||||||
|
const mixed = h.start("token", ["A", "B"]);
|
||||||
h.progress(mixed, { optyNumber: "A", items: [{ RevnId: "cached" }], fromCache: true });
|
h.progress(mixed, { optyNumber: "A", items: [{ RevnId: "cached" }], fromCache: true });
|
||||||
h.progress(mixed, { optyNumber: "B", items: [{ RevnId: "fresh" }], fromCache: false });
|
h.progress(mixed, { optyNumber: "B", items: [{ RevnId: "fresh" }], fromCache: false });
|
||||||
h.progress(mixed, { state: "complete" });
|
h.progress(mixed, { state: "complete" });
|
||||||
await flush();
|
await flush();
|
||||||
assert.deepEqual(h.calls, ["SR token", "fresh"]);
|
assert.deepEqual(h.calls, ["SR token", "cached", "fresh"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("activity scope includes hidden reference opportunities and excludes entries from other references", async () => {
|
||||||
|
const h = setup();
|
||||||
|
h.app.opportunitiesFilterState.tableSearch = "hide everything";
|
||||||
|
const id = h.start("token", ["A", "B"]);
|
||||||
|
h.progress(id, { optyNumber: "A", items: [{ RevnId: "1" }, { RevnId: "2" }] });
|
||||||
|
h.progress(id, { optyNumber: "B", items: [{ RevnId: "3" }] });
|
||||||
|
h.progress(id, { optyNumber: "OTHER_REFERENCE", items: [{ RevnId: "outside" }] });
|
||||||
|
h.progress(id, { state: "complete" });
|
||||||
|
await flush();
|
||||||
|
assert.deepEqual(h.calls, ["SR token", "1", "3"]);
|
||||||
|
assert.equal(h.calls.includes("outside"), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("reopening an existing modal does not reload products or reset/query activities", async () => {
|
test("reopening an existing modal does not reload products or reset/query activities", async () => {
|
||||||
|
|||||||
@@ -64,6 +64,29 @@ function contentFixture(send) {
|
|||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("Actions progress counts product workloads, not opportunities, even with filters", async () => {
|
||||||
|
const pending = new Map();
|
||||||
|
const app = contentFixture(message => new Promise(resolve => pending.set(message.revnId, resolve)));
|
||||||
|
const progress = [];
|
||||||
|
app.activityProgress.update = (...args) => progress.push(args);
|
||||||
|
app.opportunitiesFilterState.tableSearch = "hidden";
|
||||||
|
const a = { items: [{ RevnId: "1" }, { RevnId: "2" }] };
|
||||||
|
const b = { items: [{ RevnId: "3" }] };
|
||||||
|
app.productsContentState.opptyProductsByNumber = new Map([["A", a], ["B", b]]);
|
||||||
|
const loading = [app.opportunityWorkloadActions.loadProducts(a), app.opportunityWorkloadActions.loadProducts(b)];
|
||||||
|
await new Promise(resolve => setImmediate(resolve));
|
||||||
|
assert.equal(pending.size, 3);
|
||||||
|
pending.get("1")({ ok: true, actions: [], hasNext: false });
|
||||||
|
await new Promise(resolve => setImmediate(resolve));
|
||||||
|
assert.deepEqual(progress.at(-1), ["actions", "running", 1, 3]);
|
||||||
|
pending.get("2")({ ok: true, actions: [], hasNext: false });
|
||||||
|
await new Promise(resolve => setImmediate(resolve));
|
||||||
|
assert.deepEqual(progress.at(-1), ["actions", "running", 2, 3]);
|
||||||
|
pending.get("3")({ ok: true, actions: [], hasNext: false });
|
||||||
|
await Promise.all(loading);
|
||||||
|
assert.deepEqual(progress.at(-1), ["actions", "complete", 3, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
test("saved response updates the workload badge, tooltip and matching cache without fetching", async () => {
|
test("saved response updates the workload badge, tooltip and matching cache without fetching", async () => {
|
||||||
const app = contentFixture(() => assert.fail("Save response must not trigger a GET"));
|
const app = contentFixture(() => assert.fail("Save response must not trigger a GET"));
|
||||||
const writes = [];
|
const writes = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user