improve(messages): add visible "Nowa wiadomość" button and server-side recipient search
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

The compose button was just a small pencil icon with no label. Now it shows
"Nowa wiadomość" text (hidden on mobile). Recipient search was broken because
window.__USERS__ was always empty — replaced with /api/users/search API endpoint
that queries active users by name/email with autocomplete suggestions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-04-08 16:15:42 +02:00
parent d237cb4bf1
commit 48b60ba416
4 changed files with 139 additions and 55 deletions

View File

@ -721,3 +721,64 @@ def api_conversation_mark_read(conv_id):
return jsonify({'error': 'Błąd serwera'}), 500
finally:
db.close()
# ============================================================
# 8. GET /api/users/search — Search users for new message
# ============================================================
@bp.route('/api/users/search')
@login_required
@member_required
def api_users_search():
"""Search active, verified users by name or email for recipient picker."""
query = (request.args.get('q') or '').strip()
if len(query) < 2:
return jsonify([])
db = SessionLocal()
try:
search_pattern = f'%{query}%'
users_with_companies = db.query(
User.id,
User.name,
User.email,
User.avatar_path,
Company.name.label('company_name'),
).outerjoin(
UserCompanyPermissions,
UserCompanyPermissions.user_id == User.id
).outerjoin(
Company,
and_(
Company.id == UserCompanyPermissions.company_id,
Company.status == 'active',
)
).filter(
User.is_active == True, # noqa: E712
User.is_verified == True, # noqa: E712
User.id != current_user.id,
or_(
User.name.ilike(search_pattern),
User.email.ilike(search_pattern),
),
).order_by(User.name).limit(10).all()
# Deduplicate (user may have multiple company permissions)
seen = set()
results = []
for uid, name, email, avatar_path, company_name in users_with_companies:
if uid in seen:
continue
seen.add(uid)
results.append({
'id': uid,
'name': name or email.split('@')[0],
'email': email,
'avatar_url': ('/static/' + avatar_path) if avatar_path else None,
'company_name': company_name,
})
return jsonify(results)
finally:
db.close()

View File

