improve(messages): add "Nowa grupa" button with dedicated group creation modal
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

New button next to "Nowa wiadomość" with outline style. Modal includes
group name field, multi-member search picker, and optional first message.
Uses the existing POST /api/conversations endpoint with is_group=true.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-04-08 16:32:42 +02:00
parent 0c9ea2e69f
commit ca75468367
3 changed files with 288 additions and 2 deletions

View File

@ -160,6 +160,15 @@
background: var(--conv-primary-hover); background: var(--conv-primary-hover);
} }
.btn-new-group {
background: var(--conv-surface);
color: var(--conv-primary);
border: 1.5px solid var(--conv-primary);
}
.btn-new-group:hover {
background: var(--conv-primary-light);
}
@media (max-width: 768px) { @media (max-width: 768px) {
.btn-new-text { display: none; } .btn-new-text { display: none; }
.btn-new-conversation { width: 36px; padding: 0; } .btn-new-conversation { width: 36px; padding: 0; }

View File

@ -2201,6 +2201,246 @@
}, },
}; };
// ============================================================
// 10b. NEW GROUP MODAL
// ============================================================
var NewGroupModal = {
_quill: null,
_selectedMembers: [],
_debounceTimer: null,
init: function () {
var btn = document.getElementById('newGroupBtn');
var modal = document.getElementById('newGroupModal');
if (!btn || !modal) return;
btn.addEventListener('click', function () {
NewGroupModal.open();
});
var closeBtn = document.getElementById('closeNewGroup');
var cancelBtn = document.getElementById('cancelNewGroup');
if (closeBtn) closeBtn.addEventListener('click', function () { modal.style.display = 'none'; });
if (cancelBtn) cancelBtn.addEventListener('click', function () { modal.style.display = 'none'; });
var sendBtn = document.getElementById('sendNewGroup');
if (sendBtn) sendBtn.addEventListener('click', function () { NewGroupModal.send(); });
var searchInput = document.getElementById('groupRecipientSearch');
if (searchInput) {
searchInput.addEventListener('input', function () {
clearTimeout(NewGroupModal._debounceTimer);
NewGroupModal._debounceTimer = setTimeout(function () {
NewGroupModal.filterRecipients(searchInput.value);
}, 200);
});
}
},
open: function () {
NewGroupModal._selectedMembers = [];
var nameInput = document.getElementById('groupNameInput');
if (nameInput) nameInput.value = '';
var searchInput = document.getElementById('groupRecipientSearch');
if (searchInput) searchInput.value = '';
var suggestions = document.getElementById('groupRecipientSuggestions');
if (suggestions) suggestions.innerHTML = '';
var selected = document.getElementById('groupSelectedRecipients');
if (selected) selected.innerHTML = '';
var editorEl = document.getElementById('groupMessageEditor');
if (editorEl) {
editorEl.innerHTML = '';
NewGroupModal._quill = new Quill('#groupMessageEditor', {
theme: 'snow',
placeholder: 'Pierwsza wiadomość (opcjonalna)...',
modules: {
toolbar: [['bold', 'italic'], ['link'], ['clean']],
},
});
}
var modal = document.getElementById('newGroupModal');
if (modal) modal.style.display = 'flex';
if (nameInput) nameInput.focus();
},
filterRecipients: async function (query) {
var suggestions = document.getElementById('groupRecipientSuggestions');
if (!suggestions) return;
suggestions.innerHTML = '';
query = (query || '').trim();
if (query.length < 2) return;
suggestions.innerHTML = '<div style="padding:10px 12px;color:var(--conv-text-muted);font-size:13px">Szukam...</div>';
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();
suggestions.innerHTML = '';
var selectedIds = NewGroupModal._selectedMembers.map(function (r) { return r.id; });
var matches = users.filter(function (u) {
return selectedIds.indexOf(u.id) === -1;
});
if (!matches.length) {
suggestions.innerHTML = '<div style="padding:10px 12px;color:var(--conv-text-muted);font-size:13px">Brak wyników</div>';
return;
}
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)';
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);
}
item.appendChild(avatar);
item.appendChild(info);
item.addEventListener('click', function () {
NewGroupModal.selectMember(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>';
}
},
selectMember: function (user) {
NewGroupModal._selectedMembers.push(user);
var container = document.getElementById('groupSelectedRecipients');
if (container) {
var pill = el('span', '');
pill.style.display = 'inline-flex';
pill.style.alignItems = 'center';
pill.style.gap = '4px';
pill.style.padding = '4px 10px';
pill.style.borderRadius = '16px';
pill.style.background = 'var(--conv-primary-light)';
pill.style.fontSize = '13px';
pill.style.color = 'var(--conv-text-primary)';
pill.style.margin = '2px';
pill.textContent = user.name || user.email;
var removeBtn = el('button', '', '\u00d7');
removeBtn.style.border = 'none';
removeBtn.style.background = 'transparent';
removeBtn.style.cursor = 'pointer';
removeBtn.style.fontSize = '14px';
removeBtn.style.color = 'var(--conv-text-muted)';
removeBtn.style.padding = '0';
removeBtn.style.lineHeight = '1';
removeBtn.addEventListener('click', function () {
NewGroupModal._selectedMembers = NewGroupModal._selectedMembers.filter(function (r) {
return r.id !== user.id;
});
pill.remove();
});
pill.appendChild(removeBtn);
container.appendChild(pill);
}
var searchInput = document.getElementById('groupRecipientSearch');
if (searchInput) searchInput.value = '';
var suggestions = document.getElementById('groupRecipientSuggestions');
if (suggestions) suggestions.innerHTML = '';
if (searchInput) searchInput.focus();
},
send: async function () {
var nameInput = document.getElementById('groupNameInput');
var groupName = (nameInput ? nameInput.value : '').trim();
if (NewGroupModal._selectedMembers.length < 2) {
alert('Wybierz co najmniej dwóch członków grupy');
return;
}
if (state._isCreating) return;
state._isCreating = true;
var messageContent = '';
if (NewGroupModal._quill) {
var text = NewGroupModal._quill.getText().trim();
if (text) {
messageContent = NewGroupModal._quill.root.innerHTML;
}
}
var memberIds = NewGroupModal._selectedMembers.map(function (r) { return r.id; });
// Auto-generate name if not provided
if (!groupName) {
var names = NewGroupModal._selectedMembers.map(function (r) { return r.name || r.email; });
names.push(window.__CURRENT_USER__.name);
groupName = names.join(', ');
}
try {
var result = await api('/api/conversations', 'POST', {
member_ids: memberIds,
name: groupName,
message: messageContent,
});
var modal = document.getElementById('newGroupModal');
if (modal) modal.style.display = 'none';
var existing = state.conversations.find(function (c) { return c.id === result.id; });
if (!existing) {
state.conversations.unshift(result);
} else {
Object.assign(existing, result);
}
ConversationList.renderList();
ConversationList.selectConversation(result.id);
} catch (e) {
alert('Nie udało się utworzyć grupy: ' + e.message);
} finally {
state._isCreating = false;
}
},
};
// ============================================================ // ============================================================
// 11. SEARCH // 11. SEARCH
// ============================================================ // ============================================================
@ -2571,6 +2811,7 @@
ConversationList.renderList(); ConversationList.renderList();
Composer.init(); Composer.init();
NewMessageModal.init(); NewMessageModal.init();
NewGroupModal.init();
Search.init(); Search.init();
Pins.init(); Pins.init();
initContextMenu(); initContextMenu();

