Visual enhancements for dark mode, along with search functionality for service requests and installed base

This commit is contained in:
2026-09-23 10:56:51 -03:00
parent b4d8d9dbc1
commit 6c5db94aaa
17 changed files with 436 additions and 83 deletions

View File

@@ -321,7 +321,9 @@ function createContentApplication(overrides = {}) {
requestTokenRelaySalesCommon: (...args) => authSalesCommon.requestTokenRelaySalesCommon(...args),
environment
});
const customerTableSearch = createCustomerTableSearch({ environment });
const installBaseView = createInstallBaseView({
createSearch: (...args) => customerTableSearch.createSearch(...args),
MODAL_ID: contracts.MODAL_ID,
createCloseIcon: (...args) => contentIcons.createCloseIcon(...args),
formatOracleResponseDate: (...args) => opportunitiesFormatters.formatOracleResponseDate(...args),
@@ -336,6 +338,7 @@ function createContentApplication(overrides = {}) {
environment
});
const serviceRequestsView = createServiceRequestsView({
createSearch: (...args) => customerTableSearch.createSearch(...args),
MODAL_ID: contracts.MODAL_ID,
createCloseIcon: (...args) => contentIcons.createCloseIcon(...args),
formatOracleResponseDate: (...args) => opportunitiesFormatters.formatOracleResponseDate(...args),

View File

@@ -32,6 +32,12 @@
}
.opportunities-extension-install-base-status[data-status="ACTIVE"] { color: #17693a; background: #e1f1e5; }
.opportunities-extension-install-base-status[data-status="UNSUPPORTED"] { color: #855300; background: #fff0d6; }
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-install-base-content,
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-install-base-table,
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-install-base-table th,
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-install-base-table td { color: #f6f4f2; }
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-install-base-message { color: #c9c5c1; }
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-install-base-message[role="alert"] { color: #ffb4ab; }
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-install-base-table th { background: #353331; }
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-install-base-table th,
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-install-base-table td { border-color: #484541; }

View File

@@ -1,4 +1,4 @@
function createInstallBaseView({ MODAL_ID, createCloseIcon, formatOracleResponseDate, environment }) {
function createInstallBaseView({ MODAL_ID, createCloseIcon, formatOracleResponseDate, createSearch, environment }) {
const { document } = environment;
let active = null;
const columns = [
@@ -23,6 +23,16 @@ function createInstallBaseView({ MODAL_ID, createCloseIcon, formatOracleResponse
if (previous.trigger?.isConnected) previous.trigger.focus();
return true;
}
function formatValue(value, type) {
if (value === null || value === undefined || value === "") return "-";
if (type === "date") return formatOracleResponseDate(value);
if ((type === "amount" || type === "quantity") && Number.isFinite(Number(value))) {
return new Intl.NumberFormat("en-US", {
minimumFractionDigits: type === "amount" ? 2 : 0, maximumFractionDigits: type === "amount" ? 2 : 4
}).format(Number(value));
}
return String(value).trim();
}
function createTable(items, loading = false) {
const table = element("table", "opportunities-extension-install-base-table");
table.setAttribute("aria-label", "Installed base items");
@@ -49,14 +59,7 @@ function createInstallBaseView({ MODAL_ID, createCloseIcon, formatOracleResponse
cell.append(bar);
} else {
const value = item[key];
const missing = value === null || value === undefined || value === "";
let text = missing ? "-" : String(value).trim();
if (!missing && type === "date") text = formatOracleResponseDate(value);
if (!missing && (type === "amount" || type === "quantity") && Number.isFinite(Number(value))) {
text = new Intl.NumberFormat("en-US", {
minimumFractionDigits: type === "amount" ? 2 : 0, maximumFractionDigits: type === "amount" ? 2 : 4
}).format(Number(value));
}
const text = formatValue(value, type);
if (type === "status") {
const badge = element("span", "opportunities-extension-install-base-status", text);
badge.dataset.status = text.toUpperCase();
@@ -99,27 +102,44 @@ function createInstallBaseView({ MODAL_ID, createCloseIcon, formatOracleResponse
content.setAttribute("aria-label", "Installed base results");
const status = element("p", "opportunities-extension-install-base-message", "Loading installed base...");
status.setAttribute("role", "status");
let loadedItems = [], loaded = false, failure = "";
const search = createSearch({ label: "Search installed base", onSearch: renderResults,
getValues: item => columns.flatMap(([key, , type]) => [item[key], formatValue(item[key], type)]) });
const toolbar = element("div", "opportunities-extension-customer-table-toolbar");
toolbar.append(search.input);
content.append(status, createTable([], true));
dialog.append(header, content);
dialog.append(header, toolbar, content);
modal.append(dialog);
overlay.append(modal);
active = { modal, trigger };
modal.addEventListener("keydown", event => {
if (event.key === "Tab") {
event.preventDefault();
(document.activeElement === close ? content : close).focus();
const controls = [close, search.input, content];
const index = controls.indexOf(document.activeElement);
controls[(index + (event.shiftKey ? -1 : 1) + controls.length) % controls.length].focus();
}
});
close.focus();
function renderResults() {
if (!loaded || active?.modal !== modal || overlay.hidden) return;
const items = failure ? [] : search.filter(loadedItems);
content.textContent = "";
status.textContent = failure || (loadedItems.length
? `${items.length} of ${loadedItems.length} installed base items${items.length ? "" : ". No matching records."}`
: "No installed base items found for this customer.");
status.setAttribute("role", failure ? "alert" : "status");
content.append(status);
if (!failure && items.length) content.append(createTable(items));
content.scrollTop = 0;
}
return { render(items, error = "") {
// A closed or replaced dialog must never receive a late response.
if (active?.modal !== modal || overlay.hidden) return;
modal.setAttribute("aria-busy", "false");
content.textContent = "";
status.textContent = error || (items.length ? `${items.length} installed base items` : "No installed base items found for this customer.");
status.setAttribute("role", error ? "alert" : "status");
content.append(status);
if (!error && items.length) content.append(createTable(items));
loadedItems = items; failure = error; loaded = true;
search.reset();
renderResults();
} };
}
return { openInstallBaseModal, closeInstallBaseModal, createTable };

View File

@@ -0,0 +1,24 @@
.opportunities-extension-account-plans-dialog.opportunities-extension-install-base-dialog,
.opportunities-extension-account-plans-dialog.opportunities-extension-service-requests-dialog {
grid-template-rows: auto auto minmax(0, 1fr);
}
.opportunities-extension-customer-table-toolbar { padding: 12px 16px; border-bottom: 1px solid #dedbd7; }
.opportunities-extension-customer-table-search {
box-sizing: border-box;
width: min(420px, 100%);
min-width: 0;
height: 38px;
padding: 8px 12px;
border: 1px solid #8f8a85;
border-radius: 4px;
background: #fff;
color: #312d2a;
font: inherit;
font-size: 13px;
}
.opportunities-extension-customer-table-search::placeholder { color: #5f5a55; opacity: 1; }
.opportunities-extension-customer-table-search:focus-visible { outline: 2px solid #00758f; outline-offset: 2px; }
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-table-toolbar { border-color: #484541; }
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-table-search { background: #353331; color: #f6f4f2; border-color: #8f8a85; }
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-table-search::placeholder { color: #c9c5c1; }
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-customer-table-search:focus-visible { outline-color: #59d1e5; }

View File

@@ -0,0 +1,24 @@
function createCustomerTableSearch({ environment }) {
function createSearch({ label, onSearch, getValues }) {
const input = environment.document.createElement("input");
input.type = "search";
input.className = "opportunities-extension-customer-table-search";
input.placeholder = label;
input.setAttribute("aria-label", label);
input.addEventListener("input", onSearch);
const normalize = value => String(value ?? "").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
let index = new WeakMap();
return { input,
reset() { index = new WeakMap(); },
filter(items) {
const terms = normalize(input.value).trim().split(/\s+/).filter(Boolean);
if (!terms.length) return items;
return items.filter(item => {
if (!index.has(item)) index.set(item, getValues(item).map(normalize));
return terms.every(term => index.get(item).some(value => value.includes(term)));
});
}
};
}
return { createSearch };
}

View File

@@ -24,6 +24,12 @@
color: #5f5a55; background: #eceae8;
}
.opportunities-extension-service-request-status[data-status="OPEN"] { color: #17693a; background: #e1f1e5; }
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-service-requests-content,
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-service-requests-table,
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-service-requests-table th,
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-service-requests-table td { color: #f6f4f2; }
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-service-requests-message { color: #c9c5c1; }
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-service-requests-message[role="alert"] { color: #ffb4ab; }
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-service-requests-table th { background: #353331; }
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-service-requests-table th,
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-service-requests-table td { border-color: #484541; }

View File

@@ -1,4 +1,4 @@
function createServiceRequestsView({ MODAL_ID, createCloseIcon, formatOracleResponseDate, environment }) {
function createServiceRequestsView({ MODAL_ID, createCloseIcon, formatOracleResponseDate, createSearch, environment }) {
const { document } = environment;
let active = null;
const columns = [
@@ -27,6 +27,10 @@ function createServiceRequestsView({ MODAL_ID, createCloseIcon, formatOracleResp
if (previous.trigger?.isConnected) previous.trigger.focus();
return true;
}
function formatValue(value, key) {
const text = value === null || value === undefined || value === "" ? "-" : String(value);
return text !== "-" && (key === "created" || key === "lastUpdated") ? formatOracleResponseDate(value) : text;
}
function createTable(items, loading = false) {
const table = element("table", "opportunities-extension-service-requests-table");
table.setAttribute("aria-label", "Customer service requests");
@@ -52,8 +56,7 @@ function createServiceRequestsView({ MODAL_ID, createCloseIcon, formatOracleResp
cell.append(bar);
} else {
const value = getValue(item);
let text = value === null || value === undefined || value === "" ? "-" : String(value);
if (text !== "-" && (key === "created" || key === "lastUpdated")) text = formatOracleResponseDate(value);
const text = formatValue(value, key);
if (key === "status") {
const badge = element("span", "opportunities-extension-service-request-status", text);
badge.dataset.status = text.trim().toUpperCase();
@@ -95,26 +98,43 @@ function createServiceRequestsView({ MODAL_ID, createCloseIcon, formatOracleResp
content.setAttribute("aria-label", "Service requests results");
const status = element("p", "opportunities-extension-service-requests-message", "Loading service requests...");
status.setAttribute("role", "status");
let loadedItems = [], loaded = false, failure = "";
const search = createSearch({ label: "Search service requests", onSearch: renderResults,
getValues: item => columns.flatMap(([key, , getValue]) => [getValue(item), formatValue(getValue(item), key)]) });
const toolbar = element("div", "opportunities-extension-customer-table-toolbar");
toolbar.append(search.input);
content.append(status, createTable([], true));
dialog.append(header, content);
dialog.append(header, toolbar, content);
modal.append(dialog);
overlay.append(modal);
active = { modal, trigger };
modal.addEventListener("keydown", event => {
if (event.key === "Tab") {
event.preventDefault();
(document.activeElement === close ? content : close).focus();
const controls = [close, search.input, content];
const index = controls.indexOf(document.activeElement);
controls[(index + (event.shiftKey ? -1 : 1) + controls.length) % controls.length].focus();
}
});
close.focus();
function renderResults() {
if (!loaded || active?.modal !== modal || overlay.hidden) return;
const items = failure ? [] : search.filter(loadedItems);
content.textContent = "";
status.textContent = failure || (loadedItems.length
? `${items.length} of ${loadedItems.length} service requests${items.length ? "" : ". No matching records."}`
: "No open service requests found for this customer.");
status.setAttribute("role", failure ? "alert" : "status");
content.append(status);
if (!failure && items.length) content.append(createTable(items));
content.scrollTop = 0;
}
return { render(items, error = "") {
if (active?.modal !== modal || overlay.hidden) return;
modal.setAttribute("aria-busy", "false");
content.textContent = "";
status.textContent = error || (items.length ? `${items.length} service requests` : "No open service requests found for this customer.");
status.setAttribute("role", error ? "alert" : "status");
content.append(status);
if (!error && items.length) content.append(createTable(items));
loadedItems = items; failure = error; loaded = true;
search.reset();
renderResults();
} };
}
return { openServiceRequestsModal, closeServiceRequestsModal, createTable };