Refactor to reduce lines

This commit is contained in:
2026-02-18 00:27:59 +00:00
parent a59a0b0329
commit 521692c458
6 changed files with 596 additions and 816 deletions

View File

@@ -27,74 +27,63 @@
<script src="{% static 'js/gridstack-all.js' %}"></script> <script src="{% static 'js/gridstack-all.js' %}"></script>
<script defer src="{% static 'js/magnet.min.js' %}"></script> <script defer src="{% static 'js/magnet.min.js' %}"></script>
<script> <script>
document.addEventListener("restore-scroll", function(event) { document.addEventListener("restore-scroll", function () {
var scrollpos = localStorage.getItem('scrollpos'); var scrollpos = localStorage.getItem("scrollpos");
if (scrollpos) { if (scrollpos) {
window.scrollTo(0, scrollpos) window.scrollTo(0, scrollpos);
}; }
}); });
document.addEventListener("htmx:beforeSwap", function(event) { document.addEventListener("htmx:beforeSwap", function () {
localStorage.setItem('scrollpos', window.scrollY); localStorage.setItem("scrollpos", window.scrollY);
}); });
</script>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Get all "navbar-burger" elements
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
// Add a click event on each of them
$navbarBurgers.forEach( el => {
el.addEventListener('click', () => {
// Get the target from the "data-target" attribute
const target = el.dataset.target;
const $target = document.getElementById(target);
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
el.classList.toggle('is-active');
$target.classList.toggle('is-active');
document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll(".navbar-burger").forEach(function (el) {
el.addEventListener("click", function () {
var target = document.getElementById(el.dataset.target);
el.classList.toggle("is-active");
if (target) {
target.classList.toggle("is-active");
}
}); });
}); });
const composeLink = document.getElementById('nav-compose-link'); var composeLink = document.getElementById("nav-compose-link");
const composeDropdown = document.getElementById('nav-compose-contacts'); var composeDropdown = document.getElementById("nav-compose-contacts");
let composePreviewLoaded = false; var composePreviewLoaded = false;
let composePreviewLoading = false; var composePreviewLoading = false;
if (composeLink && composeDropdown) { if (!composeLink || !composeDropdown) {
composeLink.addEventListener('mouseenter', () => { return;
const previewUrl = composeLink.dataset.previewUrl || ''; }
composeLink.addEventListener("mouseenter", function () {
var previewUrl = composeLink.dataset.previewUrl || "";
if (!previewUrl || composePreviewLoaded || composePreviewLoading) { if (!previewUrl || composePreviewLoaded || composePreviewLoading) {
return; return;
} }
composePreviewLoading = true; composePreviewLoading = true;
fetch(previewUrl, { fetch(previewUrl, {
method: 'GET', method: "GET",
credentials: 'same-origin', credentials: "same-origin",
headers: { 'HX-Request': 'true' }, headers: { "HX-Request": "true" },
}) })
.then((response) => { .then(function (response) {
if (!response.ok) { if (!response.ok) {
throw new Error('Failed contacts preview fetch.'); throw new Error("Failed contacts preview fetch.");
} }
return response.text(); return response.text();
}) })
.then((html) => { .then(function (html) {
composeDropdown.innerHTML = html; composeDropdown.innerHTML = html;
composePreviewLoaded = true; composePreviewLoaded = true;
}) })
.catch(() => { .catch(function () {
composePreviewLoaded = false; composePreviewLoaded = false;
}) })
.finally(() => { .finally(function () {
composePreviewLoading = false; composePreviewLoading = false;
}); });
}); });
}
}); });
</script> </script>
<style> <style>
@@ -112,9 +101,7 @@
opacity:0; opacity:0;
transition: opacity 500ms ease-in; transition: opacity 500ms ease-in;
} }
.htmx-request .htmx-indicator{ .htmx-request .htmx-indicator,
opacity:1
}
.htmx-request.htmx-indicator{ .htmx-request.htmx-indicator{
opacity:1 opacity:1
} }
@@ -142,15 +129,11 @@
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
} }
tr:hover {
cursor:pointer;
background-color:rgba(221, 224, 255, 0.3) !important;
}
a.panel-block { a.panel-block {
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
} }
tr:hover,
a.panel-block:hover { a.panel-block:hover {
cursor:pointer; cursor:pointer;
background-color:rgba(221, 224, 255, 0.3) !important; background-color:rgba(221, 224, 255, 0.3) !important;
@@ -180,11 +163,9 @@
} }
.panel, .box, .modal { .panel, .box, .modal {
/* background-color:rgba(250, 250, 250, 0.5) !important; */
background-color: var(--modal-color) !important; background-color: var(--modal-color) !important;
} }
.modal, .modal.box{ .modal, .modal.box{
/* background-color:rgba(210, 210, 210, 0.9) !important; */
background-color: var(--background-color) !important; background-color: var(--background-color) !important;
} }
.modal-background{ .modal-background{
@@ -218,7 +199,8 @@
background-color:rgba(0, 0, 0, 0.03) !important; background-color:rgba(0, 0, 0, 0.03) !important;
} }
.grid-stack-item-content { .grid-stack-item-content,
.floating-window {
display: flex !important; display: flex !important;
flex-direction: column !important; flex-direction: column !important;
overflow-x: hidden !important; overflow-x: hidden !important;
@@ -239,11 +221,6 @@
} }
.floating-window { .floating-window {
/* background-color:rgba(210, 210, 210, 0.6) !important; */
display: flex !important;
flex-direction: column !important;
overflow-x: hidden !important;
overflow-y: hidden !important;
max-height: 300px; max-height: 300px;
z-index: 9000; z-index: 9000;
position: absolute; position: absolute;
@@ -257,8 +234,7 @@
.float-right { .float-right {
float: right; float: right;
padding-right: 5px; padding: 0 5px;
padding-left: 5px;
} }
.grid-stack-item:hover .ui-resizable-handle { .grid-stack-item:hover .ui-resizable-handle {
display: block !important; display: block !important;

View File

@@ -819,135 +819,104 @@
(function() { (function() {
const personId = "{{ person.id }}"; const personId = "{{ person.id }}";
const canSend = "{{ send_state.can_send|yesno:'1,0' }}" === "1"; const canSend = "{{ send_state.can_send|yesno:'1,0' }}" === "1";
function resizeEditableTextareas(root) { const TAB_NAMES = ["plan_board", "corrections", "engage", "fundamentals", "auto", "ask_ai"];
const defineGlobal = function(name, handler) {
if (typeof window[name] !== "function") {
window[name] = handler;
}
};
const resizeEditableTextareas = function(root) {
if (!root) return; if (!root) return;
root.querySelectorAll('textarea[data-editable="1"]').forEach(function(area) { root.querySelectorAll('textarea[data-editable="1"]').forEach(function(area) {
area.style.height = "auto"; area.style.height = "auto";
area.style.height = Math.max(area.scrollHeight, 72) + "px"; area.style.height = Math.max(area.scrollHeight, 72) + "px";
}); });
}
window.giaEngageSyncSendOverride = function(pid) {
if (pid !== personId) return;
const forceInput = document.getElementById("engage-force-send-" + pid);
const sendBtn = document.getElementById("engage-send-btn-" + pid);
const force =
!!(window.giaWorkspaceState
&& window.giaWorkspaceState[pid]
&& window.giaWorkspaceState[pid].forceSend);
if (forceInput) {
forceInput.value = force ? "1" : "0";
}
if (sendBtn) {
sendBtn.disabled = !canSend && !force;
}
}; };
function setActiveTabHiddenFields(tabName) { const syncActiveTab = function(tabName) {
const root = document.getElementById("mitigation-shell-" + personId); const root = document.getElementById("mitigation-shell-" + personId);
if (!root) return; if (!root) return;
root.querySelectorAll('input[name="active_tab"]').forEach(function(input) { root.querySelectorAll('input[name="active_tab"]').forEach(function(input) {
input.value = tabName; input.value = tabName;
}); });
resizeEditableTextareas(root); resizeEditableTextareas(root);
};
window.giaEngageSyncSendOverride = function(pid) {
if (pid !== personId) return;
const forceInput = document.getElementById("engage-force-send-" + pid);
const sendBtn = document.getElementById("engage-send-btn-" + pid);
const force = !!(window.giaWorkspaceState && window.giaWorkspaceState[pid] && window.giaWorkspaceState[pid].forceSend);
if (forceInput) forceInput.value = force ? "1" : "0";
if (sendBtn) {
sendBtn.disabled = !canSend && !force;
} }
};
window.giaMitigationShowTab = function(pid, tabName) { window.giaMitigationShowTab = function(pid, tabName) {
if (pid !== personId) return; if (pid !== personId) return;
["plan_board", "corrections", "engage", "fundamentals", "auto", "ask_ai"].forEach(function(name) { TAB_NAMES.forEach(function(name) {
const pane = document.getElementById("mitigation-tab-" + personId + "-" + name); const pane = document.getElementById("mitigation-tab-" + personId + "-" + name);
const tab = document.getElementById("mitigation-tab-btn-" + personId + "-" + name); const tab = document.getElementById("mitigation-tab-btn-" + personId + "-" + name);
if (!pane) return; if (!pane) return;
const active = name === tabName; const active = name === tabName;
pane.style.display = active ? "block" : "none"; pane.style.display = active ? "block" : "none";
if (tab) { if (tab) tab.classList.toggle("is-active", active);
tab.classList.toggle("is-active", active);
}
}); });
setActiveTabHiddenFields(tabName); syncActiveTab(tabName);
}; };
window.giaMitigationToggleEdit = function(button) { defineGlobal("giaMitigationToggleEdit", function(button) {
const form = button.closest("form"); const form = button && button.closest ? button.closest("form") : null;
if (!form) return; if (!form) return;
const card = form.closest(".mitigation-artifact-card");
const editing = button.dataset.editState === "edit"; const editing = button.dataset.editState === "edit";
const fields = form.querySelectorAll('[data-editable="1"]');
const toggles = form.querySelectorAll('[data-editable-toggle="1"]');
if (!editing) { if (!editing) {
fields.forEach(function(field) { form.querySelectorAll('[data-editable="1"]').forEach(function(field) { field.removeAttribute("readonly"); });
field.removeAttribute("readonly"); form.querySelectorAll('[data-editable-toggle="1"]').forEach(function(field) { field.removeAttribute("disabled"); });
}); const card = form.closest(".mitigation-artifact-card");
toggles.forEach(function(field) { if (card) card.classList.add("is-editing");
field.removeAttribute("disabled");
});
if (card) {
card.classList.add("is-editing");
}
button.dataset.editState = "edit"; button.dataset.editState = "edit";
button.classList.remove("is-light"); button.classList.remove("is-light");
button.title = "Save"; button.title = "Save";
button.innerHTML = '<span class="icon is-small"><i class="fa-solid fa-check"></i></span>'; button.innerHTML = '<span class="icon is-small"><i class="fa-solid fa-check"></i></span>';
resizeEditableTextareas(form); resizeEditableTextareas(form);
} else { return;
}
form.requestSubmit(); form.requestSubmit();
} });
};
window.giaEngageSetAction = function(pid, action) { defineGlobal("giaEngageSetAction", function(pid, action) {
if (pid !== personId) return;
const actionInput = document.getElementById("engage-action-input-" + pid); const actionInput = document.getElementById("engage-action-input-" + pid);
if (actionInput) { if (actionInput) actionInput.value = action;
actionInput.value = action; if (action === "send") window.giaEngageSyncSendOverride(pid);
} });
if (action === "send") {
window.giaEngageSyncSendOverride(pid);
}
};
window.giaEngageAutoPreview = function(pid) { defineGlobal("giaEngageAutoPreview", function(pid) {
if (pid !== personId) return;
const form = document.getElementById("engage-form-" + pid); const form = document.getElementById("engage-form-" + pid);
if (!form) return; if (!form) return;
window.giaEngageSetAction(pid, "preview"); window.giaEngageSetAction(pid, "preview");
form.requestSubmit(); form.requestSubmit();
}; });
window.giaEngageSetTarget = function(pid, targetId) { window.giaEngageSetTarget = function(pid, targetId) {
if (pid !== personId) return; if (pid !== personId) return;
const normalized = String(targetId || "").trim(); const normalized = String(targetId || "").trim();
const input = document.getElementById("engage-target-input-" + pid); const input = document.getElementById("engage-target-input-" + pid);
if (input) { if (input) input.value = normalized;
input.value = normalized;
}
const parentSelect = document.getElementById("ai-target-select-" + pid); const parentSelect = document.getElementById("ai-target-select-" + pid);
if (parentSelect && normalized) { if (parentSelect && normalized) parentSelect.value = normalized;
parentSelect.value = normalized;
}
}; };
window.giaEngageSelect = function(pid, kind, value, node) { defineGlobal("giaEngageSelect", function(pid, kind, value, node) {
if (pid !== personId) return; const inputId = kind === "share" ? ("engage-share-input-" + pid) : (kind === "framing" ? ("engage-framing-input-" + pid) : "");
let inputId = "";
if (kind === "share") {
inputId = "engage-share-input-" + pid;
} else if (kind === "framing") {
inputId = "engage-framing-input-" + pid;
}
const input = inputId ? document.getElementById(inputId) : null; const input = inputId ? document.getElementById(inputId) : null;
if (input) { if (input) input.value = value;
input.value = value;
}
const li = node && node.closest ? node.closest("li") : null; const li = node && node.closest ? node.closest("li") : null;
if (!li) return; if (!li || !li.parentElement) return;
const ul = li.parentElement; Array.from(li.parentElement.children).forEach(function(child) { child.classList.remove("is-active"); });
if (!ul) return;
Array.from(ul.children).forEach(function(child) {
child.classList.remove("is-active");
});
li.classList.add("is-active"); li.classList.add("is-active");
window.giaEngageAutoPreview(pid); window.giaEngageAutoPreview(pid);
}; });
window.giaMitigationShowTab(personId, "{{ active_tab|default:'plan_board' }}"); window.giaMitigationShowTab(personId, "{{ active_tab|default:'plan_board' }}");
resizeEditableTextareas(document.getElementById("mitigation-shell-" + personId)); resizeEditableTextareas(document.getElementById("mitigation-shell-" + personId));

View File

@@ -833,8 +833,14 @@
syncTargetInputs(); syncTargetInputs();
}); });
if (typeof window.giaMitigationShowTab !== "function") { const defineGlobal = function(name, handler) {
window.giaMitigationShowTab = function(pid, tabName) { if (typeof window[name] === "function") {
return;
}
window[name] = handler;
};
defineGlobal("giaMitigationShowTab", function(pid, tabName) {
const names = ["plan_board", "corrections", "engage", "fundamentals", "ask_ai"]; const names = ["plan_board", "corrections", "engage", "fundamentals", "ask_ai"];
names.forEach(function(name) { names.forEach(function(name) {
const pane = document.getElementById("mitigation-tab-" + pid + "-" + name); const pane = document.getElementById("mitigation-tab-" + pid + "-" + name);
@@ -855,11 +861,9 @@
shell.querySelectorAll('input[name="active_tab"]').forEach(function(input) { shell.querySelectorAll('input[name="active_tab"]').forEach(function(input) {
input.value = tabName; input.value = tabName;
}); });
}; });
}
if (typeof window.giaMitigationToggleEdit !== "function") { defineGlobal("giaMitigationToggleEdit", function(button) {
window.giaMitigationToggleEdit = function(button) {
const form = button ? button.closest("form") : null; const form = button ? button.closest("form") : null;
if (!form) { if (!form) {
return; return;
@@ -885,31 +889,25 @@
} else { } else {
form.requestSubmit(); form.requestSubmit();
} }
}; });
}
if (typeof window.giaEngageSetAction !== "function") { defineGlobal("giaEngageSetAction", function(pid, action) {
window.giaEngageSetAction = function(pid, action) {
const actionInput = document.getElementById("engage-action-input-" + pid); const actionInput = document.getElementById("engage-action-input-" + pid);
if (actionInput) { if (actionInput) {
actionInput.value = action; actionInput.value = action;
} }
}; });
}
if (typeof window.giaEngageAutoPreview !== "function") { defineGlobal("giaEngageAutoPreview", function(pid) {
window.giaEngageAutoPreview = function(pid) {
const form = document.getElementById("engage-form-" + pid); const form = document.getElementById("engage-form-" + pid);
if (!form) { if (!form) {
return; return;
} }
window.giaEngageSetAction(pid, "preview"); window.giaEngageSetAction(pid, "preview");
form.requestSubmit(); form.requestSubmit();
}; });
}
if (typeof window.giaEngageSelect !== "function") { defineGlobal("giaEngageSelect", function(pid, kind, value, node) {
window.giaEngageSelect = function(pid, kind, value, node) {
let inputId = ""; let inputId = "";
if (kind === "share") { if (kind === "share") {
inputId = "engage-share-input-" + pid; inputId = "engage-share-input-" + pid;
@@ -928,8 +926,7 @@
li.classList.add("is-active"); li.classList.add("is-active");
} }
window.giaEngageAutoPreview(pid); window.giaEngageAutoPreview(pid);
}; });
}
window.giaWorkspaceOpenTab(personId, "plan_board", false); window.giaWorkspaceOpenTab(personId, "plan_board", false);
syncTargetInputs(); syncTargetInputs();

View File

@@ -1650,24 +1650,7 @@
button.classList.add("is-loading"); button.classList.add("is-loading");
setStatus("Requesting history sync…", "info"); setStatus("Requesting history sync…", "info");
try { try {
const payload = new URLSearchParams(); const result = await postFormJson(historySyncUrl, queryParams());
payload.set("service", thread.dataset.service || "");
payload.set("identifier", thread.dataset.identifier || "");
if (thread.dataset.person) {
payload.set("person", thread.dataset.person);
}
payload.set("limit", thread.dataset.limit || "60");
const response = await fetch(historySyncUrl, {
method: "POST",
credentials: "same-origin",
headers: {
"X-CSRFToken": csrfToken,
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json"
},
body: payload.toString(),
});
const result = await response.json();
if (!result.ok) { if (!result.ok) {
setStatus( setStatus(
String(result.message || result.error || "History sync failed."), String(result.message || result.error || "History sync failed."),
@@ -2352,14 +2335,7 @@
} }
panelState.polling = true; panelState.polling = true;
try { try {
const params = new URLSearchParams(); const params = queryParams({ after_ts: String(lastTs) });
params.set("service", thread.dataset.service || "");
params.set("identifier", thread.dataset.identifier || "");
if (thread.dataset.person) {
params.set("person", thread.dataset.person);
}
params.set("limit", thread.dataset.limit || "60");
params.set("after_ts", String(lastTs));
const response = await fetch(thread.dataset.pollUrl + "?" + params.toString(), { const response = await fetch(thread.dataset.pollUrl + "?" + params.toString(), {
method: "GET", method: "GET",
credentials: "same-origin", credentials: "same-origin",
@@ -2532,7 +2508,7 @@
return active; return active;
}; };
const queryParams = function () { const queryParams = function (extraParams) {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.set("service", thread.dataset.service || ""); params.set("service", thread.dataset.service || "");
params.set("identifier", thread.dataset.identifier || ""); params.set("identifier", thread.dataset.identifier || "");
@@ -2540,9 +2516,47 @@
params.set("person", thread.dataset.person); params.set("person", thread.dataset.person);
} }
params.set("limit", thread.dataset.limit || "60"); params.set("limit", thread.dataset.limit || "60");
const extras =
extraParams && typeof extraParams === "object" ? extraParams : {};
Object.keys(extras).forEach(function (key) {
const value = extras[key];
if (value === undefined || value === null || value === "") {
return;
}
params.set(String(key), String(value));
});
return params; return params;
}; };
const postFormJson = async function (url, params) {
const response = await fetch(url, {
method: "POST",
credentials: "same-origin",
headers: {
"X-CSRFToken": csrfToken,
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json"
},
body: params.toString(),
});
if (!response.ok) {
throw new Error("Request failed");
}
return response.json();
};
const getJson = async function (url) {
const response = await fetch(url, {
method: "GET",
credentials: "same-origin",
headers: { Accept: "application/json" }
});
if (!response.ok) {
throw new Error("Request failed");
}
return response.json();
};
const titleCase = function (value) { const titleCase = function (value) {
const raw = String(value || "").trim().toLowerCase(); const raw = String(value || "").trim().toLowerCase();
if (!raw) { if (!raw) {
@@ -2625,6 +2639,17 @@
} }
}; };
const cardContentNode = function (card) {
return card ? card.querySelector(".compose-ai-content") : null;
};
const setCardMessage = function (card, message) {
const node = cardContentNode(card);
if (node) {
node.textContent = String(message || "");
}
};
const openEngage = function (sourceRef) { const openEngage = function (sourceRef) {
const engageCard = showCard("engage"); const engageCard = showCard("engage");
if (!engageCard) { if (!engageCard) {
@@ -2644,19 +2669,19 @@
} }
setCardLoading(card, true); setCardLoading(card, true);
try { try {
const response = await fetch(thread.dataset.draftsUrl + "?" + queryParams().toString(), { const payload = await getJson(
method: "GET", thread.dataset.draftsUrl + "?" + queryParams().toString()
credentials: "same-origin", );
headers: { Accept: "application/json" }
});
const payload = await response.json();
setCardLoading(card, false); setCardLoading(card, false);
if (!payload.ok) { if (!payload.ok) {
card.querySelector(".compose-ai-content").textContent = payload.error || "Failed to load drafts."; setCardMessage(card, payload.error || "Failed to load drafts.");
return; return;
} }
const drafts = Array.isArray(payload.drafts) ? payload.drafts : []; const drafts = Array.isArray(payload.drafts) ? payload.drafts : [];
const container = card.querySelector(".compose-ai-content"); const container = cardContentNode(card);
if (!container) {
return;
}
container.innerHTML = ""; container.innerHTML = "";
const engageButton = document.createElement("button"); const engageButton = document.createElement("button");
engageButton.type = "button"; engageButton.type = "button";
@@ -2695,7 +2720,7 @@
}); });
} catch (err) { } catch (err) {
setCardLoading(card, false); setCardLoading(card, false);
card.querySelector(".compose-ai-content").textContent = "Failed to load drafts."; setCardMessage(card, "Failed to load drafts.");
} }
}; };
@@ -2706,21 +2731,18 @@
} }
setCardLoading(card, true); setCardLoading(card, true);
try { try {
const response = await fetch(thread.dataset.summaryUrl + "?" + queryParams().toString(), { const payload = await getJson(
method: "GET", thread.dataset.summaryUrl + "?" + queryParams().toString()
credentials: "same-origin", );
headers: { Accept: "application/json" }
});
const payload = await response.json();
setCardLoading(card, false); setCardLoading(card, false);
if (!payload.ok) { if (!payload.ok) {
card.querySelector(".compose-ai-content").textContent = payload.error || "Failed to load summary."; setCardMessage(card, payload.error || "Failed to load summary.");
return; return;
} }
card.querySelector(".compose-ai-content").textContent = String(payload.summary || ""); setCardMessage(card, String(payload.summary || ""));
} catch (err) { } catch (err) {
setCardLoading(card, false); setCardLoading(card, false);
card.querySelector(".compose-ai-content").textContent = "Failed to load summary."; setCardMessage(card, "Failed to load summary.");
} }
}; };
@@ -2731,17 +2753,14 @@
} }
setCardLoading(card, true); setCardLoading(card, true);
try { try {
const response = await fetch( const payload = await getJson(
thread.dataset.quickInsightsUrl + "?" + queryParams().toString(), thread.dataset.quickInsightsUrl + "?" + queryParams().toString()
{
method: "GET",
credentials: "same-origin",
headers: { Accept: "application/json" }
}
); );
const payload = await response.json();
setCardLoading(card, false); setCardLoading(card, false);
const container = card.querySelector(".compose-ai-content"); const container = cardContentNode(card);
if (!container) {
return;
}
if (!payload.ok) { if (!payload.ok) {
container.textContent = payload.error || "Failed to load quick insights."; container.textContent = payload.error || "Failed to load quick insights.";
return; return;
@@ -2994,8 +3013,7 @@
} }
} catch (err) { } catch (err) {
setCardLoading(card, false); setCardLoading(card, false);
card.querySelector(".compose-ai-content").textContent = setCardMessage(card, "Failed to load quick insights.");
"Failed to load quick insights.";
} }
}; };
@@ -3035,18 +3053,12 @@
if (showCustom && customValue) { if (showCustom && customValue) {
params.set("custom_text", customValue); params.set("custom_text", customValue);
} }
const response = await fetch( const payload = await getJson(
thread.dataset.engagePreviewUrl + "?" + params.toString(), thread.dataset.engagePreviewUrl + "?" + params.toString()
{
method: "GET",
credentials: "same-origin",
headers: { Accept: "application/json" }
}
); );
const payload = await response.json();
setCardLoading(card, false); setCardLoading(card, false);
if (!payload.ok) { if (!payload.ok) {
card.querySelector(".compose-ai-content").textContent = payload.error || "Failed to load engage preview."; setCardMessage(card, payload.error || "Failed to load engage preview.");
panelState.engageToken = ""; panelState.engageToken = "";
return; return;
} }
@@ -3081,11 +3093,11 @@
if (payload.artifact) { if (payload.artifact) {
text = text + "\n\nSource: " + String(payload.artifact); text = text + "\n\nSource: " + String(payload.artifact);
} }
card.querySelector(".compose-ai-content").textContent = text; setCardMessage(card, text);
sendBtn.disabled = !(confirm.checked && panelState.engageToken); sendBtn.disabled = !(confirm.checked && panelState.engageToken);
} catch (err) { } catch (err) {
setCardLoading(card, false); setCardLoading(card, false);
card.querySelector(".compose-ai-content").textContent = "Failed to load engage preview."; setCardMessage(card, "Failed to load engage preview.");
panelState.engageToken = ""; panelState.engageToken = "";
} finally { } finally {
if (refreshBtn) { if (refreshBtn) {
@@ -3141,27 +3153,13 @@
if (!panelState.engageToken) { if (!panelState.engageToken) {
return; return;
} }
const formData = new URLSearchParams(); const formData = queryParams({
formData.set("service", thread.dataset.service || ""); engage_token: panelState.engageToken,
formData.set("identifier", thread.dataset.identifier || ""); failsafe_arm: confirm.checked ? "1" : "0",
if (thread.dataset.person) { failsafe_confirm: confirm.checked ? "1" : "0",
formData.set("person", thread.dataset.person);
}
formData.set("engage_token", panelState.engageToken);
formData.set("failsafe_arm", confirm.checked ? "1" : "0");
formData.set("failsafe_confirm", confirm.checked ? "1" : "0");
try {
const response = await fetch(thread.dataset.engageSendUrl, {
method: "POST",
credentials: "same-origin",
headers: {
"X-CSRFToken": csrfToken,
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json"
},
body: formData.toString()
}); });
const payload = await response.json(); try {
const payload = await postFormJson(thread.dataset.engageSendUrl, formData);
if (!payload.ok) { if (!payload.ok) {
flashCompose("is-send-fail"); flashCompose("is-send-fail");
setStatus(payload.error || "Engage send failed.", "danger"); setStatus(payload.error || "Engage send failed.", "danger");
@@ -3352,28 +3350,26 @@
// Cancel send support: show a cancel button while the form request is pending. // Cancel send support: show a cancel button while the form request is pending.
let cancelBtn = null; let cancelBtn = null;
const cancelSendRequest = function (commandId) {
return postFormJson(
'{% url "compose_cancel_send" %}',
queryParams({ command_id: String(commandId || "") })
);
};
const showCancelButton = function () { const showCancelButton = function () {
if (cancelBtn) return; if (cancelBtn) return;
cancelBtn = document.createElement('button'); cancelBtn = document.createElement('button');
cancelBtn.type = 'button'; cancelBtn.type = 'button';
cancelBtn.className = 'button is-danger is-light is-small compose-cancel-send-btn'; cancelBtn.className = 'button is-danger is-light is-small compose-cancel-send-btn';
cancelBtn.textContent = 'Cancel Send'; cancelBtn.textContent = 'Cancel Send';
cancelBtn.addEventListener('click', function () { cancelBtn.addEventListener('click', async function () {
// Post cancel by service+identifier try {
const payload = new URLSearchParams(); await cancelSendRequest("");
payload.set('service', thread.dataset.service || ''); } catch (e) {
payload.set('identifier', thread.dataset.identifier || ''); // Ignore cancel failures.
fetch('{% url "compose_cancel_send" %}', { } finally {
method: 'POST',
credentials: 'same-origin',
headers: { 'X-CSRFToken': '{{ csrf_token }}', 'Content-Type': 'application/x-www-form-urlencoded' },
body: payload.toString(),
}).then(function (resp) {
// Hide cancel once requested
hideCancelButton(); hideCancelButton();
}).catch(function () { }
hideCancelButton();
});
}); });
if (statusBox) { if (statusBox) {
statusBox.appendChild(cancelBtn); statusBox.appendChild(cancelBtn);
@@ -3498,24 +3494,16 @@
btn.type = 'button'; btn.type = 'button';
btn.className = 'button is-danger is-light is-small compose-persistent-cancel-btn'; btn.className = 'button is-danger is-light is-small compose-persistent-cancel-btn';
btn.textContent = 'Cancel Queued Send'; btn.textContent = 'Cancel Queued Send';
btn.addEventListener('click', function () { btn.addEventListener('click', async function () {
const payload = new URLSearchParams(); try {
payload.set('service', thread.dataset.service || ''); await cancelSendRequest(String(commandId || ''));
payload.set('identifier', thread.dataset.identifier || '');
payload.set('command_id', String(commandId || ''));
fetch('{% url "compose_cancel_send" %}', {
method: 'POST',
credentials: 'same-origin',
headers: { 'X-CSRFToken': '{{ csrf_token }}', 'Content-Type': 'application/x-www-form-urlencoded' },
body: payload.toString(),
}).then(function (resp) {
stopPendingCommandPolling(); stopPendingCommandPolling();
hidePersistentCancelButton(); hidePersistentCancelButton();
setStatus('Send cancelled.', 'warning'); setStatus('Send cancelled.', 'warning');
poll(true); await poll(true);
}).catch(function () { } catch (e) {
hidePersistentCancelButton(); hidePersistentCancelButton();
}); }
}); });
container.appendChild(btn); container.appendChild(btn);
if (statusBox) { if (statusBox) {

View File

@@ -108,6 +108,17 @@ def _safe_after_ts(raw) -> int:
return max(0, value) return max(0, value)
def _request_scope(request, source: str = "GET"):
data = request.GET if str(source).upper() == "GET" else request.POST
service = _default_service(data.get("service"))
identifier = str(data.get("identifier") or "").strip()
person = None
person_id = data.get("person")
if person_id:
person = get_object_or_404(Person, id=person_id, user=request.user)
return service, identifier, person
def _format_ts_label(ts_value: int) -> str: def _format_ts_label(ts_value: int) -> str:
try: try:
as_dt = datetime.fromtimestamp(int(ts_value) / 1000, tz=dt_timezone.utc) as_dt = datetime.fromtimestamp(int(ts_value) / 1000, tz=dt_timezone.utc)
@@ -2111,12 +2122,7 @@ class ComposeWorkspace(LoginRequiredMixin, View):
template_name = "pages/compose-workspace.html" template_name = "pages/compose-workspace.html"
def get(self, request): def get(self, request):
service = _default_service(request.GET.get("service")) service, identifier, person = _request_scope(request, "GET")
identifier = str(request.GET.get("identifier") or "").strip()
person = None
person_id = request.GET.get("person")
if person_id:
person = Person.objects.filter(id=person_id, user=request.user).first()
limit = _safe_limit(request.GET.get("limit") or 40) limit = _safe_limit(request.GET.get("limit") or 40)
initial_widget_url = "" initial_widget_url = ""
@@ -2307,12 +2313,7 @@ class ComposePage(LoginRequiredMixin, View):
template_name = "pages/compose.html" template_name = "pages/compose.html"
def get(self, request): def get(self, request):
service = _default_service(request.GET.get("service")) service, identifier, person = _request_scope(request, "GET")
identifier = str(request.GET.get("identifier") or "").strip()
person = None
person_id = request.GET.get("person")
if person_id:
person = get_object_or_404(Person, id=person_id, user=request.user)
if not identifier and person is None: if not identifier and person is None:
return HttpResponseBadRequest("Missing contact identifier.") return HttpResponseBadRequest("Missing contact identifier.")
@@ -2328,12 +2329,7 @@ class ComposePage(LoginRequiredMixin, View):
class ComposeWidget(LoginRequiredMixin, View): class ComposeWidget(LoginRequiredMixin, View):
def get(self, request): def get(self, request):
service = _default_service(request.GET.get("service")) service, identifier, person = _request_scope(request, "GET")
identifier = str(request.GET.get("identifier") or "").strip()
person = None
person_id = request.GET.get("person")
if person_id:
person = get_object_or_404(Person, id=person_id, user=request.user)
if not identifier and person is None: if not identifier and person is None:
return HttpResponseBadRequest("Missing contact identifier.") return HttpResponseBadRequest("Missing contact identifier.")
@@ -2361,12 +2357,7 @@ class ComposeWidget(LoginRequiredMixin, View):
class ComposeThread(LoginRequiredMixin, View): class ComposeThread(LoginRequiredMixin, View):
def get(self, request): def get(self, request):
service = _default_service(request.GET.get("service")) service, identifier, person = _request_scope(request, "GET")
identifier = str(request.GET.get("identifier") or "").strip()
person = None
person_id = request.GET.get("person")
if person_id:
person = get_object_or_404(Person, id=person_id, user=request.user)
if not identifier and person is None: if not identifier and person is None:
return HttpResponseBadRequest("Missing contact identifier.") return HttpResponseBadRequest("Missing contact identifier.")
@@ -2565,12 +2556,7 @@ class ComposeHistorySync(LoginRequiredMixin, View):
return len(duplicate_ids) return len(duplicate_ids)
def post(self, request): def post(self, request):
service = _default_service(request.POST.get("service")) service, identifier, person = _request_scope(request, "POST")
identifier = str(request.POST.get("identifier") or "").strip()
person = None
person_id = request.POST.get("person")
if person_id:
person = get_object_or_404(Person, id=person_id, user=request.user)
if not identifier and person is None: if not identifier and person is None:
return JsonResponse( return JsonResponse(
{ {
@@ -2737,8 +2723,7 @@ class ComposeHistorySync(LoginRequiredMixin, View):
class ComposeCancelSend(LoginRequiredMixin, View): class ComposeCancelSend(LoginRequiredMixin, View):
def post(self, request): def post(self, request):
service = _default_service(request.POST.get("service")) service, identifier, _ = _request_scope(request, "POST")
identifier = str(request.POST.get("identifier") or "").strip()
command_id = str(request.POST.get("command_id") or "").strip() command_id = str(request.POST.get("command_id") or "").strip()
if not identifier: if not identifier:
return JsonResponse({"ok": False, "error": "missing_identifier"}) return JsonResponse({"ok": False, "error": "missing_identifier"})
@@ -2842,12 +2827,7 @@ class ComposeMediaBlob(LoginRequiredMixin, View):
class ComposeDrafts(LoginRequiredMixin, View): class ComposeDrafts(LoginRequiredMixin, View):
def get(self, request): def get(self, request):
service = _default_service(request.GET.get("service")) service, identifier, person = _request_scope(request, "GET")
identifier = str(request.GET.get("identifier") or "").strip()
person = None
person_id = request.GET.get("person")
if person_id:
person = get_object_or_404(Person, id=person_id, user=request.user)
if not identifier and person is None: if not identifier and person is None:
return JsonResponse({"ok": False, "error": "Missing contact identifier."}) return JsonResponse({"ok": False, "error": "Missing contact identifier."})
@@ -2908,12 +2888,7 @@ class ComposeDrafts(LoginRequiredMixin, View):
class ComposeSummary(LoginRequiredMixin, View): class ComposeSummary(LoginRequiredMixin, View):
def get(self, request): def get(self, request):
service = _default_service(request.GET.get("service")) service, identifier, person = _request_scope(request, "GET")
identifier = str(request.GET.get("identifier") or "").strip()
person = None
person_id = request.GET.get("person")
if person_id:
person = get_object_or_404(Person, id=person_id, user=request.user)
if not identifier and person is None: if not identifier and person is None:
return JsonResponse({"ok": False, "error": "Missing contact identifier."}) return JsonResponse({"ok": False, "error": "Missing contact identifier."})
@@ -2976,12 +2951,7 @@ class ComposeSummary(LoginRequiredMixin, View):
class ComposeQuickInsights(LoginRequiredMixin, View): class ComposeQuickInsights(LoginRequiredMixin, View):
def get(self, request): def get(self, request):
service = _default_service(request.GET.get("service")) service, identifier, person = _request_scope(request, "GET")
identifier = str(request.GET.get("identifier") or "").strip()
person = None
person_id = request.GET.get("person")
if person_id:
person = get_object_or_404(Person, id=person_id, user=request.user)
if not identifier and person is None: if not identifier and person is None:
return JsonResponse({"ok": False, "error": "Missing contact identifier."}) return JsonResponse({"ok": False, "error": "Missing contact identifier."})
@@ -3097,12 +3067,7 @@ class ComposeQuickInsights(LoginRequiredMixin, View):
class ComposeEngagePreview(LoginRequiredMixin, View): class ComposeEngagePreview(LoginRequiredMixin, View):
def get(self, request): def get(self, request):
service = _default_service(request.GET.get("service")) service, identifier, person = _request_scope(request, "GET")
identifier = str(request.GET.get("identifier") or "").strip()
person = None
person_id = request.GET.get("person")
if person_id:
person = get_object_or_404(Person, id=person_id, user=request.user)
if not identifier and person is None: if not identifier and person is None:
return JsonResponse({"ok": False, "error": "Missing contact identifier."}) return JsonResponse({"ok": False, "error": "Missing contact identifier."})
@@ -3228,12 +3193,7 @@ class ComposeEngagePreview(LoginRequiredMixin, View):
class ComposeEngageSend(LoginRequiredMixin, View): class ComposeEngageSend(LoginRequiredMixin, View):
def post(self, request): def post(self, request):
service = _default_service(request.POST.get("service")) service, identifier, person = _request_scope(request, "POST")
identifier = str(request.POST.get("identifier") or "").strip()
person = None
person_id = request.POST.get("person")
if person_id:
person = get_object_or_404(Person, id=person_id, user=request.user)
if not identifier and person is None: if not identifier and person is None:
return JsonResponse({"ok": False, "error": "Missing contact identifier."}) return JsonResponse({"ok": False, "error": "Missing contact identifier."})
@@ -3319,12 +3279,7 @@ class ComposeSend(LoginRequiredMixin, View):
return response return response
def post(self, request): def post(self, request):
service = _default_service(request.POST.get("service")) service, identifier, person = _request_scope(request, "POST")
identifier = str(request.POST.get("identifier") or "").strip()
person = None
person_id = request.POST.get("person")
if person_id:
person = get_object_or_404(Person, id=person_id, user=request.user)
render_mode = str(request.POST.get("render_mode") or "page").strip().lower() render_mode = str(request.POST.get("render_mode") or "page").strip().lower()
if render_mode not in {"page", "widget"}: if render_mode not in {"page", "widget"}:
render_mode = "page" render_mode = "page"

File diff suppressed because it is too large Load Diff