From b23af9bc7fc052945c23750729dea831a47c03bc Mon Sep 17 00:00:00 2001 From: Mark Veidemanis Date: Sun, 15 Feb 2026 22:20:14 +0000 Subject: [PATCH] Compact interfaces and edit more things inline --- .../mixins/window-content/person-form.html | 193 ++++++++++++++++++ .../window-content/queue-form-inline.html | 44 ++++ core/templates/mixins/wm/widget.html | 77 +++++++ .../partials/ai-workspace-person-widget.html | 44 +++- core/templates/partials/queue-list.html | 8 +- core/views/osint.py | 58 +++++- core/views/people.py | 1 + core/views/queues.py | 16 ++ 8 files changed, 429 insertions(+), 12 deletions(-) create mode 100644 core/templates/mixins/window-content/person-form.html create mode 100644 core/templates/mixins/window-content/queue-form-inline.html create mode 100644 core/templates/mixins/wm/widget.html diff --git a/core/templates/mixins/window-content/person-form.html b/core/templates/mixins/window-content/person-form.html new file mode 100644 index 0000000..8d9d53c --- /dev/null +++ b/core/templates/mixins/window-content/person-form.html @@ -0,0 +1,193 @@ +{% include "mixins/partials/notify.html" %} +{% if page_title is not None %} +

{{ page_title }}

+{% endif %} +{% if page_subtitle is not None %} +

{{ page_subtitle }}

+{% endif %} +{% load crispy_forms_tags %} + + + +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{ hidden }} + {% endfor %} + {% if form.non_field_errors %} +
+
{{ form.non_field_errors }}
+
+ {% endif %} + + {{ form.name|as_crispy_field }} +
+
{{ form.sentiment|as_crispy_field }}
+
{{ form.timezone|as_crispy_field }}
+
{{ form.last_interaction|as_crispy_field }}
+
+ + + +
+
+ {{ form.summary|as_crispy_field }} +
+
+ +
+
+ {{ form.profile|as_crispy_field }} +
+
+ +
+
+ {{ form.revealed|as_crispy_field }} +
+
+ +
+
+ {{ form.likes|as_crispy_field }} +
+
+ +
+
+ {{ form.dislikes|as_crispy_field }} +
+
+ +
+ {% if hide_cancel is not True %} + + {% endif %} + +
+
+ + diff --git a/core/templates/mixins/window-content/queue-form-inline.html b/core/templates/mixins/window-content/queue-form-inline.html new file mode 100644 index 0000000..c5d0dcd --- /dev/null +++ b/core/templates/mixins/window-content/queue-form-inline.html @@ -0,0 +1,44 @@ +{% include "mixins/partials/notify.html" %} +{% if page_title is not None %} +

{{ page_title }}

+{% endif %} +{% if page_subtitle is not None %} +

{{ page_subtitle }}

