Visual enhancements for dark mode, along with search functionality for service requests and installed base
This commit is contained in:
@@ -39,6 +39,14 @@ runtime module loader, browser-side eval, ES-module requirement or new dependenc
|
|||||||
|
|
||||||
## Tests and Maintenance
|
## Tests and Maintenance
|
||||||
|
|
||||||
|
Installed Base and Service Requests include a search input above their tables.
|
||||||
|
Search filters loaded records across every displayed column, including raw and
|
||||||
|
formatted numbers/dates, without additional API calls. Matching ignores case
|
||||||
|
and accents; all whitespace-separated terms must match somewhere in the row.
|
||||||
|
The result count updates as users type. Clearing the input restores all records;
|
||||||
|
reopening a dialog starts with an empty search. Search remains available during
|
||||||
|
loading and retains the entered terms when results arrive. Both themes are supported.
|
||||||
|
|
||||||
Each Cloud Consumption plan includes a **Rate card** button beside Contract ID.
|
Each Cloud Consumption plan includes a **Rate card** button beside Contract ID.
|
||||||
It opens an animated right-side drawer with skeleton rows, an all-column search
|
It opens an animated right-side drawer with skeleton rows, an all-column search
|
||||||
and sortable headers. The drawer reuses the Cloud Consumption header component
|
and sortable headers. The drawer reuses the Cloud Consumption header component
|
||||||
|
|||||||
123
dist/chromium/content.js
vendored
123
dist/chromium/content.js
vendored
File diff suppressed because one or more lines are too long
123
dist/firefox/content.js
vendored
123
dist/firefox/content.js
vendored
File diff suppressed because one or more lines are too long
@@ -16,7 +16,7 @@ function composeScript(target, { bootstrap = true } = {}) {
|
|||||||
return `// Source: ${file}\n${source}`;
|
return `// Source: ${file}\n${source}`;
|
||||||
});
|
});
|
||||||
if (target === "content") {
|
if (target === "content") {
|
||||||
const css = ["src/content/styles/redwood.css", "src/features/debug/styles.css", "src/features/opportunities/styles.css", "src/features/install-base/styles.css", "src/features/service-requests/styles.css", "src/features/consumption/plan-detail.css", "src/features/ratecard/styles.css"]
|
const css = ["src/content/styles/redwood.css", "src/features/debug/styles.css", "src/features/opportunities/styles.css", "src/features/install-base/styles.css", "src/features/service-requests/styles.css", "src/features/consumption/plan-detail.css", "src/features/ratecard/styles.css", "src/features/opportunities/customer-table-search.css"]
|
||||||
.map(file => fs.readFileSync(path.join(root, file), "utf8")).join("\n");
|
.map(file => fs.readFileSync(path.join(root, file), "utf8")).join("\n");
|
||||||
parts.unshift(`const redwoodStyles = ${JSON.stringify(css)};`);
|
parts.unshift(`const redwoodStyles = ${JSON.stringify(css)};`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ module.exports = {
|
|||||||
"src/features/consumption/plans-action.js",
|
"src/features/consumption/plans-action.js",
|
||||||
"src/features/install-base/action.js",
|
"src/features/install-base/action.js",
|
||||||
"src/features/install-base/view.js",
|
"src/features/install-base/view.js",
|
||||||
|
"src/features/opportunities/customer-table-search.js",
|
||||||
"src/features/service-requests/action.js",
|
"src/features/service-requests/action.js",
|
||||||
"src/features/service-requests/view.js",
|
"src/features/service-requests/view.js",
|
||||||
"src/features/consumption/plans-view.js",
|
"src/features/consumption/plans-view.js",
|
||||||
|
|||||||
@@ -321,7 +321,9 @@ function createContentApplication(overrides = {}) {
|
|||||||
requestTokenRelaySalesCommon: (...args) => authSalesCommon.requestTokenRelaySalesCommon(...args),
|
requestTokenRelaySalesCommon: (...args) => authSalesCommon.requestTokenRelaySalesCommon(...args),
|
||||||
environment
|
environment
|
||||||
});
|
});
|
||||||
|
const customerTableSearch = createCustomerTableSearch({ environment });
|
||||||
const installBaseView = createInstallBaseView({
|
const installBaseView = createInstallBaseView({
|
||||||
|
createSearch: (...args) => customerTableSearch.createSearch(...args),
|
||||||
MODAL_ID: contracts.MODAL_ID,
|
MODAL_ID: contracts.MODAL_ID,
|
||||||
createCloseIcon: (...args) => contentIcons.createCloseIcon(...args),
|
createCloseIcon: (...args) => contentIcons.createCloseIcon(...args),
|
||||||
formatOracleResponseDate: (...args) => opportunitiesFormatters.formatOracleResponseDate(...args),
|
formatOracleResponseDate: (...args) => opportunitiesFormatters.formatOracleResponseDate(...args),
|
||||||
@@ -336,6 +338,7 @@ function createContentApplication(overrides = {}) {
|
|||||||
environment
|
environment
|
||||||
});
|
});
|
||||||
const serviceRequestsView = createServiceRequestsView({
|
const serviceRequestsView = createServiceRequestsView({
|
||||||
|
createSearch: (...args) => customerTableSearch.createSearch(...args),
|
||||||
MODAL_ID: contracts.MODAL_ID,
|
MODAL_ID: contracts.MODAL_ID,
|
||||||
createCloseIcon: (...args) => contentIcons.createCloseIcon(...args),
|
createCloseIcon: (...args) => contentIcons.createCloseIcon(...args),
|
||||||
formatOracleResponseDate: (...args) => opportunitiesFormatters.formatOracleResponseDate(...args),
|
formatOracleResponseDate: (...args) => opportunitiesFormatters.formatOracleResponseDate(...args),
|
||||||
|
|||||||
@@ -32,6 +32,12 @@
|
|||||||
}
|
}
|
||||||
.opportunities-extension-install-base-status[data-status="ACTIVE"] { color: #17693a; background: #e1f1e5; }
|
.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-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 { 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 th,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-install-base-table td { border-color: #484541; }
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-install-base-table td { border-color: #484541; }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
function createInstallBaseView({ MODAL_ID, createCloseIcon, formatOracleResponseDate, environment }) {
|
function createInstallBaseView({ MODAL_ID, createCloseIcon, formatOracleResponseDate, createSearch, environment }) {
|
||||||
const { document } = environment;
|
const { document } = environment;
|
||||||
let active = null;
|
let active = null;
|
||||||
const columns = [
|
const columns = [
|
||||||
@@ -23,6 +23,16 @@ function createInstallBaseView({ MODAL_ID, createCloseIcon, formatOracleResponse
|
|||||||
if (previous.trigger?.isConnected) previous.trigger.focus();
|
if (previous.trigger?.isConnected) previous.trigger.focus();
|
||||||
return true;
|
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) {
|
function createTable(items, loading = false) {
|
||||||
const table = element("table", "opportunities-extension-install-base-table");
|
const table = element("table", "opportunities-extension-install-base-table");
|
||||||
table.setAttribute("aria-label", "Installed base items");
|
table.setAttribute("aria-label", "Installed base items");
|
||||||
@@ -49,14 +59,7 @@ function createInstallBaseView({ MODAL_ID, createCloseIcon, formatOracleResponse
|
|||||||
cell.append(bar);
|
cell.append(bar);
|
||||||
} else {
|
} else {
|
||||||
const value = item[key];
|
const value = item[key];
|
||||||
const missing = value === null || value === undefined || value === "";
|
const text = formatValue(value, type);
|
||||||
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));
|
|
||||||
}
|
|
||||||
if (type === "status") {
|
if (type === "status") {
|
||||||
const badge = element("span", "opportunities-extension-install-base-status", text);
|
const badge = element("span", "opportunities-extension-install-base-status", text);
|
||||||
badge.dataset.status = text.toUpperCase();
|
badge.dataset.status = text.toUpperCase();
|
||||||
@@ -99,27 +102,44 @@ function createInstallBaseView({ MODAL_ID, createCloseIcon, formatOracleResponse
|
|||||||
content.setAttribute("aria-label", "Installed base results");
|
content.setAttribute("aria-label", "Installed base results");
|
||||||
const status = element("p", "opportunities-extension-install-base-message", "Loading installed base...");
|
const status = element("p", "opportunities-extension-install-base-message", "Loading installed base...");
|
||||||
status.setAttribute("role", "status");
|
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));
|
content.append(status, createTable([], true));
|
||||||
dialog.append(header, content);
|
dialog.append(header, toolbar, content);
|
||||||
modal.append(dialog);
|
modal.append(dialog);
|
||||||
overlay.append(modal);
|
overlay.append(modal);
|
||||||
active = { modal, trigger };
|
active = { modal, trigger };
|
||||||
modal.addEventListener("keydown", event => {
|
modal.addEventListener("keydown", event => {
|
||||||
if (event.key === "Tab") {
|
if (event.key === "Tab") {
|
||||||
event.preventDefault();
|
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();
|
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 = "") {
|
return { render(items, error = "") {
|
||||||
// A closed or replaced dialog must never receive a late response.
|
// A closed or replaced dialog must never receive a late response.
|
||||||
if (active?.modal !== modal || overlay.hidden) return;
|
if (active?.modal !== modal || overlay.hidden) return;
|
||||||
modal.setAttribute("aria-busy", "false");
|
modal.setAttribute("aria-busy", "false");
|
||||||
content.textContent = "";
|
loadedItems = items; failure = error; loaded = true;
|
||||||
status.textContent = error || (items.length ? `${items.length} installed base items` : "No installed base items found for this customer.");
|
search.reset();
|
||||||
status.setAttribute("role", error ? "alert" : "status");
|
renderResults();
|
||||||
content.append(status);
|
|
||||||
if (!error && items.length) content.append(createTable(items));
|
|
||||||
} };
|
} };
|
||||||
}
|
}
|
||||||
return { openInstallBaseModal, closeInstallBaseModal, createTable };
|
return { openInstallBaseModal, closeInstallBaseModal, createTable };
|
||||||
|
|||||||
24
src/features/opportunities/customer-table-search.css
Normal file
24
src/features/opportunities/customer-table-search.css
Normal 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; }
|
||||||
24
src/features/opportunities/customer-table-search.js
Normal file
24
src/features/opportunities/customer-table-search.js
Normal 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 };
|
||||||
|
}
|
||||||
@@ -24,6 +24,12 @@
|
|||||||
color: #5f5a55; background: #eceae8;
|
color: #5f5a55; background: #eceae8;
|
||||||
}
|
}
|
||||||
.opportunities-extension-service-request-status[data-status="OPEN"] { color: #17693a; background: #e1f1e5; }
|
.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 { 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 th,
|
||||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-service-requests-table td { border-color: #484541; }
|
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-service-requests-table td { border-color: #484541; }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
function createServiceRequestsView({ MODAL_ID, createCloseIcon, formatOracleResponseDate, environment }) {
|
function createServiceRequestsView({ MODAL_ID, createCloseIcon, formatOracleResponseDate, createSearch, environment }) {
|
||||||
const { document } = environment;
|
const { document } = environment;
|
||||||
let active = null;
|
let active = null;
|
||||||
const columns = [
|
const columns = [
|
||||||
@@ -27,6 +27,10 @@ function createServiceRequestsView({ MODAL_ID, createCloseIcon, formatOracleResp
|
|||||||
if (previous.trigger?.isConnected) previous.trigger.focus();
|
if (previous.trigger?.isConnected) previous.trigger.focus();
|
||||||
return true;
|
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) {
|
function createTable(items, loading = false) {
|
||||||
const table = element("table", "opportunities-extension-service-requests-table");
|
const table = element("table", "opportunities-extension-service-requests-table");
|
||||||
table.setAttribute("aria-label", "Customer service requests");
|
table.setAttribute("aria-label", "Customer service requests");
|
||||||
@@ -52,8 +56,7 @@ function createServiceRequestsView({ MODAL_ID, createCloseIcon, formatOracleResp
|
|||||||
cell.append(bar);
|
cell.append(bar);
|
||||||
} else {
|
} else {
|
||||||
const value = getValue(item);
|
const value = getValue(item);
|
||||||
let text = value === null || value === undefined || value === "" ? "-" : String(value);
|
const text = formatValue(value, key);
|
||||||
if (text !== "-" && (key === "created" || key === "lastUpdated")) text = formatOracleResponseDate(value);
|
|
||||||
if (key === "status") {
|
if (key === "status") {
|
||||||
const badge = element("span", "opportunities-extension-service-request-status", text);
|
const badge = element("span", "opportunities-extension-service-request-status", text);
|
||||||
badge.dataset.status = text.trim().toUpperCase();
|
badge.dataset.status = text.trim().toUpperCase();
|
||||||
@@ -95,26 +98,43 @@ function createServiceRequestsView({ MODAL_ID, createCloseIcon, formatOracleResp
|
|||||||
content.setAttribute("aria-label", "Service requests results");
|
content.setAttribute("aria-label", "Service requests results");
|
||||||
const status = element("p", "opportunities-extension-service-requests-message", "Loading service requests...");
|
const status = element("p", "opportunities-extension-service-requests-message", "Loading service requests...");
|
||||||
status.setAttribute("role", "status");
|
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));
|
content.append(status, createTable([], true));
|
||||||
dialog.append(header, content);
|
dialog.append(header, toolbar, content);
|
||||||
modal.append(dialog);
|
modal.append(dialog);
|
||||||
overlay.append(modal);
|
overlay.append(modal);
|
||||||
active = { modal, trigger };
|
active = { modal, trigger };
|
||||||
modal.addEventListener("keydown", event => {
|
modal.addEventListener("keydown", event => {
|
||||||
if (event.key === "Tab") {
|
if (event.key === "Tab") {
|
||||||
event.preventDefault();
|
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();
|
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 = "") {
|
return { render(items, error = "") {
|
||||||
if (active?.modal !== modal || overlay.hidden) return;
|
if (active?.modal !== modal || overlay.hidden) return;
|
||||||
modal.setAttribute("aria-busy", "false");
|
modal.setAttribute("aria-busy", "false");
|
||||||
content.textContent = "";
|
loadedItems = items; failure = error; loaded = true;
|
||||||
status.textContent = error || (items.length ? `${items.length} service requests` : "No open service requests found for this customer.");
|
search.reset();
|
||||||
status.setAttribute("role", error ? "alert" : "status");
|
renderResults();
|
||||||
content.append(status);
|
|
||||||
if (!error && items.length) content.append(createTable(items));
|
|
||||||
} };
|
} };
|
||||||
}
|
}
|
||||||
return { openServiceRequestsModal, closeServiceRequestsModal, createTable };
|
return { openServiceRequestsModal, closeServiceRequestsModal, createTable };
|
||||||
|
|||||||
34
tests/customerModalDarkMode.test.cjs
Normal file
34
tests/customerModalDarkMode.test.cjs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
const assert = require("node:assert/strict");
|
||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
const { test } = require("node:test");
|
||||||
|
|
||||||
|
function luminance(hex) {
|
||||||
|
const rgb = hex.slice(1).match(/../g).map(value => parseInt(value, 16) / 255)
|
||||||
|
.map(value => value <= .04045 ? value / 12.92 : ((value + .055) / 1.055) ** 2.4);
|
||||||
|
return rgb[0] * .2126 + rgb[1] * .7152 + rgb[2] * .0722;
|
||||||
|
}
|
||||||
|
function contrast(a, b) {
|
||||||
|
const values = [luminance(a), luminance(b)].sort((x, y) => y - x);
|
||||||
|
return (values[0] + .05) / (values[1] + .05);
|
||||||
|
}
|
||||||
|
for (const feature of ["install-base", "service-requests"]) {
|
||||||
|
test(`${feature}: explicit dark text colors cover cells, headings, counts and errors with readable contrast`, () => {
|
||||||
|
const css = fs.readFileSync(path.join(__dirname, `../src/features/${feature}/styles.css`), "utf8");
|
||||||
|
const rules = [...css.matchAll(/([^{}]+)\{([^{}]+)\}/g)];
|
||||||
|
const prefix = `.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-${feature}`;
|
||||||
|
function color(selector) {
|
||||||
|
const matches = rules.filter(([, selectors, body]) => selectors.split(",").map(value => value.trim()).includes(selector)
|
||||||
|
&& /(?:^|;)\s*color\s*:/.test(body));
|
||||||
|
assert.ok(matches.length, `Missing explicit color for ${selector}`);
|
||||||
|
return matches.at(-1)[2].match(/(?:^|;)\s*color\s*:\s*(#[a-f0-9]{6})/i)[1];
|
||||||
|
}
|
||||||
|
for (const suffix of ["-content", "-table", "-table th", "-table td", "-message", '-message[role="alert"]']) {
|
||||||
|
const foreground = color(prefix + suffix);
|
||||||
|
const background = suffix.endsWith(" th") ? "#353331" : "#292827";
|
||||||
|
assert.ok(contrast(foreground, background) >= 4.5, `${suffix} needs at least 4.5:1 contrast`);
|
||||||
|
}
|
||||||
|
assert.match(css, /color: #17693a; background: #e1f1e5/, "light status colors remain unchanged");
|
||||||
|
assert.match(css, /color: #b8e4c6; background: #214d34/, "dark status colors remain unchanged");
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -96,7 +96,7 @@ test("tabs support arrows, Home and End with a single selected tab and labelled
|
|||||||
|
|
||||||
test("scoped debug styles are packaged after the unchanged main stylesheet", () => {
|
test("scoped debug styles are packaged after the unchanged main stylesheet", () => {
|
||||||
const root = path.join(__dirname, "..");
|
const root = path.join(__dirname, "..");
|
||||||
const css = ["src/content/styles/redwood.css", "src/features/debug/styles.css", "src/features/opportunities/styles.css", "src/features/install-base/styles.css", "src/features/service-requests/styles.css", "src/features/consumption/plan-detail.css", "src/features/ratecard/styles.css"]
|
const css = ["src/content/styles/redwood.css", "src/features/debug/styles.css", "src/features/opportunities/styles.css", "src/features/install-base/styles.css", "src/features/service-requests/styles.css", "src/features/consumption/plan-detail.css", "src/features/ratecard/styles.css", "src/features/opportunities/customer-table-search.css"]
|
||||||
.map(file => fs.readFileSync(path.join(root, file), "utf8")).join("\n");
|
.map(file => fs.readFileSync(path.join(root, file), "utf8")).join("\n");
|
||||||
assert.ok(composeScript("content").includes(`const redwoodStyles = ${JSON.stringify(css)};`));
|
assert.ok(composeScript("content").includes(`const redwoodStyles = ${JSON.stringify(css)};`));
|
||||||
assert.ok(!composeScript("background").includes("opportunities-extension-debug-tabs"));
|
assert.ok(!composeScript("background").includes("opportunities-extension-debug-tabs"));
|
||||||
|
|||||||
@@ -100,6 +100,42 @@ function setup() {
|
|||||||
const customer = { CustomerAccount: { PartyNumber: "customer-123", PartyUniqueName: "Synthetic customer" } };
|
const customer = { CustomerAccount: { PartyNumber: "customer-123", PartyUniqueName: "Synthetic customer" } };
|
||||||
const walk = node => [node, ...node.children.flatMap(walk)];
|
const walk = node => [node, ...node.children.flatMap(walk)];
|
||||||
|
|
||||||
|
test("installed base searches every displayed column and formatted values, including text entered during loading", () => {
|
||||||
|
const { app, overlay, document } = setup();
|
||||||
|
const view = app.installBaseView.openInstallBaseModal(customer);
|
||||||
|
const modal = overlay.children[0];
|
||||||
|
const input = walk(modal).find(node => node.tag === "input");
|
||||||
|
const row = { partyName: "S\u00e3o Customer", productGroupName: "Group Alpha", productName: "Database Beta",
|
||||||
|
currentSupport: 1234.56, orderNumber: "ORD-321", supportStatus: "ACTIVE", quantityType: "Processor",
|
||||||
|
quantity: 7, supportStartDate: "2026-03-12", supportEndDate: "2027-04-13", licenseValue: 9876.54, shipDate: "2020-05-14",
|
||||||
|
hiddenField: "secretword" };
|
||||||
|
input.value = "sao beta";
|
||||||
|
input.listeners.input();
|
||||||
|
assert.equal(modal.attributes["aria-busy"], "true");
|
||||||
|
view.render([row, { productName: "Other product" }]);
|
||||||
|
const rows = () => walk(modal).filter(node => node.tag === "td").length / 12;
|
||||||
|
assert.equal(rows(), 1);
|
||||||
|
for (const query of [...Object.values(row).slice(0, 12), "1,234.56", "9,876.54", "12/03/2026", "13/04/2027", "14/05/2020"]) {
|
||||||
|
input.value = String(query); input.listeners.input();
|
||||||
|
assert.equal(rows(), 1, String(query));
|
||||||
|
assert.equal(walk(modal).find(node => node.tag === "input"), input, "input is not replaced while typing");
|
||||||
|
}
|
||||||
|
input.value = "secretword"; input.listeners.input();
|
||||||
|
assert.equal(rows(), 0);
|
||||||
|
assert.ok(walk(modal).some(node => node.textContent.includes("No matching records.")));
|
||||||
|
input.value = " "; input.listeners.input();
|
||||||
|
assert.equal(rows(), 2);
|
||||||
|
input.focus();
|
||||||
|
modal.listeners.keydown({ key: "Tab", shiftKey: true, preventDefault() {} });
|
||||||
|
assert.equal(document.activeElement.tag, "button");
|
||||||
|
view.render([], "Synthetic failure");
|
||||||
|
input.value = "other"; input.listeners.input();
|
||||||
|
assert.ok(walk(modal).some(node => node.textContent === "Synthetic failure"));
|
||||||
|
app.installBaseView.closeInstallBaseModal();
|
||||||
|
app.installBaseView.openInstallBaseModal(customer).render([row]);
|
||||||
|
assert.equal(walk(overlay).filter(node => node.tag === "td").length, 12, "reopening starts without an invisible filter");
|
||||||
|
});
|
||||||
|
|
||||||
test("modal shows skeleton, renders only requested fields safely and ignores closed/replaced responses", () => {
|
test("modal shows skeleton, renders only requested fields safely and ignores closed/replaced responses", () => {
|
||||||
const { app, overlay, document } = setup();
|
const { app, overlay, document } = setup();
|
||||||
const first = app.installBaseView.openInstallBaseModal(customer);
|
const first = app.installBaseView.openInstallBaseModal(customer);
|
||||||
@@ -122,6 +158,8 @@ test("modal shows skeleton, renders only requested fields safely and ignores clo
|
|||||||
assert.equal(cells[10].textContent, "0.00");
|
assert.equal(cells[10].textContent, "0.00");
|
||||||
assert.ok(cells[8].textContent.includes("2026"));
|
assert.ok(cells[8].textContent.includes("2026"));
|
||||||
current.listeners.keydown({ key: "Tab", preventDefault() {} });
|
current.listeners.keydown({ key: "Tab", preventDefault() {} });
|
||||||
|
assert.equal(document.activeElement.type, "search");
|
||||||
|
current.listeners.keydown({ key: "Tab", preventDefault() {} });
|
||||||
assert.equal(document.activeElement.attributes.role, "region");
|
assert.equal(document.activeElement.attributes.role, "region");
|
||||||
assert.equal(app.installBaseView.closeInstallBaseModal(), true);
|
assert.equal(app.installBaseView.closeInstallBaseModal(), true);
|
||||||
second.render([], "Late error");
|
second.render([], "Late error");
|
||||||
|
|||||||
@@ -158,6 +158,6 @@ test("table styles reserve compact widths, prioritize Name/Customer and keep nes
|
|||||||
assert.ok(remaining > 0);
|
assert.ok(remaining > 0);
|
||||||
assert.ok(remaining / 2 >= 12 + 84, "three customer controls fit after the left inset");
|
assert.ok(remaining / 2 >= 12 + 84, "three customer controls fit after the left inset");
|
||||||
}
|
}
|
||||||
const allCss = ["src/content/styles/redwood.css", "src/features/debug/styles.css", "src/features/opportunities/styles.css", "src/features/install-base/styles.css", "src/features/service-requests/styles.css", "src/features/consumption/plan-detail.css", "src/features/ratecard/styles.css"].map(read).join("\n");
|
const allCss = ["src/content/styles/redwood.css", "src/features/debug/styles.css", "src/features/opportunities/styles.css", "src/features/install-base/styles.css", "src/features/service-requests/styles.css", "src/features/consumption/plan-detail.css", "src/features/ratecard/styles.css", "src/features/opportunities/customer-table-search.css"].map(read).join("\n");
|
||||||
assert.ok(composeScript("content").includes(`const redwoodStyles = ${JSON.stringify(allCss)};`));
|
assert.ok(composeScript("content").includes(`const redwoodStyles = ${JSON.stringify(allCss)};`));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -143,6 +143,35 @@ function setup() {
|
|||||||
}
|
}
|
||||||
const customer = { CustomerAccount: { PartyNumber: "customer-test", PartyUniqueName: "Synthetic customer" } };
|
const customer = { CustomerAccount: { PartyNumber: "customer-test", PartyUniqueName: "Synthetic customer" } };
|
||||||
|
|
||||||
|
test("service request search covers nested and formatted columns and preserves errors and full results on clearing", () => {
|
||||||
|
const { app, overlay } = setup();
|
||||||
|
const view = app.serviceRequestsView.openServiceRequestsModal(customer);
|
||||||
|
const modal = overlay.children[0];
|
||||||
|
const input = walk(modal).find(node => node.tag === "input");
|
||||||
|
const row = { customer: { name: "S\u00e3o Customer", registryId: "REG-123" }, id: "SR-456", severity: "2", status: "Open",
|
||||||
|
subStatus: "Work In Progress", component: { productName: "Database Alpha" }, os: "Linux", csi: "CSI-789",
|
||||||
|
created: "2026-03-12", lastUpdated: "2026-04-13", hiddenField: "secretword" };
|
||||||
|
input.value = "SAO linux"; input.listeners.input();
|
||||||
|
view.render([row, { id: "Other" }]);
|
||||||
|
const rows = () => walk(modal).filter(node => node.tag === "td").length / 11;
|
||||||
|
assert.equal(rows(), 1);
|
||||||
|
for (const query of ["sao", "REG-123", "SR-456", "2", "open", "progress", "alpha", "linux", "CSI-789", "12/03/2026", "13/04/2026", "2026-03-12"]) {
|
||||||
|
input.value = query; input.listeners.input();
|
||||||
|
assert.equal(rows(), 1, query);
|
||||||
|
}
|
||||||
|
input.value = "secretword"; input.listeners.input();
|
||||||
|
assert.equal(rows(), 0);
|
||||||
|
assert.ok(walk(modal).some(node => node.textContent.includes("No matching records.")));
|
||||||
|
input.value = ""; input.listeners.input();
|
||||||
|
assert.equal(rows(), 2);
|
||||||
|
view.render([], "Synthetic failure");
|
||||||
|
input.value = "anything"; input.listeners.input();
|
||||||
|
assert.ok(walk(modal).some(node => node.textContent === "Synthetic failure"));
|
||||||
|
app.serviceRequestsView.closeServiceRequestsModal();
|
||||||
|
app.serviceRequestsView.openServiceRequestsModal(customer).render([row]);
|
||||||
|
assert.equal(walk(overlay).filter(node => node.tag === "td").length, 11);
|
||||||
|
});
|
||||||
|
|
||||||
test("skeleton, exact field mapping, safe text, dates, badge, empty/error and focus states", () => {
|
test("skeleton, exact field mapping, safe text, dates, badge, empty/error and focus states", () => {
|
||||||
const { app, overlay, document } = setup();
|
const { app, overlay, document } = setup();
|
||||||
const trigger = new Element("button", document);
|
const trigger = new Element("button", document);
|
||||||
@@ -169,6 +198,8 @@ test("skeleton, exact field mapping, safe text, dates, badge, empty/error and fo
|
|||||||
assert.equal(cells[9].textContent, app.opportunitiesFormatters.formatOracleResponseDate("2026-03-04"));
|
assert.equal(cells[9].textContent, app.opportunitiesFormatters.formatOracleResponseDate("2026-03-04"));
|
||||||
assert.equal(cells[10].textContent, app.opportunitiesFormatters.formatOracleResponseDate("2026-09-17"));
|
assert.equal(cells[10].textContent, app.opportunitiesFormatters.formatOracleResponseDate("2026-09-17"));
|
||||||
modal.listeners.keydown({ key: "Tab", preventDefault() {} });
|
modal.listeners.keydown({ key: "Tab", preventDefault() {} });
|
||||||
|
assert.equal(document.activeElement.type, "search");
|
||||||
|
modal.listeners.keydown({ key: "Tab", preventDefault() {} });
|
||||||
assert.equal(document.activeElement.attributes.role, "region");
|
assert.equal(document.activeElement.attributes.role, "region");
|
||||||
view.render([]);
|
view.render([]);
|
||||||
assert.ok(walk(modal).some(node => node.textContent === "No open service requests found for this customer."));
|
assert.ok(walk(modal).some(node => node.textContent === "No open service requests found for this customer."));
|
||||||
|
|||||||
Reference in New Issue
Block a user