Use plain Bulma themes when possible
This commit is contained in:
@@ -150,9 +150,11 @@
|
|||||||
{% if variant.warn_verbatim_plan %}
|
{% if variant.warn_verbatim_plan %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="9">
|
<td colspan="9">
|
||||||
<article class="command-variant-warning">
|
<article class="message is-warning is-light">
|
||||||
|
<div class="message-body is-size-7 has-text-dark">
|
||||||
<strong>Warning:</strong> <strong>{{ variant.variant_label }}</strong> is in <code>verbatim</code> mode with plan fanout enabled.
|
<strong>Warning:</strong> <strong>{{ variant.variant_label }}</strong> is in <code>verbatim</code> mode with plan fanout enabled.
|
||||||
Recipients will get raw transcript-style output.
|
Recipients will get raw transcript-style output.
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -188,15 +190,17 @@
|
|||||||
|
|
||||||
<h4 class="title is-7" style="margin-top: 0.95rem;">Effective Destinations</h4>
|
<h4 class="title is-7" style="margin-top: 0.95rem;">Effective Destinations</h4>
|
||||||
{% if profile.enabled_egress_bindings %}
|
{% if profile.enabled_egress_bindings %}
|
||||||
<ul class="command-destination-list is-size-7">
|
<div class="content is-size-7" style="margin-bottom: 0.4rem;">
|
||||||
{% for row in profile.enabled_egress_bindings %}
|
{% for row in profile.enabled_egress_bindings %}
|
||||||
<li class="command-destination-item">
|
<p style="margin-bottom: 0.3rem;">
|
||||||
<span class="tag is-link is-light is-rounded is-small">{{ row.service }}</span>
|
<span class="tag is-link is-light is-rounded is-small">{{ row.service }}</span>
|
||||||
<code>{{ row.channel_identifier }}</code>
|
<code>{{ row.channel_identifier }}</code>
|
||||||
</li>
|
</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</div>
|
||||||
<p class="command-destination-summary">{{ profile.enabled_egress_bindings|length }} enabled egress destination{{ profile.enabled_egress_bindings|length|pluralize }}.</p>
|
<article class="notification is-info is-light is-size-7" style="padding: 0.5rem 0.7rem;">
|
||||||
|
{{ profile.enabled_egress_bindings|length }} enabled egress destination{{ profile.enabled_egress_bindings|length|pluralize }}.
|
||||||
|
</article>
|
||||||
{% else %}
|
{% else %}
|
||||||
<article class="notification is-warning is-light is-size-7">No enabled egress destinations. Plan fanout will show sent:0.</article>
|
<article class="notification is-warning is-light is-size-7">No enabled egress destinations. Plan fanout will show sent:0.</article>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -441,51 +445,5 @@
|
|||||||
.command-order-capsule-form + .command-order-capsule-form .command-order-btn {
|
.command-order-capsule-form + .command-order-capsule-form .command-order-btn {
|
||||||
border-top: 1px solid #dbdbdb;
|
border-top: 1px solid #dbdbdb;
|
||||||
}
|
}
|
||||||
.command-variant-warning {
|
|
||||||
border: 1px solid rgba(171, 109, 17, 0.45);
|
|
||||||
background: linear-gradient(180deg, rgba(255, 246, 226, 0.98), rgba(255, 238, 204, 0.95));
|
|
||||||
color: #6e450e;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 0.48rem 0.62rem;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
.command-variant-warning strong {
|
|
||||||
color: #3f2a09;
|
|
||||||
}
|
|
||||||
.command-variant-warning code {
|
|
||||||
color: #5b3a0c;
|
|
||||||
background: rgba(255, 255, 255, 0.55);
|
|
||||||
}
|
|
||||||
.command-destination-list {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.28rem;
|
|
||||||
}
|
|
||||||
.command-destination-item {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.42rem;
|
|
||||||
background: rgba(244, 248, 255, 0.85);
|
|
||||||
border: 1px solid rgba(58, 103, 165, 0.2);
|
|
||||||
border-radius: 7px;
|
|
||||||
padding: 0.26rem 0.38rem;
|
|
||||||
width: fit-content;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
.command-destination-summary {
|
|
||||||
margin-top: 0.44rem;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(239, 247, 255, 0.95);
|
|
||||||
border: 1px solid rgba(58, 103, 165, 0.25);
|
|
||||||
padding: 0.16rem 0.52rem;
|
|
||||||
font-size: 0.73rem;
|
|
||||||
color: #284d7c;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -25,7 +25,19 @@
|
|||||||
<div class="has-text-grey"><code>{{ row.actor_identifier }}</code></div>
|
<div class="has-text-grey"><code>{{ row.actor_identifier }}</code></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td><code>{{ row.payload }}</code></td>
|
<td>
|
||||||
|
{% if row.payload_view.summary_items %}
|
||||||
|
<div class="tags" style="margin-bottom: 0.35rem;">
|
||||||
|
{% for item in row.payload_view.summary_items %}
|
||||||
|
<span class="tag task-ui-badge"><strong>{{ item.0 }}</strong>: {{ item.1 }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<details>
|
||||||
|
<summary class="is-size-7 has-text-link" style="cursor:pointer;">View payload JSON</summary>
|
||||||
|
<pre class="task-event-payload">{{ row.payload_view.pretty_text }}</pre>
|
||||||
|
</details>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr><td colspan="4">No events.</td></tr>
|
<tr><td colspan="4">No events.</td></tr>
|
||||||
@@ -47,4 +59,23 @@
|
|||||||
</table>
|
</table>
|
||||||
</article>
|
</article>
|
||||||
</div></section>
|
</div></section>
|
||||||
|
<style>
|
||||||
|
.task-event-payload {
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
padding: 0.6rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(127, 127, 127, 0.25);
|
||||||
|
background: rgba(245, 245, 245, 0.75);
|
||||||
|
color: #1f1f1f;
|
||||||
|
max-width: 72ch;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .task-event-payload {
|
||||||
|
background: rgba(35, 35, 35, 0.75);
|
||||||
|
color: #f5f5f5;
|
||||||
|
border-color: rgba(200, 200, 200, 0.35);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from core.models import (
|
|||||||
ChatSession,
|
ChatSession,
|
||||||
ChatTaskSource,
|
ChatTaskSource,
|
||||||
DerivedTask,
|
DerivedTask,
|
||||||
|
DerivedTaskEvent,
|
||||||
Message,
|
Message,
|
||||||
Person,
|
Person,
|
||||||
PersonIdentifier,
|
PersonIdentifier,
|
||||||
@@ -154,3 +155,38 @@ class TasksPagesManagementTests(TestCase):
|
|||||||
response = self.client.get(reverse("tasks_hub"))
|
response = self.client.get(reverse("tasks_hub"))
|
||||||
self.assertEqual(200, response.status_code)
|
self.assertEqual(200, response.status_code)
|
||||||
self.assertContains(response, "Scope Person")
|
self.assertContains(response, "Scope Person")
|
||||||
|
|
||||||
|
def test_task_detail_renders_payload_summary_and_json(self):
|
||||||
|
project = TaskProject.objects.create(user=self.user, name="Payload Test")
|
||||||
|
session = ChatSession.objects.create(user=self.user, identifier=self.pid_signal)
|
||||||
|
origin = Message.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
session=session,
|
||||||
|
ts=1_700_000_000_000,
|
||||||
|
text="origin",
|
||||||
|
sender_uuid="+15551230000",
|
||||||
|
custom_author="OTHER",
|
||||||
|
source_service="signal",
|
||||||
|
source_chat_id="+15551230000",
|
||||||
|
)
|
||||||
|
task = DerivedTask.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
project=project,
|
||||||
|
title="Payload detail",
|
||||||
|
source_service="signal",
|
||||||
|
source_channel="+15551230000",
|
||||||
|
origin_message=origin,
|
||||||
|
reference_code="42",
|
||||||
|
status_snapshot="open",
|
||||||
|
)
|
||||||
|
DerivedTaskEvent.objects.create(
|
||||||
|
task=task,
|
||||||
|
event_type="reaction_captured",
|
||||||
|
payload={"source": "signal", "emoji": "❤️", "reason": "heart_reaction"},
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("tasks_task", kwargs={"task_id": str(task.id)}))
|
||||||
|
self.assertEqual(200, response.status_code)
|
||||||
|
self.assertContains(response, "View payload JSON")
|
||||||
|
self.assertContains(response, "<strong>source</strong>: signal", html=True)
|
||||||
|
self.assertContains(response, ""emoji": "❤️"")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from asgiref.sync import async_to_sync
|
from asgiref.sync import async_to_sync
|
||||||
@@ -174,6 +175,60 @@ def _service_label(service: str) -> str:
|
|||||||
return labels.get(key, key.title() if key else "Unknown")
|
return labels.get(key, key.title() if key else "Unknown")
|
||||||
|
|
||||||
|
|
||||||
|
def _format_task_event_payload(raw_payload):
|
||||||
|
payload = raw_payload
|
||||||
|
if payload is None:
|
||||||
|
payload = {}
|
||||||
|
if isinstance(payload, str):
|
||||||
|
text = payload.strip()
|
||||||
|
if not text:
|
||||||
|
return {
|
||||||
|
"summary_items": [],
|
||||||
|
"pretty_text": "{}",
|
||||||
|
"is_mapping": False,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
parsed = json.loads(text)
|
||||||
|
payload = parsed
|
||||||
|
except Exception:
|
||||||
|
return {
|
||||||
|
"summary_items": [("text", text[:140])],
|
||||||
|
"pretty_text": text,
|
||||||
|
"is_mapping": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
if isinstance(payload, dict):
|
||||||
|
summary = []
|
||||||
|
preferred = ("source", "reason", "reaction", "emoji", "presence", "last_seen_ts")
|
||||||
|
for key in preferred:
|
||||||
|
if key in payload:
|
||||||
|
summary.append((key, str(payload.get(key))))
|
||||||
|
if not summary:
|
||||||
|
for key in list(payload.keys())[:4]:
|
||||||
|
summary.append((str(key), str(payload.get(key))))
|
||||||
|
pretty = json.dumps(payload, indent=2, ensure_ascii=False, sort_keys=True)
|
||||||
|
return {
|
||||||
|
"summary_items": summary,
|
||||||
|
"pretty_text": pretty,
|
||||||
|
"is_mapping": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
if isinstance(payload, (list, tuple)):
|
||||||
|
pretty = json.dumps(list(payload), indent=2, ensure_ascii=False)
|
||||||
|
return {
|
||||||
|
"summary_items": [("items", str(len(payload)))],
|
||||||
|
"pretty_text": pretty,
|
||||||
|
"is_mapping": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
text = str(payload)
|
||||||
|
return {
|
||||||
|
"summary_items": [("value", text[:140])],
|
||||||
|
"pretty_text": text,
|
||||||
|
"is_mapping": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _creator_label_for_message(user, service: str, message) -> str:
|
def _creator_label_for_message(user, service: str, message) -> str:
|
||||||
msg = message
|
msg = message
|
||||||
if msg is None:
|
if msg is None:
|
||||||
@@ -800,6 +855,7 @@ class TaskDetail(LoginRequiredMixin, View):
|
|||||||
raw_actor = str(getattr(row, "actor_identifier", "") or "").strip()
|
raw_actor = str(getattr(row, "actor_identifier", "") or "").strip()
|
||||||
if raw_actor:
|
if raw_actor:
|
||||||
row.actor_display = raw_actor
|
row.actor_display = raw_actor
|
||||||
|
row.payload_view = _format_task_event_payload(getattr(row, "payload", None))
|
||||||
task.creator_label = _creator_label_for_message(
|
task.creator_label = _creator_label_for_message(
|
||||||
request.user,
|
request.user,
|
||||||
str(getattr(task, "source_service", "") or "").strip().lower(),
|
str(getattr(task, "source_service", "") or "").strip().lower(),
|
||||||
|
|||||||
Reference in New Issue
Block a user