Adjusted typography, styles, and other visual elements to ensure consistent layouts
This commit is contained in:
108
dist/chromium/content.js
vendored
108
dist/chromium/content.js
vendored
File diff suppressed because one or more lines are too long
108
dist/firefox/content.js
vendored
108
dist/firefox/content.js
vendored
File diff suppressed because one or more lines are too long
@@ -25,7 +25,7 @@ function composeScript(target, { bootstrap = true } = {}) {
|
||||
const calendarLicense = fs.readFileSync(path.join(root, "src/vendor/fullcalendar/LICENSE.md"), "utf8");
|
||||
parts.unshift(`function loadOpportunityCalendarLibrary() {\n/* ${calendarLicense} */\n${calendarLibrary}\nreturn FullCalendar;\n}`);
|
||||
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", "src/features/opportunities/calendar.css"]
|
||||
.concat("src/features/manage-time/styles.css", "src/features/products/ramp-projection.css", "src/features/org-chart/styles.css")
|
||||
.concat("src/features/manage-time/styles.css", "src/features/products/ramp-projection.css", "src/features/org-chart/styles.css", "src/content/styles/tables.css", "src/content/styles/dialogs.css")
|
||||
.map(file => fs.readFileSync(path.join(root, file), "utf8")).join("\n");
|
||||
parts.unshift(`const redwoodStyles = ${JSON.stringify(css)};`);
|
||||
}
|
||||
|
||||
@@ -111,6 +111,15 @@ const os = require("node:os");
|
||||
await page.evaluate(() => { window.fixtureOpportunitiesDelay = 0; });
|
||||
assert.equal(await page.evaluate(() => window.fixtureContactQueries.length), queriesBeforeOpportunities, "Opportunity lookup must reuse the person's PartyNumber without searching contacts");
|
||||
assert.equal(await contactOpportunities.locator("tbody tr").count(), 6);
|
||||
const presentation = await contactOpportunities.locator("table").evaluate(table => ({
|
||||
marker: table.getAttribute("data-table-layout"),
|
||||
textHeader: getComputedStyle(table.querySelector("th")).textAlign,
|
||||
moneyHeader: getComputedStyle(table.querySelector('th[data-align="money"]')).textAlign,
|
||||
moneyCell: getComputedStyle(table.querySelector('td[data-align="money"]')).textAlign,
|
||||
nowrap: getComputedStyle(table.querySelector(".opportunities-extension-sort-label")).whiteSpace
|
||||
}));
|
||||
assert.deepEqual(presentation,{marker:"standard",textHeader:"left",moneyHeader:"right",moneyCell:"right",nowrap:"nowrap"});
|
||||
await page.screenshot({path:path.join(output,"shared-table.png")});
|
||||
assert.equal(await contactOpportunities.getByRole("link", {name:"Contact opportunity 0",exact:true}).getAttribute("target"), "_blank");
|
||||
assert.ok((await contactOpportunities.getByRole("link", {name:"Contact opportunity 0",exact:true}).getAttribute("href")).endsWith("?puid=CONTACT0"));
|
||||
assert.deepEqual((await page.evaluate(()=>window.fixtureOpportunitiesByContact)).map(url=>new URL(url).searchParams.get("offset")), ["0","5"]);
|
||||
|
||||
@@ -404,6 +404,7 @@ function createContentModal({
|
||||
const opportunitiesTable = document.createElement("table");
|
||||
opportunitiesTable.id = OPPORTUNITIES_TABLE_ID;
|
||||
opportunitiesTable.className = "opportunities-extension-table";
|
||||
opportunitiesTable.setAttribute("data-table-layout", "standard");
|
||||
opportunitiesTable.setAttribute("aria-label", "Opportunities");
|
||||
tableSurface.append(opportunitiesTable);
|
||||
|
||||
|
||||
78
src/content/styles/dialogs.css
Normal file
78
src/content/styles/dialogs.css
Normal file
@@ -0,0 +1,78 @@
|
||||
/* Modal titles follow the customer Service Requests heading. Keep section and
|
||||
calendar headings independent from dialog typography. */
|
||||
.opportunities-extension-modal :is(
|
||||
.opportunities-extension-header,
|
||||
.opportunities-extension-account-plans-header,
|
||||
.opportunities-extension-preferences-header
|
||||
) {
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
/* These headers live in panels that already supply the outer inset. */
|
||||
.opportunities-extension-modal :is(.oe-org-dialog.oe-contact-dialog,
|
||||
.oe-org-confirm-panel, .opportunities-extension-projection-confirm,
|
||||
.opportunities-extension-debug-panel) { padding: 16px; }
|
||||
.opportunities-extension-modal :is(.oe-org-editor-header,
|
||||
.opportunities-extension-debug-panel-header) {
|
||||
padding: 0;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.opportunities-extension-modal :is(
|
||||
.opportunities-extension-account-plans-header,
|
||||
.opportunities-extension-preferences-header,
|
||||
.opportunities-extension-debug-panel-header
|
||||
) p {
|
||||
margin: 5px 0 0;
|
||||
font-family: "Oracle Sans", Arial, Helvetica, sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 1.35;
|
||||
letter-spacing: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
/* Search controls share the 16px inset of their table frames. */
|
||||
.opportunities-extension-modal :is(.opportunities-extension-customer-table-toolbar,
|
||||
.opportunities-extension-ratecard-toolbar,
|
||||
.opportunities-extension-ratecard-plan-details) { padding-inline: 16px; }
|
||||
.opportunities-extension-modal .opportunities-extension-customer-table-toolbar {
|
||||
padding-block: 12px;
|
||||
}
|
||||
.opportunities-extension-modal .opportunities-extension-customer-table-search {
|
||||
width: min(420px, 100%);
|
||||
height: 38px;
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.opportunities-extension-modal :is(
|
||||
.opportunities-extension-header,
|
||||
.opportunities-extension-account-plans-header,
|
||||
.opportunities-extension-preferences-header,
|
||||
.opportunities-extension-debug-panel-header,
|
||||
.oe-org-editor-header
|
||||
) :is(h1, h2, h3),
|
||||
.opportunities-extension-modal .opportunities-extension-projection-confirm h2,
|
||||
.opportunities-extension-modal .oe-org-confirm-panel h3 {
|
||||
margin: 0;
|
||||
font-family: "Oracle Sans", Arial, Helvetica, sans-serif;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
line-height: 24px;
|
||||
letter-spacing: normal;
|
||||
color: #312d2a;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.opportunities-extension-modal[data-theme="dark"] :is(
|
||||
.opportunities-extension-header,
|
||||
.opportunities-extension-account-plans-header,
|
||||
.opportunities-extension-preferences-header,
|
||||
.opportunities-extension-debug-panel-header,
|
||||
.oe-org-editor-header
|
||||
) :is(h1, h2, h3),
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-projection-confirm h2,
|
||||
.opportunities-extension-modal[data-theme="dark"] .oe-org-confirm-panel h3 {
|
||||
color: #f6f4f2;
|
||||
}
|
||||
106
src/content/styles/tables.css
Normal file
106
src/content/styles/tables.css
Normal file
@@ -0,0 +1,106 @@
|
||||
/* Shared data-table presentation. Calendar grids are intentionally not data tables. */
|
||||
.opportunities-extension-modal :is(
|
||||
.opportunities-extension-table-surface,
|
||||
.opportunities-extension-install-base-content,
|
||||
.opportunities-extension-service-requests-content,
|
||||
.opportunities-extension-ratecard-viewport,
|
||||
.opportunities-extension-projection-viewport,
|
||||
.oe-time-grid,
|
||||
.oe-contact-opportunities-table
|
||||
) {
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
border: 1px solid var(--oe-table-heading-border);
|
||||
border-radius: 4px;
|
||||
overflow: auto;
|
||||
}
|
||||
/* Unpadded dialogs need an inset on the scrolling frame itself. */
|
||||
.opportunities-extension-modal :is(
|
||||
.opportunities-extension-install-base-content,
|
||||
.opportunities-extension-service-requests-content,
|
||||
.opportunities-extension-ratecard-viewport
|
||||
) { margin: 16px; }
|
||||
/* Other frames already sit inside a padded body: keep a single gutter. */
|
||||
.opportunities-extension-modal :is(
|
||||
.opportunities-extension-body,
|
||||
.opportunities-extension-projection-body,
|
||||
.opportunities-extension-products-detail,
|
||||
.oe-time-body,
|
||||
.oe-org-dialog.oe-contact-opportunities
|
||||
) { padding: 16px; }
|
||||
.opportunities-extension-modal table.opportunities-extension-products-table {
|
||||
border: 1px solid var(--oe-table-heading-border);
|
||||
}
|
||||
.opportunities-extension-modal {
|
||||
--oe-table-heading: #faf9f8;
|
||||
--oe-table-text: #312d2a;
|
||||
--oe-table-border: #ebe8e5;
|
||||
--oe-table-heading-border: #b8b2ad;
|
||||
--oe-table-hover: #f5f4f2;
|
||||
}
|
||||
.opportunities-extension-modal[data-theme="dark"] {
|
||||
--oe-table-heading: #333130;
|
||||
--oe-table-text: #f6f4f2;
|
||||
--oe-table-border: #3f3c39;
|
||||
--oe-table-heading-border: #4e4a46;
|
||||
--oe-table-hover: #35322f;
|
||||
}
|
||||
.opportunities-extension-modal table[data-table-layout="standard"]:not(.fc-scrollgrid) {
|
||||
width: 100%; border-collapse: collapse; border-spacing: 0; table-layout: auto;
|
||||
color: var(--oe-table-text); font-size: 12px; line-height: 1.35;
|
||||
}
|
||||
.opportunities-extension-modal table[data-table-layout="standard"] > thead > tr > th {
|
||||
padding: 10px 12px; height: 44px; vertical-align: middle; text-align: left;
|
||||
white-space: nowrap; overflow-wrap: normal; word-break: normal;
|
||||
font-size: 12px; font-weight: 700; color: var(--oe-table-text);
|
||||
background: var(--oe-table-heading); border-bottom: 1px solid var(--oe-table-heading-border);
|
||||
}
|
||||
.opportunities-extension-modal table[data-table-layout="standard"] > tbody > tr > td {
|
||||
padding: 10px 12px; vertical-align: middle; text-align: left;
|
||||
border-bottom: 1px solid var(--oe-table-border);
|
||||
}
|
||||
.opportunities-extension-modal table[data-table-layout="standard"] > tbody > tr:hover { background: var(--oe-table-hover); }
|
||||
.opportunities-extension-modal table[data-table-layout="standard"] > thead > tr > th .opportunities-extension-sort-button,
|
||||
.opportunities-extension-modal table[data-table-layout="standard"] .opportunities-extension-ratecard-sort {
|
||||
display: flex; align-items: center; justify-content: flex-start; gap: 8px;
|
||||
width: 100%; min-width: max-content; min-height: 28px; padding: 4px 18px 4px 0;
|
||||
border: 0; border-radius: 0; background: transparent; color: inherit;
|
||||
text-align: left; white-space: nowrap; overflow-wrap: normal; font-weight: 700;
|
||||
}
|
||||
.opportunities-extension-modal table[data-table-layout="standard"] .opportunities-extension-sort-label { white-space: nowrap; overflow-wrap: normal; word-break: normal; }
|
||||
.opportunities-extension-modal table[data-table-layout="standard"] > thead > tr > th .opportunities-extension-sort-button::after { position: absolute; right: 0; left: auto; font-size: 12px; }
|
||||
.opportunities-extension-modal table[data-table-layout="standard"] .opportunities-extension-ratecard-sort-icon { margin-left: auto; }
|
||||
/* Currency columns keep their heading and value on the same right edge. */
|
||||
.opportunities-extension-modal table[data-table-layout="standard"] :is(th, td)[data-align="money"] { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.opportunities-extension-modal table[data-table-layout="standard"] > thead > tr > th[data-align="money"] .opportunities-extension-sort-button,
|
||||
.opportunities-extension-modal table[data-table-layout="standard"] th[data-align="money"] .opportunities-extension-ratecard-sort { justify-content: flex-end; text-align: right; padding: 4px 0 4px 18px; }
|
||||
.opportunities-extension-modal table[data-table-layout="standard"] > thead > tr > th[data-align="money"] .opportunities-extension-sort-button::after { left: 0; right: auto; }
|
||||
.opportunities-extension-modal table[data-table-layout="standard"] th[data-align="money"] .opportunities-extension-ratecard-sort-icon { order: -1; margin-left: 0; margin-right: auto; }
|
||||
.opportunities-extension-modal table[data-table-layout="standard"] td[data-align="money"] > span[class*="skeleton"] { margin-left: auto; }
|
||||
/* Preserve room for Overview badges and the compact daily entry controls. */
|
||||
.opportunities-extension-modal table.opportunities-extension-table > tbody > tr > td:is([data-column="name"], [data-column="customer"]) { padding-bottom: 38px; vertical-align: top; }
|
||||
.opportunities-extension-modal .oe-time-grid table[data-table-layout="standard"] > :is(thead, tbody) > tr > :is(th, td) { padding-left: 5px; padding-right: 5px; }
|
||||
.opportunities-extension-modal .oe-time-grid table[data-table-layout="standard"] { table-layout: fixed; }
|
||||
.opportunities-extension-modal table[data-table-layout="standard"] > tbody > tr > th[scope="row"] { padding: 10px 12px; text-align: left; background: var(--oe-table-heading); border-bottom: 1px solid var(--oe-table-border); }
|
||||
/* Rate Card must fit its drawer; headings may wrap at spaces, never mid-word. */
|
||||
.opportunities-extension-modal table.opportunities-extension-ratecard-table[data-table-layout="standard"] {
|
||||
min-width: 0;
|
||||
table-layout: fixed;
|
||||
}
|
||||
.opportunities-extension-modal table.opportunities-extension-ratecard-table[data-table-layout="standard"] > :is(thead, tbody) > tr > :is(th, td) {
|
||||
padding-inline: 6px;
|
||||
}
|
||||
.opportunities-extension-modal table.opportunities-extension-ratecard-table[data-table-layout="standard"] > thead > tr > th,
|
||||
.opportunities-extension-modal table.opportunities-extension-ratecard-table[data-table-layout="standard"] > thead > tr > th .opportunities-extension-ratecard-sort {
|
||||
white-space: normal;
|
||||
overflow-wrap: normal;
|
||||
word-break: normal;
|
||||
hyphens: none;
|
||||
min-width: 0;
|
||||
}
|
||||
.opportunities-extension-modal table.opportunities-extension-ratecard-table[data-table-layout="standard"] > thead > tr > th .opportunities-extension-ratecard-sort {
|
||||
padding: 4px 0;
|
||||
gap: 4px;
|
||||
}
|
||||
.opportunities-extension-modal .opportunities-extension-ratecard-sort > span:first-child { min-width: 0; }
|
||||
.opportunities-extension-modal .opportunities-extension-ratecard-sort-icon { flex: 0 0 12px; }
|
||||
@@ -43,7 +43,6 @@
|
||||
width: 100%; min-width: 1120px; table-layout: fixed; border-collapse: collapse; font-size: 11px;
|
||||
}
|
||||
.opportunities-extension-install-base-table col { width: 7%; }
|
||||
.opportunities-extension-install-base-table .install-base-col-partyName { width: 12%; }
|
||||
.opportunities-extension-install-base-table .install-base-col-productGroupName { width: 13%; }
|
||||
.opportunities-extension-install-base-table .install-base-col-productName { width: 13%; }
|
||||
.opportunities-extension-install-base-table .install-base-col-quantity { width: 5%; }
|
||||
|
||||
@@ -2,7 +2,7 @@ function createInstallBaseView({ MODAL_ID, createCloseIcon, formatOracleResponse
|
||||
const { document } = environment;
|
||||
let active = null;
|
||||
const columns = [
|
||||
["partyName", "Customer"], ["productGroupName", "Product Group"], ["productName", "Product"],
|
||||
["productGroupName", "Product Group"], ["productName", "Product"],
|
||||
["currentSupport", "Current Support", "amount"], ["orderNumber", "Order Number"],
|
||||
["supportStatus", "Support Status", "status"], ["quantityType", "Quantity Type"],
|
||||
["quantity", "Quantity", "quantity"], ["supportStartDate", "Support Start Date", "date"],
|
||||
@@ -36,6 +36,7 @@ function createInstallBaseView({ MODAL_ID, createCloseIcon, formatOracleResponse
|
||||
const sortColumns = columns.map(([key, , type]) => ({ key, type: ["amount", "quantity"].includes(type) ? "number" : type }));
|
||||
function createTable(items, loading = false, sorting = tableSorting.create("installed-base", sortColumns), onSort) {
|
||||
const table = element("table", "opportunities-extension-install-base-table");
|
||||
table.setAttribute("data-table-layout", "standard");
|
||||
table.setAttribute("aria-label", "Installed base items");
|
||||
const head = element("thead");
|
||||
const header = element("tr");
|
||||
|
||||
@@ -114,6 +114,7 @@ function createManageTime({ MODAL_ID, environment, tableSorting, searchableSelec
|
||||
const taskStatus = node("div", "oe-time-status"); taskStatus.setAttribute("role", "status");
|
||||
const taskRetry = button("Retry Task Types", "clock", loadTaskTypes, true); taskRetry.hidden = true;
|
||||
const scroll = node("div", "oe-time-grid"), table = node("table");
|
||||
table.setAttribute("data-table-layout", "standard");
|
||||
table.setAttribute("aria-label", "Weekly time entries"); scroll.append(table);
|
||||
const footer = node("footer", "opportunities-extension-action-form-footer oe-time-footer");
|
||||
const add = button("Add", "add", () => addRow("Service"), true);
|
||||
|
||||
@@ -76,6 +76,7 @@ function createOpportunitiesTable({
|
||||
const isSorted = opportunitiesTableState.opportunitiesTableState.sortKey === column.key;
|
||||
header.scope = "col";
|
||||
header.setAttribute("data-column", column.key);
|
||||
if (column.key === "revenue") header.setAttribute("data-align", "money");
|
||||
|
||||
if (column.sortable === false) {
|
||||
const label = document.createElement("span");
|
||||
@@ -117,6 +118,7 @@ function createOpportunitiesTable({
|
||||
OPPORTUNITIES_COLUMNS.forEach(column => {
|
||||
const cell = document.createElement("td");
|
||||
cell.setAttribute("data-column", column.key);
|
||||
if (column.key === "revenue") cell.setAttribute("data-align", "money");
|
||||
const bar = document.createElement("span");
|
||||
bar.className = "opportunities-extension-account-plans-skeleton-bar opportunities-extension-table-skeleton-text";
|
||||
cell.append(bar);
|
||||
@@ -149,6 +151,7 @@ function createOpportunitiesTable({
|
||||
OPPORTUNITIES_COLUMNS.forEach((column) => {
|
||||
const cell = document.createElement("td");
|
||||
cell.setAttribute("data-column", column.key);
|
||||
if (column.key === "revenue") cell.setAttribute("data-align", "money");
|
||||
|
||||
if (column.key === "stage") {
|
||||
cell.append(createStageBadge(column.display(item)));
|
||||
|
||||
@@ -136,6 +136,7 @@ function createOrgChartView({ environment, tableSorting, model, loadLayout, icon
|
||||
header.append(title, button("Close opportunities", "close", finish));
|
||||
const status = el("p", "oe-org-status"), surface = el("div", "oe-contact-opportunities-table"), table = el("table"), head = el("thead"), row = el("tr"), body = el("tbody");
|
||||
status.setAttribute("role", "status");
|
||||
table.setAttribute("data-table-layout", "standard");
|
||||
const columns = [
|
||||
{ key: "OptyNumber", label: "OptyNumber" }, { key: "WinProb", label: "Win Probability", type: "number" },
|
||||
{ key: "Name", label: "Name" }, { key: "CurrencyCode", label: "CurrencyCode" },
|
||||
@@ -143,7 +144,7 @@ function createOrgChartView({ environment, tableSorting, model, loadLayout, icon
|
||||
];
|
||||
const sorting = tableSorting.create("contact-opportunities", columns);
|
||||
let allItems = [], loading = false;
|
||||
for (const column of columns) { const th = el("th"); th.scope = "col"; sorting.header(th, column.key, column.label, renderRows); row.append(th); }
|
||||
for (const column of columns) { const th = el("th"); th.scope = "col"; if (column.key === "Revenue") th.setAttribute("data-align", "money"); sorting.header(th, column.key, column.label, renderRows); row.append(th); }
|
||||
head.append(row); table.append(head, body); surface.append(table);
|
||||
const footer = el("footer", "oe-org-form-actions");
|
||||
const retry = button("Retry", "plus", () => load(), true);
|
||||
@@ -167,6 +168,7 @@ function createOrgChartView({ environment, tableSorting, model, loadLayout, icon
|
||||
const row = el("tr"); row.setAttribute("aria-hidden", "true");
|
||||
for (const column of columns) {
|
||||
const cell = el("td"), bar = el("span", "oe-contact-skeleton");
|
||||
if (column.key === "Revenue") cell.setAttribute("data-align", "money");
|
||||
bar.style.width = column.key === "Name" ? "90%" : column.key === "WinProb" || column.key === "StatusCode" ? "45%" : "70%";
|
||||
cell.append(bar); row.append(cell);
|
||||
}
|
||||
@@ -177,7 +179,7 @@ function createOrgChartView({ environment, tableSorting, model, loadLayout, icon
|
||||
for (const item of sorting.select(allItems)) {
|
||||
const row = el("tr");
|
||||
for (const key of ["OptyNumber", "WinProb", "Name", "CurrencyCode", "Revenue", "EffectiveDate", "StatusCode"]) {
|
||||
const cell = el("td"); let value = item[key] ?? "—";
|
||||
const cell = el("td"); if (key === "Revenue") cell.setAttribute("data-align", "money"); let value = item[key] ?? "—";
|
||||
if (key === "Name" && item.OptyNumber) {
|
||||
const link = el("a", "", String(value)); link.href = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-detail?puid=" + encodeURIComponent(item.OptyNumber); link.target = "_blank"; link.rel = "noopener noreferrer"; cell.append(link);
|
||||
} else {
|
||||
|
||||
@@ -101,6 +101,7 @@ function createProductsDetails({
|
||||
} else {
|
||||
const table = document.createElement("table");
|
||||
table.className = "opportunities-extension-products-table";
|
||||
table.setAttribute("data-table-layout", "standard");
|
||||
table.setAttribute("aria-label", `Products for opportunity ${optyNumber}`);
|
||||
|
||||
const columns = [
|
||||
@@ -123,6 +124,7 @@ function createProductsDetails({
|
||||
columns.forEach((column) => {
|
||||
const header = document.createElement("th");
|
||||
header.scope = "col";
|
||||
if (column.label === "Amount") header.setAttribute("data-align", "money");
|
||||
header.textContent = column.label;
|
||||
headerRow.append(header);
|
||||
});
|
||||
@@ -133,6 +135,7 @@ function createProductsDetails({
|
||||
const productRow = document.createElement("tr");
|
||||
columns.forEach((column) => {
|
||||
const productCell = document.createElement("td");
|
||||
if (column.label === "Amount") productCell.setAttribute("data-align", "money");
|
||||
if (column.render) {
|
||||
productCell.className = "opportunities-extension-projection-cell";
|
||||
productCell.append(column.render(product));
|
||||
|
||||
@@ -94,6 +94,7 @@ function createRampProjectionView({ MODAL_ID, compare, createAlertIcon, createCl
|
||||
viewport.setAttribute("role", "region");
|
||||
viewport.setAttribute("aria-label", "Monthly consumption comparison");
|
||||
const table = node("table", "opportunities-extension-projection-table");
|
||||
table.setAttribute("data-table-layout", "standard");
|
||||
const caption = node("caption", "opportunities-extension-visually-hidden", `Monthly consumption comparison (${currency}). Difference is forecast minus calculated.`);
|
||||
const head = node("thead"), headRow = node("tr"), tbody = node("tbody");
|
||||
const sorting = tableSorting.create("consumption-projection", [
|
||||
@@ -103,7 +104,7 @@ function createRampProjectionView({ MODAL_ID, compare, createAlertIcon, createCl
|
||||
const rowNodes = new Map();
|
||||
const renderRows = () => tbody.replaceChildren(...sorting.select(result.rows).map(item => rowNodes.get(item)));
|
||||
for (const [key, label] of [["month", "Month"], ["actual", "Forecast"], ["calculated", "Calculated"], ["difference", "Difference"]]) {
|
||||
const th = node("th"); th.scope = "col";
|
||||
const th = node("th"); th.scope = "col"; if (key !== "month") th.setAttribute("data-align", "money");
|
||||
sorting.header(th, key, label, renderRows); headRow.append(th);
|
||||
}
|
||||
head.append(headRow);
|
||||
@@ -114,7 +115,7 @@ function createRampProjectionView({ MODAL_ID, compare, createAlertIcon, createCl
|
||||
month.scope = "row";
|
||||
if (item.prorated) month.append(node("span", "opportunities-extension-projection-prorated", " (Prorated)"));
|
||||
tr.append(month);
|
||||
for (const value of [item.actual, item.calculated, item.difference]) tr.append(node("td", "", value === null ? "-" : formatAmount(value, currency)));
|
||||
for (const value of [item.actual, item.calculated, item.difference]) { const cell = node("td", "", value === null ? "-" : formatAmount(value, currency)); cell.setAttribute("data-align", "money"); tr.append(cell); }
|
||||
rowNodes.set(item, tr);
|
||||
}
|
||||
renderRows();
|
||||
|
||||
@@ -139,3 +139,8 @@
|
||||
.opportunities-extension-ratecard-overlay, .opportunities-extension-ratecard-drawer, .opportunities-extension-ratecard-skeleton,
|
||||
.opportunities-extension-ratecard-overlay[data-closing="true"], .opportunities-extension-ratecard-overlay[data-closing="true"] .opportunities-extension-ratecard-drawer { animation: none; transition: none; }
|
||||
}
|
||||
|
||||
.opportunities-extension-ratecard-toolbar { flex-wrap: wrap; }
|
||||
.opportunities-extension-ratecard-order-filter { position: relative; min-width: 0; }
|
||||
.opportunities-extension-ratecard-order-filter .opportunities-extension-customer-filter-trigger { min-height: 38px; padding-block: 8px; font-size: 13px; }
|
||||
.opportunities-extension-ratecard-order-filter .opportunities-extension-customer-filter-panel { width: min(320px, calc(100vw - 80px)); max-height: 45vh; overflow: auto; }
|
||||
|
||||
@@ -65,12 +65,80 @@ function createRatecardView({ ACCOUNT_PLANS_MODAL_ID, columns, formatValue, sele
|
||||
search.setAttribute("aria-label", "Search rate card");
|
||||
const count = element("span", "count", "Loading rate card...");
|
||||
count.setAttribute("role", "status");
|
||||
toolbar.append(search, count);
|
||||
const orderFilter = element("div", "order-filter");
|
||||
const filterElement = (tag, suffix, text) => {
|
||||
const node = element(tag, null, text);
|
||||
node.className = `opportunities-extension-customer-filter-${suffix}`;
|
||||
return node;
|
||||
};
|
||||
const orderButton = filterElement("button", "trigger", "All order numbers");
|
||||
orderButton.type = "button";
|
||||
orderButton.disabled = true;
|
||||
orderButton.setAttribute("aria-label", "Filter by Order Number");
|
||||
orderButton.setAttribute("aria-expanded", "false");
|
||||
const orderPanel = filterElement("section", "panel");
|
||||
orderPanel.id = "opportunities-extension-ratecard-orders";
|
||||
orderButton.setAttribute("aria-controls", orderPanel.id);
|
||||
orderPanel.hidden = true;
|
||||
const orderSearch = filterElement("input", "search");
|
||||
orderSearch.type = "search";
|
||||
orderSearch.placeholder = "Search order numbers";
|
||||
orderSearch.setAttribute("aria-label", "Search order numbers");
|
||||
const allLabel = filterElement("label", "select-all");
|
||||
const allOrders = element("input");
|
||||
allOrders.type = "checkbox";
|
||||
allLabel.append(allOrders, element("span", null, "Select all"));
|
||||
const orderList = filterElement("div", "list");
|
||||
orderPanel.append(orderSearch, allLabel, orderList);
|
||||
orderFilter.append(orderButton, orderPanel);
|
||||
toolbar.append(search, orderFilter, count);
|
||||
let orders = [], selectedOrders = new Set();
|
||||
const orderKey = item => String(item.orderNumber ?? "").trim();
|
||||
function closeOrders() {
|
||||
orderPanel.hidden = true;
|
||||
orderButton.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
function updateOrders() {
|
||||
const all = orders.length > 0 && selectedOrders.size === orders.length;
|
||||
allOrders.checked = all;
|
||||
allOrders.indeterminate = selectedOrders.size > 0 && !all;
|
||||
orderButton.textContent = all ? "All order numbers" : `${selectedOrders.size} order numbers selected`;
|
||||
orderList.textContent = "";
|
||||
const query = orderSearch.value.trim().toLowerCase();
|
||||
orders.filter(value => (value || "Not provided").toLowerCase().includes(query)).forEach(value => {
|
||||
const label = filterElement("label", "option");
|
||||
const checkbox = element("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.checked = selectedOrders.has(value);
|
||||
checkbox.addEventListener("change", () => {
|
||||
if (checkbox.checked) selectedOrders.add(value); else selectedOrders.delete(value);
|
||||
allOrders.checked = selectedOrders.size === orders.length;
|
||||
allOrders.indeterminate = selectedOrders.size > 0 && !allOrders.checked;
|
||||
orderButton.textContent = allOrders.checked ? "All order numbers" : `${selectedOrders.size} order numbers selected`;
|
||||
renderRows();
|
||||
});
|
||||
label.append(checkbox, element("span", null, value || "Not provided"));
|
||||
orderList.append(label);
|
||||
});
|
||||
}
|
||||
orderButton.addEventListener("click", () => {
|
||||
orderPanel.hidden = !orderPanel.hidden;
|
||||
orderButton.setAttribute("aria-expanded", String(!orderPanel.hidden));
|
||||
if (!orderPanel.hidden) orderSearch.focus();
|
||||
});
|
||||
orderSearch.addEventListener("input", updateOrders);
|
||||
allOrders.addEventListener("change", () => {
|
||||
selectedOrders = new Set(allOrders.checked ? orders : []);
|
||||
updateOrders();
|
||||
renderRows();
|
||||
});
|
||||
root.addEventListener("click", event => { if (!orderFilter.contains(event.target)) closeOrders(); });
|
||||
const viewport = element("div", "viewport");
|
||||
viewport.tabIndex = 0;
|
||||
viewport.setAttribute("role", "region");
|
||||
viewport.setAttribute("aria-label", "Rate card items");
|
||||
const table = element("table", "table");
|
||||
table.setAttribute("data-table-layout", "standard");
|
||||
const caption = element("caption", null, "Rate card items");
|
||||
caption.className = "opportunities-extension-visually-hidden";
|
||||
const colgroup = element("colgroup");
|
||||
@@ -90,6 +158,7 @@ function createRatecardView({ ACCOUNT_PLANS_MODAL_ID, columns, formatValue, sele
|
||||
colgroup.append(col);
|
||||
const th = element("th");
|
||||
th.scope = "col";
|
||||
if (column.type === "price") th.setAttribute("data-align", "money");
|
||||
th.setAttribute("aria-sort", "none");
|
||||
const sort = element("button", "sort");
|
||||
sort.type = "button";
|
||||
@@ -139,7 +208,7 @@ function createRatecardView({ ACCOUNT_PLANS_MODAL_ID, columns, formatValue, sele
|
||||
function renderRows() {
|
||||
if (!alive()) return;
|
||||
body.textContent = "";
|
||||
const visible = selectItems(current.items, search.value, current.sortKey, current.direction);
|
||||
const visible = selectItems(current.items.filter(item => selectedOrders.has(orderKey(item))), search.value, current.sortKey, current.direction);
|
||||
count.textContent = `${visible.length} of ${current.items.length} items`;
|
||||
if (!visible.length) {
|
||||
showNotice(current.items.length ? "No matching rate card items." : "No rate card items found for this plan.");
|
||||
@@ -148,6 +217,7 @@ function createRatecardView({ ACCOUNT_PLANS_MODAL_ID, columns, formatValue, sele
|
||||
const row = element("tr");
|
||||
columns.forEach(column => {
|
||||
const cell = element("td", null, formatValue(item, column));
|
||||
if (column.type === "price") cell.setAttribute("data-align", "money");
|
||||
if (["price", "number", "percent"].includes(column.type)) cell.className = "opportunities-extension-ratecard-numeric";
|
||||
row.append(cell);
|
||||
});
|
||||
@@ -172,9 +242,12 @@ function createRatecardView({ ACCOUNT_PLANS_MODAL_ID, columns, formatValue, sele
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!orderPanel.hidden) { closeOrders(); orderButton.focus(); return; }
|
||||
closeRatecardDrawer();
|
||||
} else if (event.key === "Tab") {
|
||||
const controls = [close, search, viewport, ...headRow.querySelectorAll("button")];
|
||||
const controls = [close, search, ...(!orderButton.disabled ? [orderButton] : []),
|
||||
...(!orderPanel.hidden ? [orderSearch, allOrders, ...orderList.querySelectorAll("input")] : []),
|
||||
viewport, ...headRow.querySelectorAll("button")];
|
||||
const index = controls.indexOf(document.activeElement);
|
||||
if (index < 0 || (event.shiftKey && index === 0) || (!event.shiftKey && index === controls.length - 1)) {
|
||||
event.preventDefault();
|
||||
@@ -196,6 +269,10 @@ function createRatecardView({ ACCOUNT_PLANS_MODAL_ID, columns, formatValue, sele
|
||||
showItems(items) {
|
||||
if (!alive()) return;
|
||||
current.items = items;
|
||||
orders = [...new Set(items.map(orderKey))].sort((a, b) => a.localeCompare(b, "en", { numeric: true }));
|
||||
selectedOrders = new Set(orders);
|
||||
orderButton.disabled = !orders.length;
|
||||
updateOrders();
|
||||
current.status = "ready";
|
||||
panel.setAttribute("aria-busy", "false");
|
||||
renderRows();
|
||||
|
||||
@@ -34,6 +34,7 @@ function createServiceRequestsView({ MODAL_ID, createCloseIcon, formatOracleResp
|
||||
const sortColumns = columns.map(([key, , value]) => ({ key, value, type: ["created", "lastUpdated"].includes(key) ? "date" : "text" }));
|
||||
function createTable(items, loading = false, sorting = tableSorting.create("service-requests", sortColumns), onSort) {
|
||||
const table = element("table", "opportunities-extension-service-requests-table");
|
||||
table.setAttribute("data-table-layout", "standard");
|
||||
table.setAttribute("aria-label", "Customer service requests");
|
||||
const group = element("colgroup");
|
||||
const head = element("thead");
|
||||
|
||||
@@ -31,6 +31,7 @@ test("every source belongs to an explicit runtime composition, with no duplicate
|
||||
const source = read(file);
|
||||
assert.doesNotMatch(source, /\beval\s*\(|\bnew\s+Function\s*\(|\bimportScripts\s*\(|\brequire\s*\(|^\s*(?:import|export)\s/m, file);
|
||||
if (file.endsWith("/application.js") || file.endsWith("/main.js")) continue;
|
||||
if (file === "src/features/org-chart/calling-codes.js") { assert.match(source, /const orgChartCallingCodes\s*=/); continue; }
|
||||
const declaration = source.match(/^function (\w+)\(/m)[1];
|
||||
for (const factory of factories.filter(name => name !== declaration)) {
|
||||
assert.doesNotMatch(source, new RegExp(`\\b${factory}\\s*\\(`), `${file} must receive dependencies, not construct ${factory}`);
|
||||
|
||||
@@ -308,7 +308,7 @@ test("full-page debug CSS defines responsive bounds and readable semantic colors
|
||||
test("scoped debug styles are packaged after the unchanged main stylesheet", () => {
|
||||
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", "src/features/opportunities/customer-table-search.css", "src/features/opportunities/calendar.css"]
|
||||
.concat("src/features/manage-time/styles.css", "src/features/products/ramp-projection.css", "src/features/org-chart/styles.css")
|
||||
.concat("src/features/manage-time/styles.css", "src/features/products/ramp-projection.css", "src/features/org-chart/styles.css", "src/content/styles/tables.css")
|
||||
.map(file => fs.readFileSync(path.join(root, file), "utf8")).join("\n");
|
||||
assert.ok(composeScript("content").includes(`const redwoodStyles = ${JSON.stringify(css)};`));
|
||||
assert.ok(!composeScript("background").includes("opportunities-extension-debug-tabs"));
|
||||
|
||||
@@ -319,6 +319,6 @@ test("table styles reserve compact widths, prioritize Name/Customer and keep nes
|
||||
assert.ok(remaining > 0);
|
||||
assert.ok(remaining / 2 >= 12 + 114, "four 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", "src/features/opportunities/customer-table-search.css", "src/features/opportunities/calendar.css", "src/features/manage-time/styles.css", "src/features/products/ramp-projection.css", "src/features/org-chart/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", "src/features/opportunities/calendar.css", "src/features/manage-time/styles.css", "src/features/products/ramp-projection.css", "src/features/org-chart/styles.css", "src/content/styles/tables.css"].map(read).join("\n");
|
||||
assert.ok(composeScript("content").includes(`const redwoodStyles = ${JSON.stringify(allCss)};`));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user