+{% endif %} +{% load crispy_forms_tags %} + +
+ {% csrf_token %} + {% for hidden in form.hidden_fields %} + {{ hidden }} + {% endfor %} + {% if form.non_field_errors %} +
+
{{ form.non_field_errors }}
+
+ {% endif %} + {{ form|crispy }} +
+ {% if is_inline_edit %} + + {% elif hide_cancel is not True %} + + {% endif %} + +
+
diff --git a/core/templates/mixins/wm/widget.html b/core/templates/mixins/wm/widget.html new file mode 100644 index 0000000..44f1f66 --- /dev/null +++ b/core/templates/mixins/wm/widget.html @@ -0,0 +1,77 @@ +
+
+
+ + +
+
+
+ + +{% block custom_end %} +{% endblock %} diff --git a/core/templates/partials/ai-workspace-person-widget.html b/core/templates/partials/ai-workspace-person-widget.html index f12841b..681aba8 100644 --- a/core/templates/partials/ai-workspace-person-widget.html +++ b/core/templates/partials/ai-workspace-person-widget.html @@ -199,9 +199,14 @@ -
-
+ {% endfor %} diff --git a/core/views/osint.py b/core/views/osint.py index 4cac4c7..bba4692 100644 --- a/core/views/osint.py +++ b/core/views/osint.py @@ -53,6 +53,21 @@ def _column_field_name(column: "OsintColumn") -> str: return str(column.key) +def _safe_icon_class(raw: str | None, default: str) -> str: + icon_class = str(raw or "").strip() + if not icon_class: + return default + cleaned_parts = [] + for part in icon_class.split(): + if not part: + continue + if all(ch.isalnum() or ch in {"-", "_"} for ch in part): + cleaned_parts.append(part) + if not cleaned_parts: + return default + return " ".join(cleaned_parts) + + def _url_with_query(base_url: str, query: dict[str, Any]) -> str: params = {} for key, value in query.items(): @@ -378,6 +393,13 @@ OSINT_SCOPES: dict[str, OsintScopeConfig] = { ), } +OSINT_SCOPE_ICONS: dict[str, str] = { + "people": "fa-solid fa-user-group", + "groups": "fa-solid fa-users", + "personas": "fa-solid fa-masks-theater", + "manipulations": "fa-solid fa-sliders", +} + class OSINTListBase(ObjectList): list_template = "partials/osint/list-table.html" @@ -511,6 +533,10 @@ class OSINTListBase(ObjectList): request_type: str, ) -> list[dict[str, Any]]: context_type = _context_type(request_type) + update_type = "window" if request_type == "widget" else context_type + update_target = ( + "#windows-here" if update_type == "window" else f"#{update_type}s-here" + ) rows = [] for item in object_list: row = {"id": str(item.pk), "cells": [], "actions": []} @@ -524,7 +550,7 @@ class OSINTListBase(ObjectList): update_url = reverse( scope.update_url_name, - kwargs={"type": context_type, "pk": item.pk}, + kwargs={"type": update_type, "pk": item.pk}, ) delete_url = reverse( scope.delete_url_name, @@ -535,7 +561,7 @@ class OSINTListBase(ObjectList): { "mode": "hx-get", "url": update_url, - "target": f"#{context_type}s-here", + "target": update_target, "icon": "fa-solid fa-pencil", "title": "Edit", } @@ -653,6 +679,10 @@ class OSINTListBase(ObjectList): context["osint_show_actions"] = True context["osint_search_url"] = list_url context["osint_result_count"] = context["osint_pagination"].get("count", 0) + context["widget_icon"] = _safe_icon_class( + self.request.GET.get("widget_icon"), + OSINT_SCOPE_ICONS.get(scope.key, "fa-solid fa-arrows-minimize"), + ) return context @@ -1030,6 +1060,10 @@ class OSINTSearch(LoginRequiredMixin, View): "unique": "osint-search-widget", "window_content": self.panel_template, "widget_options": 'gs-w="8" gs-h="14" gs-x="0" gs-y="0" gs-min-w="5"', + "widget_icon": _safe_icon_class( + request.GET.get("widget_icon"), + "fa-solid fa-magnifying-glass", + ), **context, } return render(request, self.widget_template, widget_context) @@ -1054,25 +1088,37 @@ class OSINTWorkspaceTabsWidget(LoginRequiredMixin, View): "key": "people", "label": "People", "icon": "fa-solid fa-user-group", - "widget_url": reverse("people", kwargs={"type": "widget"}), + "widget_url": _url_with_query( + reverse("people", kwargs={"type": "widget"}), + {"widget_icon": "fa-solid fa-user-group"}, + ), }, { "key": "groups", "label": "Groups", "icon": "fa-solid fa-users", - "widget_url": reverse("groups", kwargs={"type": "widget"}), + "widget_url": _url_with_query( + reverse("groups", kwargs={"type": "widget"}), + {"widget_icon": "fa-solid fa-users"}, + ), }, { "key": "personas", "label": "Personas", "icon": "fa-solid fa-masks-theater", - "widget_url": reverse("personas", kwargs={"type": "widget"}), + "widget_url": _url_with_query( + reverse("personas", kwargs={"type": "widget"}), + {"widget_icon": "fa-solid fa-masks-theater"}, + ), }, { "key": "manipulations", "label": "Manipulations", "icon": "fa-solid fa-sliders", - "widget_url": reverse("manipulations", kwargs={"type": "widget"}), + "widget_url": _url_with_query( + reverse("manipulations", kwargs={"type": "widget"}), + {"widget_icon": "fa-solid fa-sliders"}, + ), }, ] context = { diff --git a/core/views/people.py b/core/views/people.py index d86fd47..d7dee91 100644 --- a/core/views/people.py +++ b/core/views/people.py @@ -30,6 +30,7 @@ class PersonCreate(LoginRequiredMixin, ObjectCreate): class PersonUpdate(LoginRequiredMixin, ObjectUpdate): model = Person form_class = PersonForm + window_content = "mixins/window-content/person-form.html" submit_url_name = "person_update" diff --git a/core/views/queues.py b/core/views/queues.py index 7107c33..2cee57f 100644 --- a/core/views/queues.py +++ b/core/views/queues.py @@ -1,3 +1,5 @@ +import re + from asgiref.sync import async_to_sync from django.contrib.auth.mixins import LoginRequiredMixin from django.db import transaction @@ -12,6 +14,7 @@ from core.models import Message, QueuedMessage from core.util import logs log = logs.get_logger("queue") +_INLINE_TARGET_RE = re.compile(r"^#queue-inline-editor-[A-Za-z0-9_-]+$") class AcceptMessageAPI(LoginRequiredMixin, APIView): @@ -92,9 +95,22 @@ class QueueCreate(LoginRequiredMixin, ObjectCreate): class QueueUpdate(LoginRequiredMixin, ObjectUpdate): model = QueuedMessage form_class = QueueForm + window_content = "mixins/window-content/queue-form-inline.html" submit_url_name = "queue_update" + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + raw_target = str(self.request.GET.get("hx_target") or "").strip() + if _INLINE_TARGET_RE.fullmatch(raw_target): + context["submit_target"] = raw_target + else: + context["submit_target"] = "#modals-here" + context["is_inline_edit"] = context["submit_target"].startswith( + "#queue-inline-editor-" + ) + return context + class QueueDelete(LoginRequiredMixin, ObjectDelete): model = QueuedMessage