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
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:
parent
d237cb4bf1
commit
48b60ba416
@ -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()
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user