Tightly integrate WhatsApp selectors into existing UIs

This commit is contained in:
2026-02-16 10:51:57 +00:00
parent a38339c809
commit 15af8af6b2
19 changed files with 2846 additions and 156 deletions

View File

@@ -1,53 +1,231 @@
{% include 'mixins/partials/notify.html' %}
<table
class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table"
<div
id="whatsapp-contacts-shell"
hx-target="#whatsapp-contacts-shell"
hx-swap="outerHTML"
hx-trigger="{{ context_object_name_singular }}Event from:body"
hx-get="{{ list_url }}">
<thead>
<th>name</th>
<th>identifier</th>
<th>jid</th>
<th>person</th>
<th>actions</th>
</thead>
{% for item in object_list %}
<tr>
<td>{{ item.name|default:"-" }}</td>
<td>
<code>{{ item.identifier }}</code>
</td>
<td>{{ item.jid|default:"-" }}</td>
<td>{{ item.person_name|default:"-" }}</td>
<td>
<div class="buttons">
{% if type == 'page' %}
<a href="{{ item.compose_page_url }}" class="button" title="Open manual chat">
<span class="icon"><i class="fa-solid fa-paper-plane"></i></span>
</a>
{% else %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{{ item.compose_widget_url }}"
hx-trigger="click"
hx-target="#widgets-here"
hx-swap="afterend"
class="button"
title="Open manual chat widget">
<span class="icon"><i class="fa-solid fa-paper-plane"></i></span>
</button>
{% endif %}
<a href="{{ item.match_url }}" class="button" title="Match identifier">
<span class="icon"><i class="fa-solid fa-link"></i></span>
</a>
<div class="field has-addons is-flex-wrap-wrap" style="margin-bottom: 0.6rem;">
<div class="control is-expanded" style="min-width: 14rem;">
<input
id="whatsapp-contacts-search"
class="input"
type="text"
placeholder="Search WhatsApp contacts...">
</div>
<div class="control">
<button id="whatsapp-contacts-reset" class="button is-light" type="button">
Reset
</button>
</div>
</div>
<div class="osint-results-meta">
<div class="osint-results-meta-left">
<div class="dropdown is-hoverable" id="whatsapp-contacts-columns-dropdown">
<div class="dropdown-trigger">
<button class="button is-small is-light" aria-haspopup="true" aria-controls="whatsapp-contacts-columns-menu">
<span>Show/Hide Fields</span>
<span class="icon is-small"><i class="fa-solid fa-angle-down"></i></span>
</button>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="has-text-grey">No WhatsApp contacts discovered yet.</td>
</tr>
{% endfor %}
</table>
<div class="dropdown-menu" id="whatsapp-contacts-columns-menu" role="menu">
<div class="dropdown-content">
<a class="dropdown-item whatsapp-col-toggle" data-col-index="0" href="#" onclick="return false;">
<span class="icon is-small"><i class="fa-solid fa-check"></i></span>
<span>Name</span>
<span class="is-size-7 has-text-grey ml-2">(name)</span>
</a>
<a class="dropdown-item whatsapp-col-toggle" data-col-index="1" href="#" onclick="return false;">
<span class="icon is-small"><i class="fa-solid fa-check"></i></span>
<span>Identifier</span>
<span class="is-size-7 has-text-grey ml-2">(identifier)</span>
</a>
<a class="dropdown-item whatsapp-col-toggle" data-col-index="2" href="#" onclick="return false;">
<span class="icon is-small"><i class="fa-solid fa-check"></i></span>
<span>JID</span>
<span class="is-size-7 has-text-grey ml-2">(jid)</span>
</a>
<a class="dropdown-item whatsapp-col-toggle" data-col-index="3" href="#" onclick="return false;">
<span class="icon is-small"><i class="fa-solid fa-check"></i></span>
<span>Person</span>
<span class="is-size-7 has-text-grey ml-2">(person)</span>
</a>
</div>
</div>
</div>
<button class="button is-small is-light" type="button" disabled>
<span class="icon is-small"><i class="fa-solid fa-database"></i></span>
<span>Static</span>
</button>
</div>
<p class="osint-results-count">
fetched {{ object_list|length }} result{% if object_list|length != 1 %}s{% endif %}
</p>
</div>
<div class="table-container">
<table
class="table is-fullwidth is-hoverable"
id="whatsapp-contacts-table">
<thead>
<tr>
<th data-whatsapp-col="0" class="whatsapp-col-0">name</th>
<th data-whatsapp-col="1" class="whatsapp-col-1">identifier</th>
<th data-whatsapp-col="2" class="whatsapp-col-2">jid</th>
<th data-whatsapp-col="3" class="whatsapp-col-3">person</th>
<th>actions</th>
</tr>
</thead>
<tbody>
{% for item in object_list %}
<tr data-search="{{ item.name|default:'-'|lower }} {{ item.identifier|lower }} {{ item.jid|default:'-'|lower }} {{ item.person_name|default:'-'|lower }}">
<td data-whatsapp-col="0" class="whatsapp-col-0">{{ item.name|default:"-" }}</td>
<td data-whatsapp-col="1" class="whatsapp-col-1">
<code>{{ item.identifier }}</code>
</td>
<td data-whatsapp-col="2" class="whatsapp-col-2">{{ item.jid|default:"-" }}</td>
<td data-whatsapp-col="3" class="whatsapp-col-3">{{ item.person_name|default:"-" }}</td>
<td>
<div class="buttons">
{% if type == 'page' %}
<a href="{{ item.compose_page_url }}" class="button" title="Open manual chat">
<span class="icon"><i class="fa-solid fa-paper-plane"></i></span>
</a>
{% else %}
<button
type="button"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{{ item.compose_widget_url }}"
hx-trigger="click"
hx-target="#widgets-here"
hx-swap="afterend"
class="button"
title="Open manual chat widget">
<span class="icon"><i class="fa-solid fa-paper-plane"></i></span>
</button>
{% endif %}
<a href="{{ item.match_url }}" class="button" title="Match identifier">
<span class="icon"><i class="fa-solid fa-link"></i></span>
</a>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="has-text-grey">No WhatsApp contacts discovered yet.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<style>
#whatsapp-contacts-shell .osint-results-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.6rem;
flex-wrap: wrap;
margin-bottom: 0.6rem;
}
#whatsapp-contacts-shell .osint-results-meta-left {
display: inline-flex;
align-items: center;
gap: 0.42rem;
flex-wrap: wrap;
}
#whatsapp-contacts-shell .osint-results-count {
margin: 0;
color: #6c757d;
font-size: 0.75rem;
}
#whatsapp-contacts-shell .whatsapp-col-toggle .icon {
color: #3273dc;
}
#whatsapp-contacts-shell .whatsapp-col-toggle.is-hidden-col .icon {
color: #b5b5b5;
}
</style>
<script>
(function () {
const shell = document.getElementById("whatsapp-contacts-shell");
if (!shell) return;
const table = document.getElementById("whatsapp-contacts-table");
if (!table) return;
const rows = Array.from(table.querySelectorAll("tbody tr[data-search]"));
const searchInput = document.getElementById("whatsapp-contacts-search");
const resetBtn = document.getElementById("whatsapp-contacts-reset");
const toggles = Array.from(shell.querySelectorAll(".whatsapp-col-toggle"));
const storageKey = "gia_whatsapp_contacts_hidden_cols_v1";
let hidden = [];
try {
hidden = JSON.parse(localStorage.getItem(storageKey) || "[]");
} catch (e) {
hidden = [];
}
if (!Array.isArray(hidden)) hidden = [];
hidden = hidden.map(String);
function applyFilters() {
const query = String(searchInput?.value || "").trim().toLowerCase();
rows.forEach((row) => {
const hay = String(row.dataset.search || "").toLowerCase();
row.style.display = !query || hay.includes(query) ? "" : "none";
});
}
function applyColumns() {
const hiddenSet = new Set(hidden);
shell.querySelectorAll("[data-whatsapp-col]").forEach((node) => {
const idx = String(node.getAttribute("data-whatsapp-col") || "");
node.style.display = hiddenSet.has(idx) ? "none" : "";
});
toggles.forEach((toggle) => {
const idx = String(toggle.getAttribute("data-col-index") || "");
const isHidden = hiddenSet.has(idx);
toggle.classList.toggle("is-hidden-col", isHidden);
const icon = toggle.querySelector("i");
if (icon) {
icon.className = isHidden ? "fa-solid fa-xmark" : "fa-solid fa-check";
}
});
}
function persistColumns() {
try {
localStorage.setItem(storageKey, JSON.stringify(hidden));
} catch (e) {
// Ignore storage failures.
}
}
toggles.forEach((toggle) => {
toggle.addEventListener("click", function () {
const idx = String(toggle.getAttribute("data-col-index") || "");
if (!idx) return;
if (hidden.includes(idx)) {
hidden = hidden.filter((item) => item !== idx);
} else {
hidden.push(idx);
}
persistColumns();
applyColumns();
});
});
if (searchInput) searchInput.addEventListener("input", applyFilters);
if (resetBtn) {
resetBtn.addEventListener("click", function () {
if (searchInput) searchInput.value = "";
applyFilters();
});
}
applyFilters();
applyColumns();
})();
</script>
</div>