mail + portal: самоподключение почты, веб-клиент и глобальный поиск

Сотрудник сам подключает ящик (autoconfig, вложения, пагинация списка, надёжный IMAP-поиск); в хедере работает /api/search по разделам и сущностям с ACL. Также устойчивость Mattermost-токена для чата.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-22 12:22:53 +03:00
parent 3387418332
commit a2fe4a49e3
30 changed files with 3885 additions and 536 deletions
+44 -7
View File
@@ -516,18 +516,25 @@
</button>
<!-- Поиск -->
<div class="header-search" style="display:flex; align-items:center; gap:0.5rem; background:var(--color-muted); border:1px solid var(--color-border); border-radius:var(--radius); padding:0.4375rem 0.75rem; width:18rem;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--color-muted-foreground); flex-shrink:0;">
<div class="header-search" id="header-search">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="header-search-icon">
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
</svg>
<input type="text"
<input type="search"
id="portal-search-input"
name="q"
placeholder="{% trans 'Поиск...' %}"
class="input"
style="border:none; background:transparent; padding:0; box-shadow:none;"
class="header-search-input"
autocomplete="off"
enterkeyhint="search"
hx-get="/api/search"
hx-trigger="keyup changed delay:300ms"
hx-trigger="keyup changed delay:280ms, search, focus"
hx-target="#search-results"
name="q">
hx-swap="innerHTML"
aria-autocomplete="list"
aria-controls="search-results"
aria-expanded="false">
<div id="search-results" class="portal-search-results" hidden></div>
</div>
</div>
@@ -849,6 +856,36 @@
}
setActiveNavItem();
(function initPortalSearch() {
var wrap = document.getElementById('header-search');
var input = document.getElementById('portal-search-input');
var panel = document.getElementById('search-results');
if (!wrap || !input || !panel) return;
function openSearch() {
panel.hidden = false;
input.setAttribute('aria-expanded', 'true');
}
function closeSearch() {
panel.hidden = true;
input.setAttribute('aria-expanded', 'false');
}
window.closePortalSearch = closeSearch;
input.addEventListener('focus', openSearch);
document.addEventListener('click', function (e) {
if (!wrap.contains(e.target)) closeSearch();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') closeSearch();
});
document.body.addEventListener('htmx:afterSwap', function (e) {
if (e.detail && e.detail.target && e.detail.target.id === 'search-results') {
openSearch();
}
});
})();
// Восстановление темы и фона
var savedTheme = localStorage.getItem('theme') || 'light';
document.querySelectorAll('.theme-option').forEach(function(btn) {