Implemented the Organization Chart feature to create customer organizational

This commit is contained in:
2026-10-26 18:15:32 -03:00
parent bf48ed1dcb
commit 3229ce9c41
41 changed files with 4099 additions and 63 deletions

View File

@@ -6,7 +6,10 @@ function createSearchableSelect({ environment }) {
if (text !== undefined) node.textContent = text;
return node;
}
function searchableSelect(select, field, onChange, { floating = false, tooltipContent, listWidth = 0, listMaxHeight = 180 } = {}) {
function searchableSelect(select, field, onChange, { floating = false, tooltipContent, listWidth = 0, listMaxHeight = 180, search, searchDelay = 350 } = {}) {
let searchTimer, searchVersion = 0, disposed = false;
const schedule = environment.setTimeout || globalThis.setTimeout, cancel = environment.clearTimeout || globalThis.clearTimeout;
function cancelSearch() { searchVersion++; if (searchTimer) cancel(searchTimer); searchTimer = null; input.removeAttribute("aria-busy"); }
const box = element("div", "opportunities-extension-action-combobox");
const input = element("input");
input.type = "text";
@@ -67,6 +70,7 @@ function createSearchableSelect({ environment }) {
const options = () => Array.from(select.children).filter(item => item.value);
const normalize = value => String(value).normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
function closeList() {
if (search) cancelSearch();
hideTooltip();
list.hidden = true;
input.setAttribute("aria-expanded", "false");
@@ -115,10 +119,10 @@ function createSearchableSelect({ environment }) {
list.style.transform = opensAbove ? "translateY(-100%)" : "none";
}
}
function render(query = "") {
function render(query = "", statusText = "") {
if (input.disabled) return;
hideTooltip();
matches = options().filter(item => normalize(`${item.textContent} ${item.getAttribute?.("data-description") || ""} ${item.value}`).includes(normalize(query)));
matches = statusText ? [] : options().filter(item => search || normalize(`${item.textContent} ${item.getAttribute?.("data-description") || ""} ${item.value}`).includes(normalize(query)));
index = matches.findIndex(item => item.value === select.value);
list.textContent = "";
matches.forEach((item, i) => {
@@ -137,7 +141,7 @@ function createSearchableSelect({ environment }) {
list.append(row);
});
if (!matches.length) {
const empty = element("div", "opportunities-extension-action-option", "No results");
const empty = element("div", "opportunities-extension-action-option", statusText || "No results");
empty.setAttribute("role", "status");
list.append(empty);
}
@@ -147,7 +151,33 @@ function createSearchableSelect({ environment }) {
highlight(false);
}
input.addEventListener("click", () => { render(); input.select?.(); });
input.addEventListener("input", () => render(input.value));
function searchInput(event) {
if (!search) { render(input.value); return; }
cancelSearch(); select.replaceChildren(select.children[0]); select.value = ""; onChange?.();
const query = input.value;
render("", query.trim() ? "Searching..." : "Type to search.");
if (!query.trim() || event?.isComposing) return;
const version = searchVersion;
const isCurrent = () => !disposed && version === searchVersion && input.isConnected && document.activeElement === input;
input.setAttribute("aria-busy", "true");
searchTimer = schedule(async () => {
if (!isCurrent()) return;
try {
const results = await search(query, isCurrent);
if (!isCurrent()) return;
const placeholder = select.children[0]; select.replaceChildren(placeholder);
for (const result of results) {
const option = element("option", "", result.label); option.value = result.value;
if (result.description) option.setAttribute("data-description", result.description);
select.append(option);
}
select.value = ""; render();
} catch (error) { if (isCurrent()) render("", (error.message || "Unable to search contacts.") + " Type to retry."); }
finally { if (isCurrent()) input.removeAttribute("aria-busy"); }
}, searchDelay);
}
input.addEventListener("input", searchInput);
if (search) input.addEventListener("compositionend", searchInput);
input.addEventListener("blur", sync);
if (tooltip) {
input.addEventListener("mouseenter", () => { if (list.hidden) showTooltip(select.value, input); });
@@ -169,7 +199,7 @@ function createSearchableSelect({ environment }) {
} else if (event.key === "Tab") sync();
});
sync();
return { input, sync };
return { input, sync, destroy() { disposed = true; closeList(); } };
}
return searchableSelect;
}

View File

@@ -408,7 +408,7 @@
/* Keep layout rules on the outer table, never on expanded product tables. */
.opportunities-extension-table[data-opportunities-layout="inline-actions"] {
width: 100%;
min-width: 1120px;
min-width: 1168px;
table-layout: fixed;
}
@@ -494,7 +494,7 @@
}
.opportunities-extension-table > tbody > tr > td[data-column="customer"] .opportunities-extension-cell-actions {
min-width: 84px;
min-width: 114px;
}
.opportunities-extension-cell-actions .opportunities-extension-action-button svg {

View File

@@ -7,6 +7,7 @@ function createOpportunitiesTable({
createCloudConsumptionButton,
createInstallBaseButton,
createServiceRequestsButton,
createOrganizationChartButton,
createCopyOpportunityButton,
createDetailLink,
createProductsCountButton,
@@ -122,7 +123,7 @@ function createOpportunitiesTable({
if (column.key === "name" || column.key === "customer") {
const actions = document.createElement("span");
actions.className = "opportunities-extension-cell-actions";
for (let action = 0; action < (column.key === "name" ? 4 : 3); action++) {
for (let action = 0; action < 4; action++) {
const badge = document.createElement("span");
badge.className = "opportunities-extension-account-plans-skeleton-bar opportunities-extension-table-skeleton-badge";
actions.append(badge);
@@ -198,7 +199,7 @@ function createOpportunitiesTable({
);
const customerActions = document.createElement("span");
customerActions.className = "opportunities-extension-cell-actions";
customerActions.append(createCloudConsumptionButton(item), createInstallBaseButton(item), createServiceRequestsButton(item));
customerActions.append(createCloudConsumptionButton(item), createInstallBaseButton(item), createServiceRequestsButton(item), createOrganizationChartButton(item));
customerCellContent.append(customerLink, customerActions);
cell.append(customerCellContent);
} else {