@ -141,8 +141,9 @@
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
gap: 6px;
height: 36px;
padding: 0 14px;
border: none;
border-radius: var(--conv-radius);
background: var(--conv-primary);
@ -150,12 +151,20 @@
cursor: pointer;
transition: background var(--conv-transition);
flex-shrink: 0;
font-size: 13px;
font-weight: 500;
white-space: nowrap;
}
.btn-new-conversation:hover {
background: var(--conv-primary-hover);
}
@media (max-width: 768px) {
.btn-new-text { display: none; }
.btn-new-conversation { width: 36px; padding: 0; }
}
/* --- Conversation list (scrollable) --- */
.conversation-list {
flex: 1;

View File

@ -2025,70 +2025,83 @@
}
},
filterRecipients: function (query) {
filterRecipients: async function (query) {
var suggestions = document.getElementById('recipientSuggestions');
if (!suggestions) return;
suggestions.innerHTML = '';
query = (query || '').toLowerCase().trim();
if (!query) return;
query = (query || '').trim();
if (query.length < 2) return;
var users = window.__USERS__ || [];
var selectedIds = state.selectedRecipients.map(function (r) { return r.id; });
// Show loading indicator
suggestions.innerHTML = '<div style="padding:10px 12px;color:var(--conv-text-muted);font-size:13px">Szukam...</div>';
var matches = users.filter(function (u) {
if (u.id === window.__CURRENT_USER__.id) return false;
if (selectedIds.indexOf(u.id) !== -1) return false;
var name = (u.name || '').toLowerCase();
var email = (u.email || '').toLowerCase();
return name.indexOf(query) !== -1 || email.indexOf(query) !== -1;
}).slice(0, 8);
try {
var resp = await fetch('/api/users/search?q=' + encodeURIComponent(query), {
headers: { 'X-CSRFToken': window.__CSRF_TOKEN__ },
});
if (!resp.ok) throw new Error('search failed');
var users = await resp.json();
matches.forEach(function (u) {
var item = el('div', 'suggestion-item');
item.style.padding = '8px 12px';
item.style.cursor = 'pointer';
item.style.display = 'flex';
item.style.alignItems = 'center';
item.style.gap = '8px';
item.style.borderBottom = '1px solid var(--conv-border)';
suggestions.innerHTML = '';
var selectedIds = state.selectedRecipients.map(function (r) { return r.id; });
var matches = users.filter(function (u) {
return selectedIds.indexOf(u.id) === -1;
});
var avatar = el('div', 'conv-avatar ' + avatarColor(u.name));
avatar.style.width = '30px';
avatar.style.height = '30px';
avatar.style.minWidth = '30px';
avatar.style.fontSize = '11px';
avatar.textContent = initials(u.name);
var info = el('div', '');
var nameEl = el('div', '', u.name || u.email);
nameEl.style.fontSize = '14px';
nameEl.style.fontWeight = '500';
if (u.company_name) {
var companyEl = el('div', '', u.company_name);
companyEl.style.fontSize = '12px';
companyEl.style.color = 'var(--conv-text-muted)';
info.appendChild(nameEl);
info.appendChild(companyEl);
} else {
info.appendChild(nameEl);
if (!matches.length) {
suggestions.innerHTML = '<div style="padding:10px 12px;color:var(--conv-text-muted);font-size:13px">Brak wyników</div>';
return;
}
item.appendChild(avatar);
item.appendChild(info);
matches.forEach(function (u) {
var item = el('div', 'suggestion-item');
item.style.padding = '8px 12px';
item.style.cursor = 'pointer';
item.style.display = 'flex';
item.style.alignItems = 'center';
item.style.gap = '8px';
item.style.borderBottom = '1px solid var(--conv-border)';
item.addEventListener('click', function () {
NewMessageModal.selectRecipient(u);
});
item.addEventListener('mouseenter', function () {
item.style.background = 'var(--conv-surface-secondary)';
});
item.addEventListener('mouseleave', function () {
item.style.background = '';
});
var avatar = el('div', 'conv-avatar ' + avatarColor(u.name));
avatar.style.width = '30px';
avatar.style.height = '30px';
avatar.style.minWidth = '30px';
avatar.style.fontSize = '11px';
avatar.textContent = initials(u.name);
suggestions.appendChild(item);
});
var info = el('div', '');
var nameEl = el('div', '', u.name || u.email);
nameEl.style.fontSize = '14px';
nameEl.style.fontWeight = '500';
if (u.company_name) {
var companyEl = el('div', '', u.company_name);
companyEl.style.fontSize = '12px';
companyEl.style.color = 'var(--conv-text-muted)';
info.appendChild(nameEl);
info.appendChild(companyEl);
} else {
info.appendChild(nameEl);
}
item.appendChild(avatar);
item.appendChild(info);
item.addEventListener('click', function () {
NewMessageModal.selectRecipient(u);
});
item.addEventListener('mouseenter', function () {
item.style.background = 'var(--conv-surface-secondary)';
});
item.addEventListener('mouseleave', function () {
item.style.background = '';
});
suggestions.appendChild(item);
});
} catch (e) {
suggestions.innerHTML = '<div style="padding:10px 12px;color:var(--conv-text-muted);font-size:13px">Błąd wyszukiwania</div>';
}
},
selectRecipient: function (user) {

View File

@ -58,10 +58,11 @@
<input type="text" id="searchInput" placeholder="Szukaj rozmów i wiadomości...">
</div>
<button class="btn-new-conversation" id="newMessageBtn" title="Nowa wiadomość">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 20h9"></path>
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path>
</svg>
<span class="btn-new-text">Nowa wiadomość</span>
</button>
</div>
</div>
@ -255,7 +256,7 @@ window.__CSRF_TOKEN__ = '{{ csrf_token() }}';
// Load conversations.js after data is set
(function() {
var s = document.createElement('script');
s.src = '{{ url_for("static", filename="js/conversations.js") }}?v=17';
s.src = '{{ url_for("static", filename="js/conversations.js") }}?v=18';
document.body.appendChild(s);
})();
{% endblock %}