Compact interfaces and edit more things inline
This commit is contained in:
193
core/templates/mixins/window-content/person-form.html
Normal file
193
core/templates/mixins/window-content/person-form.html
Normal file
@@ -0,0 +1,193 @@
|
||||
{% include "mixins/partials/notify.html" %}
|
||||
{% if page_title is not None %}
|
||||
<h1 class="title is-4">{{ page_title }}</h1>
|
||||
{% endif %}
|
||||
{% if page_subtitle is not None %}
|
||||
<h1 class="subtitle">{{ page_subtitle }}</h1>
|
||||
{% endif %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
<style>
|
||||
.person-tab-pane {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.person-tab-pane.is-active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.person-tab-pane-body {
|
||||
max-height: min(52vh, 30rem);
|
||||
overflow-y: auto;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
.person-content-count {
|
||||
margin-left: 0.3rem;
|
||||
min-width: 2.35rem;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<form
|
||||
id="person-form-tabs-{{ object.id }}"
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-post="{{ submit_url }}"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML">
|
||||
{% csrf_token %}
|
||||
{% for hidden in form.hidden_fields %}
|
||||
{{ hidden }}
|
||||
{% endfor %}
|
||||
{% if form.non_field_errors %}
|
||||
<article class="message is-danger">
|
||||
<div class="message-body">{{ form.non_field_errors }}</div>
|
||||
</article>
|
||||
{% endif %}
|
||||
|
||||
{{ form.name|as_crispy_field }}
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-12-mobile is-4-tablet">{{ form.sentiment|as_crispy_field }}</div>
|
||||
<div class="column is-12-mobile is-4-tablet">{{ form.timezone|as_crispy_field }}</div>
|
||||
<div class="column is-12-mobile is-4-tablet">{{ form.last_interaction|as_crispy_field }}</div>
|
||||
</div>
|
||||
|
||||
<div class="tabs is-small is-toggle is-toggle-rounded" style="margin-bottom: 0.75rem;">
|
||||
<ul>
|
||||
<li class="is-active">
|
||||
<a href="#" data-person-tab-target="summary">
|
||||
<span class="icon is-small"><i class="fa-solid fa-file-lines"></i></span>
|
||||
<span>Summary</span>
|
||||
<span id="person-count-summary-{{ object.id }}" class="tag is-light is-small person-content-count">0</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" data-person-tab-target="profile">
|
||||
<span class="icon is-small"><i class="fa-solid fa-id-card"></i></span>
|
||||
<span>Profile</span>
|
||||
<span id="person-count-profile-{{ object.id }}" class="tag is-light is-small person-content-count">0</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" data-person-tab-target="revealed">
|
||||
<span class="icon is-small"><i class="fa-solid fa-eye"></i></span>
|
||||
<span>Revealed</span>
|
||||
<span id="person-count-revealed-{{ object.id }}" class="tag is-light is-small person-content-count">0</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" data-person-tab-target="likes">
|
||||
<span class="icon is-small"><i class="fa-solid fa-thumbs-up"></i></span>
|
||||
<span>Likes</span>
|
||||
<span id="person-count-likes-{{ object.id }}" class="tag is-light is-small person-content-count">0</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" data-person-tab-target="dislikes">
|
||||
<span class="icon is-small"><i class="fa-solid fa-thumbs-down"></i></span>
|
||||
<span>Dislikes</span>
|
||||
<span id="person-count-dislikes-{{ object.id }}" class="tag is-light is-small person-content-count">0</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="person-tab-pane is-active" data-person-tab-pane="summary">
|
||||
<div class="person-tab-pane-body">
|
||||
{{ form.summary|as_crispy_field }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="person-tab-pane" data-person-tab-pane="profile">
|
||||
<div class="person-tab-pane-body">
|
||||
{{ form.profile|as_crispy_field }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="person-tab-pane" data-person-tab-pane="revealed">
|
||||
<div class="person-tab-pane-body">
|
||||
{{ form.revealed|as_crispy_field }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="person-tab-pane" data-person-tab-pane="likes">
|
||||
<div class="person-tab-pane-body">
|
||||
{{ form.likes|as_crispy_field }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="person-tab-pane" data-person-tab-pane="dislikes">
|
||||
<div class="person-tab-pane-body">
|
||||
{{ form.dislikes|as_crispy_field }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons" style="margin-top: 1rem; margin-bottom: 0;">
|
||||
{% if hide_cancel is not True %}
|
||||
<button
|
||||
type="button"
|
||||
class="button is-light modal-close-button">
|
||||
Cancel
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="submit" class="button modal-close-button">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const root = document.getElementById("person-form-tabs-{{ object.id }}");
|
||||
if (!root || root.dataset.tabsBound === "1") {
|
||||
return;
|
||||
}
|
||||
root.dataset.tabsBound = "1";
|
||||
|
||||
const tabs = Array.from(root.querySelectorAll("[data-person-tab-target]"));
|
||||
const panes = Array.from(root.querySelectorAll("[data-person-tab-pane]"));
|
||||
const contentFields = ["summary", "profile", "revealed", "likes", "dislikes"];
|
||||
|
||||
function setActive(key) {
|
||||
tabs.forEach((tab) => {
|
||||
const li = tab.closest("li");
|
||||
if (!li) {
|
||||
return;
|
||||
}
|
||||
li.classList.toggle("is-active", tab.dataset.personTabTarget === key);
|
||||
});
|
||||
panes.forEach((pane) => {
|
||||
pane.classList.toggle("is-active", pane.dataset.personTabPane === key);
|
||||
});
|
||||
}
|
||||
|
||||
function updateCounts() {
|
||||
contentFields.forEach((name) => {
|
||||
const field = root.querySelector("[name='" + name + "']");
|
||||
const counter = document.getElementById("person-count-" + name + "-{{ object.id }}");
|
||||
if (!field || !counter) {
|
||||
return;
|
||||
}
|
||||
const length = (field.value || "").trim().length;
|
||||
counter.textContent = String(length);
|
||||
});
|
||||
}
|
||||
|
||||
tabs.forEach((tab) => {
|
||||
tab.addEventListener("click", function (event) {
|
||||
event.preventDefault();
|
||||
setActive(tab.dataset.personTabTarget);
|
||||
});
|
||||
});
|
||||
|
||||
contentFields.forEach((name) => {
|
||||
const field = root.querySelector("[name='" + name + "']");
|
||||
if (field) {
|
||||
field.addEventListener("input", updateCounts);
|
||||
}
|
||||
});
|
||||
|
||||
updateCounts();
|
||||
if (tabs[0]) {
|
||||
setActive(tabs[0].dataset.personTabTarget);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
44
core/templates/mixins/window-content/queue-form-inline.html
Normal file
44
core/templates/mixins/window-content/queue-form-inline.html
Normal file
@@ -0,0 +1,44 @@
|
||||
{% include "mixins/partials/notify.html" %}
|
||||
{% if page_title is not None %}
|
||||
<h1 class="title is-4">{{ page_title }}</h1>
|
||||
{% endif %}
|
||||
{% if page_subtitle is not None %}
|
||||
<h1 class="subtitle">{{ page_subtitle }}</h1>
|
||||
{% endif %}
|
||||
{% load crispy_forms_tags %}
|
||||
|
||||
<form
|
||||
id="queue-inline-form-{{ object.id }}"
|
||||
data-inline-target="{{ submit_target|default:'#modals-here' }}"
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-post="{{ submit_url }}"
|
||||
hx-target="{{ submit_target|default:'#modals-here' }}"
|
||||
hx-swap="innerHTML">
|
||||
{% csrf_token %}
|
||||
{% for hidden in form.hidden_fields %}
|
||||
{{ hidden }}
|
||||
{% endfor %}
|
||||
{% if form.non_field_errors %}
|
||||
<article class="message is-danger">
|
||||
<div class="message-body">{{ form.non_field_errors }}</div>
|
||||
</article>
|
||||
{% endif %}
|
||||
{{ form|crispy }}
|
||||
<div class="buttons are-small" style="margin-bottom: 0;">
|
||||
{% if is_inline_edit %}
|
||||
<button
|
||||
type="button"
|
||||
class="button is-light"
|
||||
onclick="(function(){const f=document.getElementById('queue-inline-form-{{ object.id }}'); if(!f){return;} const target=f.dataset.inlineTarget; const host=target ? document.querySelector(target) : null; if(host){host.innerHTML=''; host.style.display='none';}})(); return false;">
|
||||
Cancel
|
||||
</button>
|
||||
{% elif hide_cancel is not True %}
|
||||
<button
|
||||
type="button"
|
||||
class="button is-light modal-close-button">
|
||||
Cancel
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="submit" class="button modal-close-button">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
77
core/templates/mixins/wm/widget.html
Normal file
77
core/templates/mixins/wm/widget.html
Normal file
@@ -0,0 +1,77 @@
|
||||
<div id="widget">
|
||||
<div id="widget-{{ unique }}" class="grid-stack-item" {% block widget_options %}{% if widget_options is None %}gs-w="6" gs-h="1" gs-y="20" gs-x="0"{% else %}{% autoescape off %}{{ widget_options }}{% endautoescape %}{% endif %}{% endblock %}>
|
||||
<div class="grid-stack-item-content">
|
||||
|
||||
<nav class="panel">
|
||||
<p class="panel-heading" style="padding: .2em; line-height: .5em;">
|
||||
<i class="{{ widget_icon|default:'fa-solid fa-arrows-up-down-left-right' }} has-text-grey-light"></i>
|
||||
{% block close_button %}
|
||||
{% include "mixins/partials/close-widget.html" %}
|
||||
{% endblock %}
|
||||
<i
|
||||
class="fa-solid fa-arrows-minimize has-text-grey-light float-right"
|
||||
onclick="grid.compact();"></i>
|
||||
{% block heading %}
|
||||
{{ title }}
|
||||
{% endblock %}
|
||||
</p>
|
||||
<article class="panel-block is-active">
|
||||
<div class="control">
|
||||
{% block panel_content %}
|
||||
{% include window_content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</article>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
{% block custom_script %}
|
||||
{% endblock %}
|
||||
var widget_event = new Event("load-widget");
|
||||
document.dispatchEvent(widget_event);
|
||||
(function () {
|
||||
var widgetRoot = document.getElementById("widget-{{ unique }}");
|
||||
var iconClass = "{{ widget_icon|default:'fa-solid fa-arrows-minimize'|escapejs }}";
|
||||
function decorateHandle() {
|
||||
if (!widgetRoot) {
|
||||
return true;
|
||||
}
|
||||
var handles = widgetRoot.querySelectorAll(".ui-resizable-se");
|
||||
if (!handles.length) {
|
||||
handles = widgetRoot.querySelectorAll(".ui-resizable-handle");
|
||||
}
|
||||
if (!handles.length) {
|
||||
return false;
|
||||
}
|
||||
handles.forEach(function (handle) {
|
||||
if (handle.dataset.iconApplied === "1") {
|
||||
return;
|
||||
}
|
||||
handle.dataset.iconApplied = "1";
|
||||
handle.style.display = "flex";
|
||||
handle.style.alignItems = "center";
|
||||
handle.style.justifyContent = "center";
|
||||
handle.style.overflow = "hidden";
|
||||
var icon = document.createElement("i");
|
||||
icon.className = iconClass + " has-text-grey-light";
|
||||
icon.style.fontSize = "0.65rem";
|
||||
icon.style.opacity = "0.65";
|
||||
icon.style.pointerEvents = "none";
|
||||
handle.appendChild(icon);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
var attempts = 0;
|
||||
var timer = window.setInterval(function () {
|
||||
attempts += 1;
|
||||
if (decorateHandle() || attempts > 10) {
|
||||
window.clearInterval(timer);
|
||||
}
|
||||
}, 80);
|
||||
})();
|
||||
</script>
|
||||
{% block custom_end %}
|
||||
{% endblock %}
|
||||
@@ -199,9 +199,14 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="is-flex is-align-items-center" style="gap: 0.35rem;">
|
||||
<span id="ai-cache-indicator-{{ person.id }}" class="tag is-warning is-light is-small" style="display: none;">
|
||||
Cached
|
||||
<div class="is-flex is-align-items-center ai-capsule-controls">
|
||||
<span class="ai-cache-indicator-slot">
|
||||
<span
|
||||
id="ai-cache-indicator-{{ person.id }}"
|
||||
class="tag is-warning is-light is-small"
|
||||
aria-hidden="true">
|
||||
Cached
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
@@ -267,12 +272,38 @@
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ai-person-widget .ai-capsule-controls {
|
||||
gap: 0.35rem;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.ai-person-widget .ai-cache-indicator-slot {
|
||||
width: 9.5rem;
|
||||
display: inline-flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
.ai-person-widget .ai-cache-indicator-slot .tag {
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
.ai-person-widget .ai-cache-indicator-slot .tag.is-visible {
|
||||
visibility: visible;
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
.ai-person-widget .ai-top-tabs li a {
|
||||
min-height: 2.1rem;
|
||||
min-width: 2.1rem;
|
||||
padding: 0 0.45rem;
|
||||
}
|
||||
.ai-person-widget .ai-cache-indicator-slot {
|
||||
width: 8.4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -407,8 +438,13 @@
|
||||
}
|
||||
if (show) {
|
||||
indicator.textContent = formatCacheAge(ts);
|
||||
indicator.classList.add("is-visible");
|
||||
indicator.setAttribute("aria-hidden", "false");
|
||||
return;
|
||||
}
|
||||
indicator.style.display = show ? "inline-flex" : "none";
|
||||
indicator.textContent = "Cached";
|
||||
indicator.classList.remove("is-visible");
|
||||
indicator.setAttribute("aria-hidden", "true");
|
||||
}
|
||||
|
||||
const OPERATION_TABS = ["summarise", "draft_reply", "extract_patterns"];
|
||||
|
||||
@@ -61,10 +61,11 @@
|
||||
<div class="buttons are-small" style="margin: 0;">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'queue_update' type=type pk=item.id %}"
|
||||
hx-get="{% url 'queue_update' type='window' pk=item.id %}?hx_target=%23queue-inline-editor-{{ item.id }}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-target="#queue-inline-editor-{{ item.id }}"
|
||||
hx-swap="innerHTML"
|
||||
_="on htmx:afterRequest if event.detail.successful set #queue-inline-editor-{{ item.id }}.style.display to 'block' end"
|
||||
class="button is-light">
|
||||
<span class="icon is-small"><i class="fa-solid fa-pen"></i></span>
|
||||
<span>Edit</span>
|
||||
@@ -82,6 +83,9 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="queue-inline-editor-{{ item.id }}"
|
||||
style="display: none; margin-top: 0.55rem; padding: 0.55rem; border-radius: 8px; border: 1px solid rgba(50, 115, 220, 0.25); background: rgba(255, 255, 255, 0.78);"></div>
|
||||
</article>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
Reference in New Issue
Block a user