Implementation of the opportunity product view
This commit is contained in:
716
dist/firefox/content.js
vendored
716
dist/firefox/content.js
vendored
@@ -24,14 +24,18 @@
|
||||
const OWNER_FILTER_LIST_ID = "opportunities-extension-owner-filter-list";
|
||||
const TABLE_SEARCH_ID = "opportunities-extension-table-search";
|
||||
const STAGE_DASHBOARD_ID = "opportunities-extension-stage-dashboard";
|
||||
const PRODUCTS_PROGRESS_ID = "opportunities-extension-products-progress";
|
||||
const PRODUCTS_CACHE_TIMESTAMP_ID = "opportunities-extension-products-cache-timestamp";
|
||||
const FILTER_PREFERENCES_STORAGE_KEY = "opportunities-extension-filter-preferences";
|
||||
const THEME_STORAGE_KEY = "opportunities-extension-theme";
|
||||
const OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY = "opportunities-extension-opportunity-type-preferences";
|
||||
const PREFERENCES_MODAL_ID = "opportunities-extension-preferences-modal";
|
||||
const PRODUCTS_MESSAGE_LISTENER_KEY = "opportunitiesExtensionProductsMessageListener";
|
||||
const OPPORTUNITY_DETAIL_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-detail?puid=";
|
||||
const ACCOUNT_DETAIL_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/accounts/accounts-detail?id=";
|
||||
const DEBUG_BODY_LIMIT = 12000;
|
||||
const OPPORTUNITIES_PAGE_LIMIT = 15;
|
||||
const DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS = 24;
|
||||
const TOKEN_RELAY_URL = "https://eeho.fa.us2.oraclecloud.com/fscmRestApi/tokenrelay";
|
||||
const OPPORTUNITIES_QUERY_URL = "https://eeho.fa.us2.oraclecloud.com/crmRestApi/searchResources/11.13.18.05/custom-actions/queries";
|
||||
const AUTH_STATUS = {
|
||||
@@ -60,6 +64,14 @@
|
||||
"ORA_MYASSGTERROPTIES",
|
||||
"ORA_CREDITRECEIVER_ISME"
|
||||
];
|
||||
const OPPTY_PRODUCTS_CACHE_OPTIONS = [
|
||||
{ label: "1 hour", value: 1 },
|
||||
{ label: "6 hours", value: 6 },
|
||||
{ label: "12 hours", value: 12 },
|
||||
{ label: "24 hours (default)", value: 24 },
|
||||
{ label: "48 hours", value: 48 },
|
||||
{ label: "7 days", value: 168 }
|
||||
];
|
||||
let tokenRelayRequest = null;
|
||||
let accessToken = "";
|
||||
let periodRequestVersion = 0;
|
||||
@@ -73,6 +85,19 @@
|
||||
let ownerSearch = "";
|
||||
let tableSearch = "";
|
||||
let selectedOpportunityTypeValues = new Set(DEFAULT_OPPORTUNITY_TYPE_VALUES);
|
||||
let opptyProductsCacheHours = DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS;
|
||||
let currentProductsRequestId = "";
|
||||
let productsRenderQueued = false;
|
||||
let opptyProductsByNumber = new Map();
|
||||
let expandedProductOptyNumbers = new Set();
|
||||
let productsProgress = {
|
||||
state: "idle",
|
||||
completed: 0,
|
||||
total: 0,
|
||||
failed: 0,
|
||||
cacheHits: 0
|
||||
};
|
||||
let productsLastCachedAt = 0;
|
||||
let hasSavedCustomerFilter = false;
|
||||
let hasSavedOwnerFilter = false;
|
||||
let customerFilterInitialized = false;
|
||||
@@ -115,6 +140,7 @@
|
||||
}
|
||||
|
||||
ensureExtensionStyles();
|
||||
ensureProductsBackgroundMessageHandler();
|
||||
|
||||
function createOpportunitiesTile() {
|
||||
const wrapper = document.createElement("div");
|
||||
@@ -198,6 +224,7 @@
|
||||
authStatus = AUTH_STATUS.idle;
|
||||
resetOpportunitiesTable();
|
||||
selectedOpportunityTypeValues = new Set(getSavedOpportunityTypeValues());
|
||||
opptyProductsCacheHours = getSavedOpptyProductsCacheHours();
|
||||
overlay.id = MODAL_ID;
|
||||
overlay.className = "opportunities-extension-modal";
|
||||
overlay.setAttribute("data-theme", getSavedTheme());
|
||||
@@ -227,7 +254,19 @@
|
||||
|
||||
const subtitleRow = document.createElement("div");
|
||||
subtitleRow.className = "opportunities-extension-subtitle-row";
|
||||
subtitleRow.append(subtitle, authBadge);
|
||||
|
||||
const productsProgressBadge = document.createElement("span");
|
||||
productsProgressBadge.id = PRODUCTS_PROGRESS_ID;
|
||||
productsProgressBadge.className = "opportunities-extension-products-progress";
|
||||
productsProgressBadge.setAttribute("role", "status");
|
||||
productsProgressBadge.hidden = true;
|
||||
|
||||
const productsCacheTimestamp = document.createElement("span");
|
||||
productsCacheTimestamp.id = PRODUCTS_CACHE_TIMESTAMP_ID;
|
||||
productsCacheTimestamp.className = "opportunities-extension-products-cache-timestamp";
|
||||
productsCacheTimestamp.setAttribute("role", "status");
|
||||
productsCacheTimestamp.hidden = true;
|
||||
subtitleRow.append(subtitle, authBadge, productsProgressBadge, productsCacheTimestamp);
|
||||
|
||||
const titleBlock = document.createElement("div");
|
||||
titleBlock.append(title, subtitleRow);
|
||||
@@ -698,6 +737,18 @@
|
||||
|
||||
async function requestOpportunities(token, period, dateRange, requestVersion) {
|
||||
if (isCurrentPeriodRequest(requestVersion)) {
|
||||
currentProductsRequestId = "";
|
||||
opptyProductsByNumber = new Map();
|
||||
expandedProductOptyNumbers = new Set();
|
||||
productsProgress = {
|
||||
state: "idle",
|
||||
completed: 0,
|
||||
total: 0,
|
||||
failed: 0,
|
||||
cacheHits: 0
|
||||
};
|
||||
productsLastCachedAt = 0;
|
||||
renderProductsProgress();
|
||||
setOpportunitiesTableState({
|
||||
items: [],
|
||||
status: "loading",
|
||||
@@ -714,6 +765,7 @@
|
||||
status: "ready",
|
||||
message: ""
|
||||
});
|
||||
requestOpptyProductsInBackground(allItems, token);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isCurrentPeriodRequest(requestVersion)) {
|
||||
@@ -809,6 +861,180 @@
|
||||
}
|
||||
}
|
||||
|
||||
function requestOpptyProductsInBackground(opportunities, token) {
|
||||
const optyNumbers = Array.from(new Set(opportunities
|
||||
.map((item) => item && item.OptyNumber)
|
||||
.filter(Boolean)
|
||||
.map(String)));
|
||||
|
||||
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
currentProductsRequestId = requestId;
|
||||
opptyProductsByNumber = new Map();
|
||||
expandedProductOptyNumbers = new Set();
|
||||
productsProgress = {
|
||||
state: optyNumbers.length ? "running" : "complete",
|
||||
completed: 0,
|
||||
total: optyNumbers.length,
|
||||
failed: 0,
|
||||
cacheHits: 0
|
||||
};
|
||||
renderProductsProgress();
|
||||
|
||||
if (optyNumbers.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendRuntimeMessage({
|
||||
type: "opportunitiesExtension.requestOpptyProducts",
|
||||
requestId,
|
||||
accessToken: token,
|
||||
optyNumbers,
|
||||
cacheMaxAgeMs: opptyProductsCacheHours * 60 * 60 * 1000
|
||||
}).then((response) => {
|
||||
if (response && response.ok === false && response.requestId === currentProductsRequestId && productsProgress.state === "running") {
|
||||
productsProgress.state = "error";
|
||||
productsProgress.failed = productsProgress.total;
|
||||
renderProductsProgress();
|
||||
}
|
||||
}).catch(() => {
|
||||
if (requestId === currentProductsRequestId && productsProgress.state === "running") {
|
||||
productsProgress.state = "error";
|
||||
productsProgress.failed = productsProgress.total;
|
||||
renderProductsProgress();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function ensureProductsBackgroundMessageHandler() {
|
||||
if (window[PRODUCTS_MESSAGE_LISTENER_KEY]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
||||
|
||||
if (!runtimeApi || !runtimeApi.runtime || !runtimeApi.runtime.onMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
window[PRODUCTS_MESSAGE_LISTENER_KEY] = true;
|
||||
runtimeApi.runtime.onMessage.addListener((message) => {
|
||||
if (!message || message.type !== "opportunitiesExtension.opptyProductsProgress" || message.requestId !== currentProductsRequestId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (message.optyNumber) {
|
||||
opptyProductsByNumber.set(String(message.optyNumber), {
|
||||
items: Array.isArray(message.items) ? message.items : [],
|
||||
fromCache: Boolean(message.fromCache),
|
||||
cachedAt: Number(message.cachedAt) || 0,
|
||||
error: message.error || ""
|
||||
});
|
||||
}
|
||||
|
||||
if (Number(message.cachedAt) > productsLastCachedAt) {
|
||||
productsLastCachedAt = Number(message.cachedAt);
|
||||
}
|
||||
|
||||
productsProgress = {
|
||||
state: message.state || productsProgress.state,
|
||||
completed: Number(message.completed) || 0,
|
||||
total: Number(message.total) || productsProgress.total,
|
||||
failed: Number(message.failed) || 0,
|
||||
cacheHits: Number(message.cacheHits) || 0
|
||||
};
|
||||
renderProductsProgress();
|
||||
queueProductsTableRender();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function sendRuntimeMessage(message) {
|
||||
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
||||
|
||||
if (!runtimeApi || !runtimeApi.runtime || !runtimeApi.runtime.sendMessage) {
|
||||
return Promise.reject(new Error("Runtime messaging API unavailable."));
|
||||
}
|
||||
|
||||
if (typeof browser !== "undefined") {
|
||||
return runtimeApi.runtime.sendMessage(message);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
runtimeApi.runtime.sendMessage(message, (response) => {
|
||||
const lastError = runtimeApi.runtime.lastError;
|
||||
|
||||
if (lastError) {
|
||||
reject(new Error(lastError.message));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderProductsProgress() {
|
||||
const badge = document.getElementById(PRODUCTS_PROGRESS_ID);
|
||||
|
||||
if (!badge) {
|
||||
return;
|
||||
}
|
||||
|
||||
badge.hidden = productsProgress.state === "idle";
|
||||
badge.setAttribute("data-state", productsProgress.state);
|
||||
|
||||
if (productsProgress.state === "running") {
|
||||
badge.textContent = `Products ${productsProgress.completed}/${productsProgress.total}`;
|
||||
badge.title = `${productsProgress.cacheHits} loaded from cache`;
|
||||
} else if (productsProgress.state === "complete") {
|
||||
badge.textContent = `Products ready ${productsProgress.total}`;
|
||||
badge.title = `${productsProgress.cacheHits} loaded from cache`;
|
||||
} else if (productsProgress.state === "complete-with-errors") {
|
||||
badge.textContent = `Products ${productsProgress.total - productsProgress.failed}/${productsProgress.total}`;
|
||||
badge.title = `${productsProgress.failed} product requests failed`;
|
||||
} else if (productsProgress.state === "error") {
|
||||
badge.textContent = "Products unavailable";
|
||||
badge.title = "Unable to load opportunity products";
|
||||
} else {
|
||||
badge.textContent = "Products ready 0";
|
||||
}
|
||||
|
||||
const timestamp = document.getElementById(PRODUCTS_CACHE_TIMESTAMP_ID);
|
||||
|
||||
if (timestamp) {
|
||||
timestamp.hidden = !productsLastCachedAt;
|
||||
timestamp.textContent = productsLastCachedAt
|
||||
? `Last cache: ${formatProductsCacheTimestamp(productsLastCachedAt)}`
|
||||
: "";
|
||||
timestamp.title = productsLastCachedAt
|
||||
? `Latest product cache: ${formatProductsCacheTimestamp(productsLastCachedAt)}`
|
||||
: "";
|
||||
}
|
||||
}
|
||||
|
||||
function formatProductsCacheTimestamp(timestamp) {
|
||||
try {
|
||||
return new Intl.DateTimeFormat("en-GB", {
|
||||
dateStyle: "short",
|
||||
timeStyle: "short"
|
||||
}).format(new Date(timestamp));
|
||||
} catch (error) {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
}
|
||||
}
|
||||
|
||||
function queueProductsTableRender() {
|
||||
if (productsRenderQueued) {
|
||||
return;
|
||||
}
|
||||
|
||||
productsRenderQueued = true;
|
||||
window.requestAnimationFrame(() => {
|
||||
productsRenderQueued = false;
|
||||
renderOpportunitiesTable();
|
||||
});
|
||||
}
|
||||
|
||||
function resetOpportunitiesTable() {
|
||||
selectedStages = new Set(STAGE_OPTIONS);
|
||||
selectedStatuses = new Set(STATUS_OPTIONS);
|
||||
@@ -821,6 +1047,17 @@
|
||||
hasSavedOwnerFilter = false;
|
||||
customerFilterInitialized = false;
|
||||
ownerFilterInitialized = false;
|
||||
currentProductsRequestId = "";
|
||||
opptyProductsByNumber = new Map();
|
||||
expandedProductOptyNumbers = new Set();
|
||||
productsProgress = {
|
||||
state: "idle",
|
||||
completed: 0,
|
||||
total: 0,
|
||||
failed: 0,
|
||||
cacheHits: 0
|
||||
};
|
||||
productsLastCachedAt = 0;
|
||||
stageDashboardAmounts = new Map([["TOTAL", 0], ...STAGE_OPTIONS.map((stage) => [stage, 0])]);
|
||||
opportunitiesTableState = {
|
||||
items: [],
|
||||
@@ -991,7 +1228,19 @@
|
||||
item.OptyNumber ? `${OPPORTUNITY_DETAIL_URL}${encodeURIComponent(item.OptyNumber)}` : ""
|
||||
);
|
||||
|
||||
if (column.key === "optyNumber" && item.OptyNumber) {
|
||||
if (column.key === "name" && item.OptyNumber) {
|
||||
const nameCellContent = document.createElement("span");
|
||||
nameCellContent.className = "opportunities-extension-name-content";
|
||||
nameCellContent.append(opportunityLink);
|
||||
|
||||
const productsResult = opptyProductsByNumber.get(String(item.OptyNumber));
|
||||
|
||||
if (productsResult) {
|
||||
nameCellContent.append(createProductsCountButton(item.OptyNumber, productsResult));
|
||||
}
|
||||
|
||||
cell.append(nameCellContent);
|
||||
} else if (column.key === "optyNumber" && item.OptyNumber) {
|
||||
const opportunityCellContent = document.createElement("span");
|
||||
opportunityCellContent.className = "opportunities-extension-opty-number-content";
|
||||
opportunityCellContent.append(opportunityLink, createCopyOpportunityButton(item.OptyNumber));
|
||||
@@ -1014,6 +1263,10 @@
|
||||
});
|
||||
|
||||
tableBody.append(row);
|
||||
|
||||
if (item.OptyNumber && expandedProductOptyNumbers.has(String(item.OptyNumber))) {
|
||||
tableBody.append(createProductsDetailRow(item));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1663,6 +1916,144 @@
|
||||
return button;
|
||||
}
|
||||
|
||||
function createProductsCountButton(optyNumber, productsResult) {
|
||||
const normalizedOptyNumber = String(optyNumber);
|
||||
const button = document.createElement("button");
|
||||
const count = Array.isArray(productsResult.items) ? productsResult.items.length : 0;
|
||||
const isExpanded = expandedProductOptyNumbers.has(normalizedOptyNumber);
|
||||
|
||||
button.type = "button";
|
||||
button.className = "opportunities-extension-products-count";
|
||||
button.textContent = String(count);
|
||||
button.setAttribute("aria-expanded", String(isExpanded));
|
||||
button.setAttribute("aria-label", `${count} product${count === 1 ? "" : "s"} for opportunity ${normalizedOptyNumber}`);
|
||||
button.title = productsResult.error ? productsResult.error : `${count} product${count === 1 ? "" : "s"}`;
|
||||
button.classList.toggle("opportunities-extension-products-count-error", Boolean(productsResult.error));
|
||||
button.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (isExpanded) {
|
||||
expandedProductOptyNumbers.delete(normalizedOptyNumber);
|
||||
} else {
|
||||
expandedProductOptyNumbers.add(normalizedOptyNumber);
|
||||
}
|
||||
|
||||
renderOpportunitiesTable();
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
function createProductsDetailRow(opportunity) {
|
||||
const row = document.createElement("tr");
|
||||
const cell = document.createElement("td");
|
||||
const optyNumber = String(opportunity.OptyNumber);
|
||||
const productsResult = opptyProductsByNumber.get(optyNumber);
|
||||
const products = productsResult && Array.isArray(productsResult.items) ? productsResult.items : [];
|
||||
|
||||
row.className = "opportunities-extension-products-detail-row";
|
||||
cell.colSpan = OPPORTUNITIES_COLUMNS.length;
|
||||
cell.className = "opportunities-extension-products-detail-cell";
|
||||
|
||||
const panel = document.createElement("section");
|
||||
panel.className = "opportunities-extension-products-detail";
|
||||
|
||||
const heading = document.createElement("div");
|
||||
heading.className = "opportunities-extension-products-detail-heading";
|
||||
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = `Products for ${optyNumber}`;
|
||||
|
||||
const summary = document.createElement("span");
|
||||
summary.textContent = productsResult && productsResult.fromCache ? "Cached" : "Updated";
|
||||
heading.append(title, summary);
|
||||
panel.append(heading);
|
||||
|
||||
if (productsResult && productsResult.error) {
|
||||
const error = document.createElement("p");
|
||||
error.className = "opportunities-extension-products-empty";
|
||||
error.textContent = productsResult.error;
|
||||
panel.append(error);
|
||||
} else if (products.length === 0) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "opportunities-extension-products-empty";
|
||||
empty.textContent = "No products found for this opportunity.";
|
||||
panel.append(empty);
|
||||
} else {
|
||||
const table = document.createElement("table");
|
||||
table.className = "opportunities-extension-products-table";
|
||||
table.setAttribute("aria-label", `Products for opportunity ${optyNumber}`);
|
||||
|
||||
const columns = [
|
||||
{ label: "Product Group", value: (item) => item.ProdGroupName || "-" },
|
||||
{ label: "Workload", value: (item) => item.WorkloadName_c || "-" },
|
||||
{ label: "Currency", value: (item) => item.RevnAmountCurcyCode || "-" },
|
||||
{ label: "Amount", value: (item) => formatProductAmount(item.RevnAmount, item.RevnAmountCurcyCode) },
|
||||
{ label: "Type", value: (item) => item.TypeCode || "-" },
|
||||
{ label: "Status", value: (item) => item.StatusCode || "-" },
|
||||
{ label: "Win Probability", value: (item) => formatProductWinProbability(item.WinProb) },
|
||||
{ label: "Close date", value: (item) => formatOracleResponseDate(item.EffectiveDate) || "-" },
|
||||
{ label: "Consumption Start", value: (item) => formatOracleResponseDate(item.ConsumptionStartDate_c) || "-" },
|
||||
{ label: "Ramp Months", value: (item) => formatProductNumber(item.RampMonths_c) }
|
||||
];
|
||||
|
||||
const head = document.createElement("thead");
|
||||
const headerRow = document.createElement("tr");
|
||||
columns.forEach((column) => {
|
||||
const header = document.createElement("th");
|
||||
header.scope = "col";
|
||||
header.textContent = column.label;
|
||||
headerRow.append(header);
|
||||
});
|
||||
head.append(headerRow);
|
||||
|
||||
const body = document.createElement("tbody");
|
||||
products.forEach((product) => {
|
||||
const productRow = document.createElement("tr");
|
||||
columns.forEach((column) => {
|
||||
const productCell = document.createElement("td");
|
||||
productCell.textContent = String(column.value(product));
|
||||
productRow.append(productCell);
|
||||
});
|
||||
body.append(productRow);
|
||||
});
|
||||
|
||||
table.append(head, body);
|
||||
panel.append(table);
|
||||
}
|
||||
|
||||
cell.append(panel);
|
||||
row.append(cell);
|
||||
return row;
|
||||
}
|
||||
|
||||
function formatProductNumber(value) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(number) : "-";
|
||||
}
|
||||
|
||||
function formatProductAmount(value, currency) {
|
||||
const amount = Number(value);
|
||||
|
||||
if (!Number.isFinite(amount)) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
try {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: currency || "USD"
|
||||
}).format(amount);
|
||||
} catch (error) {
|
||||
return `${currency || ""} ${formatProductNumber(amount)}`.trim();
|
||||
}
|
||||
}
|
||||
|
||||
function formatProductWinProbability(value) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? `${number}%` : "-";
|
||||
}
|
||||
|
||||
async function copyOpportunityNumber(optyNumber) {
|
||||
try {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
@@ -2396,6 +2787,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
function getSavedOpptyProductsCacheHours() {
|
||||
try {
|
||||
const rawPreferences = localStorage.getItem(OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY);
|
||||
|
||||
if (!rawPreferences) {
|
||||
return DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS;
|
||||
}
|
||||
|
||||
const preferences = JSON.parse(rawPreferences);
|
||||
const cacheHours = Number(preferences.opptyProductsCacheHours);
|
||||
return OPPTY_PRODUCTS_CACHE_OPTIONS.some((option) => option.value === cacheHours)
|
||||
? cacheHours
|
||||
: DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS;
|
||||
} catch (error) {
|
||||
return DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS;
|
||||
}
|
||||
}
|
||||
|
||||
function openPreferencesModal() {
|
||||
const overlay = document.getElementById(MODAL_ID);
|
||||
|
||||
@@ -2406,6 +2815,16 @@
|
||||
const existingModal = document.getElementById(PREFERENCES_MODAL_ID);
|
||||
|
||||
if (existingModal) {
|
||||
const savedValues = new Set(getSavedOpportunityTypeValues());
|
||||
existingModal.querySelectorAll("input[name='opportunityTypeView']").forEach((checkbox) => {
|
||||
checkbox.checked = savedValues.has(checkbox.value);
|
||||
});
|
||||
const cacheSelect = existingModal.querySelector("select[name='opptyProductsCacheHours']");
|
||||
|
||||
if (cacheSelect) {
|
||||
cacheSelect.value = String(getSavedOpptyProductsCacheHours());
|
||||
}
|
||||
|
||||
existingModal.hidden = false;
|
||||
existingModal.querySelector("input")?.focus();
|
||||
return;
|
||||
@@ -2468,7 +2887,25 @@
|
||||
});
|
||||
|
||||
fieldset.append(options);
|
||||
content.append(fieldset);
|
||||
|
||||
const cacheField = document.createElement("label");
|
||||
cacheField.className = "opportunities-extension-preferences-cache-field";
|
||||
|
||||
const cacheLabel = document.createElement("span");
|
||||
cacheLabel.textContent = "Product cache duration";
|
||||
|
||||
const cacheSelect = document.createElement("select");
|
||||
cacheSelect.name = "opptyProductsCacheHours";
|
||||
cacheSelect.setAttribute("aria-label", "Product cache duration");
|
||||
OPPTY_PRODUCTS_CACHE_OPTIONS.forEach((option) => {
|
||||
const optionElement = document.createElement("option");
|
||||
optionElement.value = String(option.value);
|
||||
optionElement.textContent = option.label;
|
||||
optionElement.selected = option.value === getSavedOpptyProductsCacheHours();
|
||||
cacheSelect.append(optionElement);
|
||||
});
|
||||
cacheField.append(cacheLabel, cacheSelect);
|
||||
content.append(fieldset, cacheField);
|
||||
|
||||
const footer = document.createElement("footer");
|
||||
footer.className = "opportunities-extension-preferences-footer";
|
||||
@@ -2479,11 +2916,16 @@
|
||||
saveButton.textContent = "Save";
|
||||
saveButton.addEventListener("click", () => {
|
||||
const selectedValues = Array.from(preferencesModal.querySelectorAll("input[name='opportunityTypeView']:checked"), (checkbox) => checkbox.value);
|
||||
const selectedCacheHours = Number(preferencesModal.querySelector("select[name='opptyProductsCacheHours']")?.value);
|
||||
selectedOpportunityTypeValues = new Set(selectedValues);
|
||||
opptyProductsCacheHours = OPPTY_PRODUCTS_CACHE_OPTIONS.some((option) => option.value === selectedCacheHours)
|
||||
? selectedCacheHours
|
||||
: DEFAULT_OPPTY_PRODUCTS_CACHE_HOURS;
|
||||
|
||||
try {
|
||||
localStorage.setItem(OPPORTUNITY_TYPE_PREFERENCES_STORAGE_KEY, JSON.stringify({
|
||||
opportunityTypeValues: selectedValues
|
||||
opportunityTypeValues: selectedValues,
|
||||
opptyProductsCacheHours
|
||||
}));
|
||||
} catch (error) {
|
||||
// Continue with the saved selection for the active modal when storage is unavailable.
|
||||
@@ -2688,6 +3130,66 @@
|
||||
background: #6f5a7f;
|
||||
}
|
||||
|
||||
.opportunities-extension-products-progress {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 22px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: #e8f4f6;
|
||||
color: #006d7a;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.opportunities-extension-products-progress[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.opportunities-extension-products-cache-timestamp {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 22px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: #f0eeeb;
|
||||
color: #5f5a55;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.opportunities-extension-products-cache-timestamp[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.opportunities-extension-products-progress[data-state="running"]::before {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 6px;
|
||||
border: 2px solid currentColor;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
content: "";
|
||||
animation: opportunities-extension-spin .8s linear infinite;
|
||||
}
|
||||
|
||||
.opportunities-extension-products-progress[data-state="complete"] {
|
||||
background: #e2f2e5;
|
||||
color: #356d19;
|
||||
}
|
||||
|
||||
.opportunities-extension-products-progress[data-state="complete-with-errors"],
|
||||
.opportunities-extension-products-progress[data-state="error"] {
|
||||
background: #f9e3e1;
|
||||
color: #a52b1c;
|
||||
}
|
||||
|
||||
@keyframes opportunities-extension-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.opportunities-extension-icon-button {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
@@ -3357,6 +3859,128 @@
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.opportunities-extension-name-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.opportunities-extension-name-content > a {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.opportunities-extension-products-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
min-width: 24px;
|
||||
min-height: 22px;
|
||||
padding: 2px 7px;
|
||||
border: 1px solid #7ca7b2;
|
||||
border-radius: 999px;
|
||||
background: #e8f4f6;
|
||||
color: #006d7a;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.opportunities-extension-products-count:hover,
|
||||
.opportunities-extension-products-count:focus-visible,
|
||||
.opportunities-extension-products-count[aria-expanded="true"] {
|
||||
border-color: #00758f;
|
||||
background: #00758f;
|
||||
color: #ffffff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.opportunities-extension-products-count-error {
|
||||
border-color: #d8887e;
|
||||
background: #f9e3e1;
|
||||
color: #a52b1c;
|
||||
}
|
||||
|
||||
.opportunities-extension-products-detail-row:hover {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.opportunities-extension-products-detail-cell {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.opportunities-extension-products-detail {
|
||||
padding: 14px 20px 18px;
|
||||
border-bottom: 1px solid #c9c5c1;
|
||||
background: #f0eeeb;
|
||||
}
|
||||
|
||||
.opportunities-extension-products-detail-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
color: #312d2a;
|
||||
}
|
||||
|
||||
.opportunities-extension-products-detail-heading strong {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.opportunities-extension-products-detail-heading span {
|
||||
color: #5f5a55;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.opportunities-extension-products-table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
border: 1px solid #dedbd7;
|
||||
background: #ffffff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.opportunities-extension-products-table th,
|
||||
.opportunities-extension-products-table td {
|
||||
position: static;
|
||||
height: auto;
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid #ebe8e5;
|
||||
background: transparent;
|
||||
overflow-wrap: anywhere;
|
||||
vertical-align: middle;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.opportunities-extension-products-table th {
|
||||
background: #faf9f8;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.opportunities-extension-products-table th:nth-child(1) { width: 16%; }
|
||||
.opportunities-extension-products-table th:nth-child(2) { width: 16%; }
|
||||
.opportunities-extension-products-table th:nth-child(3) { width: 8%; }
|
||||
.opportunities-extension-products-table th:nth-child(4) { width: 10%; }
|
||||
.opportunities-extension-products-table th:nth-child(5) { width: 8%; }
|
||||
.opportunities-extension-products-table th:nth-child(6) { width: 8%; }
|
||||
.opportunities-extension-products-table th:nth-child(7) { width: 7%; }
|
||||
.opportunities-extension-products-table th:nth-child(8) { width: 10%; }
|
||||
.opportunities-extension-products-table th:nth-child(9) { width: 12%; }
|
||||
.opportunities-extension-products-table th:nth-child(10) { width: 5%; }
|
||||
|
||||
.opportunities-extension-products-empty {
|
||||
margin: 0;
|
||||
padding: 10px 0;
|
||||
color: #5f5a55;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.opportunities-extension-copy-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -3644,6 +4268,34 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.opportunities-extension-preferences-cache-field {
|
||||
display: grid;
|
||||
width: min(280px, 100%);
|
||||
gap: 6px;
|
||||
margin-top: 24px;
|
||||
color: #312d2a;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.opportunities-extension-preferences-cache-field select {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
padding: 0 36px 0 12px;
|
||||
border: 1px solid #8f8a85;
|
||||
border-radius: 4px;
|
||||
background: #ffffff;
|
||||
color: #312d2a;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.opportunities-extension-preferences-cache-field select:focus {
|
||||
border-color: #00758f;
|
||||
outline: 2px solid #bde7ee;
|
||||
outline-offset: 0;
|
||||
}
|
||||
|
||||
.opportunities-extension-preferences-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -3865,6 +4517,8 @@
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-header p,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-dashboard-card span,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-empty,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-detail-heading span,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-empty,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel p,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-empty {
|
||||
color: #c9c5c1;
|
||||
@@ -3875,6 +4529,7 @@
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table-surface,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-panel,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-dialog,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-table,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-item[open] {
|
||||
border-color: #4e4a46;
|
||||
@@ -3885,6 +4540,7 @@
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-filter-search,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table-search-field input,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-field select,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-cache-field select,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-stage-filter-button,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-status-filter-button {
|
||||
border-color: #6a6560;
|
||||
@@ -3915,6 +4571,8 @@
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-header h2,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-fieldset legend,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-option,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-preferences-cache-field,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-detail-heading,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-heading,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-item pre,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-debug-panel h2 {
|
||||
@@ -3959,7 +4617,8 @@
|
||||
}
|
||||
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table thead,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table th {
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table th,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-table th {
|
||||
background: #333130;
|
||||
color: #f6f4f2;
|
||||
}
|
||||
@@ -3968,6 +4627,53 @@
|
||||
border-bottom-color: #3f3c39;
|
||||
}
|
||||
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-detail {
|
||||
border-bottom-color: #4e4a46;
|
||||
background: #252423;
|
||||
}
|
||||
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-table th,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-table td {
|
||||
border-bottom-color: #3f3c39;
|
||||
}
|
||||
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-count {
|
||||
border-color: #2f9bae;
|
||||
background: #1d4e55;
|
||||
color: #91e4ed;
|
||||
}
|
||||
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-count:hover,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-count:focus-visible,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-count[aria-expanded="true"] {
|
||||
border-color: #43c4d5;
|
||||
background: #008aa6;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-progress[data-state="running"] {
|
||||
background: #1d4e55;
|
||||
color: #91e4ed;
|
||||
}
|
||||
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-progress[data-state="complete"] {
|
||||
background: #244f2e;
|
||||
color: #a8dfb7;
|
||||
}
|
||||
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-progress[data-state="complete-with-errors"],
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-progress[data-state="error"],
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-count-error {
|
||||
border-color: #b13b34;
|
||||
background: #5f2926;
|
||||
color: #ffb4ad;
|
||||
}
|
||||
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-products-cache-timestamp {
|
||||
background: #3b3937;
|
||||
color: #c9c5c1;
|
||||
}
|
||||
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-table a,
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-copy-button {
|
||||
color: #43c4d5;
|
||||
|
||||
Reference in New Issue
Block a user