This commit is contained in:
gsinghpal
2026-04-29 03:35:33 -04:00
parent 6ac6d24da6
commit a2fe1fcbcc
61 changed files with 4655 additions and 667 deletions

View File

@@ -0,0 +1,392 @@
/** @odoo-module **/
import { Interaction } from "@web/public/interaction";
import { registry } from "@web/core/registry";
export class PortalSearchLive extends Interaction {
static selector = ".o_portal_wrap";
dynamicContent = {
".fp_subtask_search": {
"t-on-input": this.debounced(this.onSubtaskInput, 80),
"t-on-keydown": this.onSubtaskKeydown,
},
".o_portal_search_panel input[name='search']": {
"t-on-input": this.debounced(this.onGlobalInput, 120),
},
};
setup() {
this._lastQ = new WeakMap();
}
start() {
// Apply once on load (handles server-rendered ?search=... and the empty initial state)
for (const input of this.el.querySelectorAll(".fp_subtask_search")) {
this.filterCard(input);
}
const g = this.el.querySelector(".o_portal_search_panel input[name='search']");
if (g && g.value) {
this.filterTables(g);
}
}
onSubtaskInput(ev) {
this.filterCard(ev.target);
}
onSubtaskKeydown(ev) {
if (ev.key === "Enter") {
ev.preventDefault();
this.filterCard(ev.target);
} else if (ev.key === "Escape") {
ev.target.value = "";
this.filterCard(ev.target);
}
}
onGlobalInput(ev) {
this.filterTables(ev.target);
}
filterCard(input) {
const card = input.closest(".card");
if (!card) {
return;
}
const list = card.querySelector("ul.list-group");
if (!list) {
return;
}
const q = (input.value || "").trim().toLowerCase();
if (this._lastQ.get(input) === q) {
return;
}
this._lastQ.set(input, q);
let visible = 0;
for (const li of list.children) {
if (li.querySelector("form")) {
// Skip inline "+ subtask" forms; show/hide them with their parent row.
continue;
}
if (!q) {
li.style.display = "";
li.classList.remove("d-none");
visible += 1;
continue;
}
const text = (li.textContent || "").toLowerCase();
const match = text.indexOf(q) !== -1;
li.style.display = match ? "" : "none";
li.classList.toggle("d-none", !match);
if (match) {
visible += 1;
}
}
// Toggle a "no matches" empty hint
let hint = card.querySelector(".fp_subtask_no_match");
if (q && visible === 0) {
if (!hint) {
hint = document.createElement("div");
hint.className = "fp_subtask_no_match card-body text-muted small";
hint.textContent = "No sub-tasks match your search.";
list.insertAdjacentElement("afterend", hint);
}
hint.style.display = "";
} else if (hint) {
hint.style.display = "none";
}
}
filterTables(input) {
const q = (input.value || "").trim().toLowerCase();
const tables = this.el.querySelectorAll("table.table, table.o_portal_my_doc_table");
tables.forEach((table) => {
table.querySelectorAll("tbody").forEach((tbody) => {
let visibleTaskRows = 0;
let groupHeaderRow = null;
tbody.querySelectorAll("tr").forEach((row) => {
const isGroupHeader =
row.getAttribute("name") === "grouped_tasks_groupby_columns" ||
row.classList.contains("table-light");
if (isGroupHeader) {
if (groupHeaderRow !== null) {
groupHeaderRow.style.display = visibleTaskRows ? "" : "none";
}
groupHeaderRow = row;
visibleTaskRows = 0;
return;
}
if (!q) {
row.style.display = "";
visibleTaskRows += 1;
return;
}
const text = (row.textContent || "").toLowerCase();
const match = text.indexOf(q) !== -1;
row.style.display = match ? "" : "none";
if (match) {
visibleTaskRows += 1;
}
});
if (groupHeaderRow !== null) {
groupHeaderRow.style.display = visibleTaskRows ? "" : "none";
}
});
});
}
}
registry.category("public.interactions").add("fusion_project_portal.portal_search_live", PortalSearchLive);
// ---------------------------------------------------------------------------
// /my/projects: client-side search + sort + group on the redesigned card grid.
// All projects are server-rendered into the page; this just toggles visibility
// and re-orders existing DOM nodes — no RPC.
// ---------------------------------------------------------------------------
const SORT_KEYS = {
name: (el) => (el.dataset.name || "").toLowerCase(),
pct: (el) => -parseFloat(el.dataset.pct || "0"),
tasks: (el) => -parseInt(el.dataset.tasks || "0", 10),
activity: (el) => {
const v = el.dataset.activity || "";
// sort newest first; missing values go last
return v ? `0${v.split("").reverse().join("")}` : "z";
},
};
const SORT_LABELS = {
name: "Name",
pct: "% Complete",
tasks: "Task Count",
activity: "Last Activity",
};
export class FusionProjectsPage extends Interaction {
static selector = ".fp_projects_page";
dynamicContent = {
".fp_projects_search": {
"t-on-input": this.debounced(this.onSearchInput, 80),
"t-on-keydown": this.onSearchKeydown,
},
".fp_projects_group_picker .btn": {
"t-on-click.prevent.withTarget": this.onGroupClick,
},
".fp_projects_sort_picker .dropdown-item": {
"t-on-click.prevent.withTarget": this.onSortClick,
},
};
setup() {
this._sortKey = "name";
this._group = this.el.dataset.defaultGroup || "status";
this._cards = Array.from(this.el.querySelectorAll(".fp_project_card"));
}
start() {
this._applyGroup(this._group, /*initial=*/ true);
this._applySort();
}
onSearchInput(ev) {
this._applyFilter((ev.target.value || "").trim().toLowerCase());
}
onSearchKeydown(ev) {
if (ev.key === "Escape") {
ev.preventDefault();
ev.target.value = "";
this._applyFilter("");
}
}
onGroupClick(ev, currentTargetEl) {
const group = currentTargetEl.dataset.group;
if (!group || group === this._group) {
return;
}
this.el.querySelectorAll(".fp_projects_group_picker .btn").forEach((b) =>
b.classList.toggle("active", b === currentTargetEl)
);
this._applyGroup(group);
this._applySort();
// Re-apply the active search after re-grouping
const searchEl = this.el.querySelector(".fp_projects_search");
this._applyFilter((searchEl && searchEl.value || "").trim().toLowerCase());
}
onSortClick(ev, currentTargetEl) {
const key = currentTargetEl.dataset.sort;
if (!key || !(key in SORT_KEYS)) {
return;
}
this._sortKey = key;
const labelEl = this.el.querySelector(".fp_sort_label");
if (labelEl) {
labelEl.textContent = SORT_LABELS[key];
}
this.el.querySelectorAll(".fp_projects_sort_picker .dropdown-item").forEach((item) =>
item.classList.toggle("active", item === currentTargetEl)
);
this._applySort();
}
_applyFilter(q) {
let visibleTotal = 0;
const perColumn = new Map();
for (const card of this._cards) {
const matches =
!q ||
(card.dataset.name || "").toLowerCase().includes(q) ||
(card.dataset.customer || "").toLowerCase().includes(q);
card.classList.toggle("d-none", !matches);
if (matches) {
visibleTotal += 1;
const col = card.closest(".fp_projects_col");
const key = col ? (col.dataset.bucket || col.dataset.group || "") : "";
perColumn.set(key, (perColumn.get(key) || 0) + 1);
}
}
// Update column counts and toggle column-empty fallback
for (const col of this.el.querySelectorAll(".fp_projects_col")) {
const key = col.dataset.bucket || col.dataset.group || "";
const n = perColumn.get(key) || 0;
const countEl = col.querySelector(".fp_col_count");
if (countEl) {
countEl.textContent = String(n);
}
const empty = col.querySelector(".fp_col_empty");
if (empty) {
empty.classList.toggle("d-none", n > 0);
}
}
const noMatch = this.el.querySelector(".fp_projects_no_match");
if (noMatch) {
noMatch.classList.toggle("d-none", visibleTotal !== 0 || !q);
}
}
_applyGroup(group, initial = false) {
this._group = group;
this.el.dataset.currentGroup = group;
const cols = this.el.querySelector(".fp_projects_cols");
if (!cols) {
return;
}
if (group === "status") {
// Restore the original 3 status columns rendered server-side
this._restoreStatusColumns(cols);
return;
}
if (group === "none") {
this._renderFlat(cols);
return;
}
if (group === "customer") {
this._renderByCustomer(cols);
return;
}
}
_restoreStatusColumns(cols) {
// Cache the original markup once so we can flip back without RPC
if (!this._originalCols) {
this._originalCols = cols.innerHTML;
} else {
cols.innerHTML = this._originalCols;
this._cards = Array.from(this.el.querySelectorAll(".fp_project_card"));
}
}
_renderFlat(cols) {
if (!this._originalCols) {
this._originalCols = cols.innerHTML;
}
const cards = Array.from(this.el.querySelectorAll(".fp_project_card"));
cols.innerHTML = "";
const col = this._buildColumn("All projects", "gray", cards.length);
col.dataset.group = "all";
const body = col.querySelector(".fp_projects_col_body");
for (const card of cards) {
body.appendChild(card);
}
cols.appendChild(col);
this._cards = cards;
}
_renderByCustomer(cols) {
if (!this._originalCols) {
this._originalCols = cols.innerHTML;
}
const cards = Array.from(this.el.querySelectorAll(".fp_project_card"));
const groups = new Map();
for (const card of cards) {
const key = (card.dataset.customer || "Unassigned").trim() || "Unassigned";
if (!groups.has(key)) {
groups.set(key, []);
}
groups.get(key).push(card);
}
const orderedKeys = Array.from(groups.keys()).sort((a, b) => {
if (a === "Unassigned") return 1;
if (b === "Unassigned") return -1;
return a.localeCompare(b);
});
cols.innerHTML = "";
for (const key of orderedKeys) {
const items = groups.get(key);
const col = this._buildColumn(key, "gray", items.length);
col.dataset.group = key;
const body = col.querySelector(".fp_projects_col_body");
for (const card of items) {
body.appendChild(card);
}
cols.appendChild(col);
}
this._cards = cards;
}
_buildColumn(label, dot, count) {
const col = document.createElement("div");
col.className = "fp_projects_col";
col.innerHTML = `
<div class="fp_projects_col_head">
<span class="fp_col_label"><span class="fp_dot fp_dot_${dot}"></span>${this._escape(label)}</span>
<span class="fp_col_count">${count}</span>
</div>
<div class="fp_projects_col_body"></div>
`;
return col;
}
_escape(s) {
const div = document.createElement("div");
div.textContent = s;
return div.innerHTML;
}
_applySort() {
const fn = SORT_KEYS[this._sortKey] || SORT_KEYS.name;
for (const col of this.el.querySelectorAll(".fp_projects_col")) {
const body = col.querySelector(".fp_projects_col_body");
if (!body) continue;
const cards = Array.from(body.querySelectorAll(".fp_project_card"));
cards.sort((a, b) => {
const av = fn(a);
const bv = fn(b);
if (av < bv) return -1;
if (av > bv) return 1;
return 0;
});
for (const c of cards) {
body.appendChild(c);
}
}
}
}
registry.category("public.interactions").add("fusion_project_portal.projects_page", FusionProjectsPage);