View File

@ -5,7 +5,7 @@
{% block head_extra %} {% block head_extra %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/quill.snow.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/quill.snow.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/conversations.css') }}?v=11"> <link rel="stylesheet" href="{{ url_for('static', filename='css/conversations.css') }}?v=12">
<script src="{{ url_for('static', filename='js/vendor/quill.js') }}"></script> <script src="{{ url_for('static', filename='js/vendor/quill.js') }}"></script>
<style> <style>
footer { display: none !important; } footer { display: none !important; }
@ -64,6 +64,15 @@
</svg> </svg>
<span class="btn-new-text">Nowa wiadomość</span> <span class="btn-new-text">Nowa wiadomość</span>
</button> </button>
<button class="btn-new-conversation btn-new-group" id="newGroupBtn" title="Nowa grupa">
<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="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
<span class="btn-new-text">Nowa grupa</span>
</button>
</div> </div>
</div> </div>
<div class="conversation-list" id="conversationList"> <div class="conversation-list" id="conversationList">
@ -208,6 +217,33 @@
<button data-emoji="✅"></button> <button data-emoji="✅"></button>
</div> </div>
<!-- New group modal -->
<div class="modal-overlay" id="newGroupModal" style="display:none">
<div class="modal-content">
<div class="modal-header">
<h3>Nowa grupa</h3>
<button class="modal-close" id="closeNewGroup">&times;</button>
</div>
<div class="modal-body">
<div class="recipient-input">
<label>Nazwa grupy:</label>
<input type="text" id="groupNameInput" placeholder="np. Projekt XYZ, Zarząd..." style="margin-bottom:12px">
</div>
<div class="recipient-input">
<label>Członkowie:</label>
<input type="text" id="groupRecipientSearch" placeholder="Wpisz imię lub nazwisko...">
<div class="recipient-suggestions" id="groupRecipientSuggestions"></div>
<div class="selected-recipients" id="groupSelectedRecipients"></div>
</div>
<div id="groupMessageEditor"></div>
</div>
<div class="modal-footer">
<button class="btn-secondary" id="cancelNewGroup">Anuluj</button>
<button class="btn-primary" id="sendNewGroup">Utwórz grupę</button>
</div>
</div>
</div>
<!-- New message modal --> <!-- New message modal -->
<div class="modal-overlay" id="newMessageModal" style="display:none"> <div class="modal-overlay" id="newMessageModal" style="display:none">
<div class="modal-content"> <div class="modal-content">
@ -256,7 +292,7 @@ window.__CSRF_TOKEN__ = '{{ csrf_token() }}';
// Load conversations.js after data is set // Load conversations.js after data is set
(function() { (function() {
var s = document.createElement('script'); var s = document.createElement('script');
s.src = '{{ url_for("static", filename="js/conversations.js") }}?v=18'; s.src = '{{ url_for("static", filename="js/conversations.js") }}?v=19';
document.body.appendChild(s); document.body.appendChild(s);
})(); })();
{% endblock %} {% endblock %}