feat: channel management — create, edit, delete, reorder with category-type enforcement

Server:
- Enforce category-type validation: text/announcement only under text categories,
  voice only under voice categories (400 on mismatch)
- Admin panel category field changed to dropdown with auto-filtered type options
- Default setup creates both Text Channels and Voice Channels categories

Client:
- Add create/edit/delete channel modals (admin/owner only)
- "+" button on category headers to create channels with pre-filled category
- Right-click context menu on channels for edit/delete
- Mouse-based drag-and-drop reordering within categories
- Admin API methods: adminCreateChannel, adminUpdateChannel, adminDeleteChannel
- Immediate local store update on reorder for instant feedback

Tests: 7 server integration tests, 31 client unit tests (create/edit/delete modals)
This commit is contained in:
jevb
2026-03-18 11:28:13 +01:00
parent 32c39e95aa
commit f36fb1ffdc
15 changed files with 1585 additions and 12 deletions
+24 -1
View File
@@ -326,7 +326,12 @@
<option value="announcement">Announcement</option>
</select>
</div>
<div class="form-group"><label>Category</label><input id="ch-category" type="text" placeholder="General"></div>
<div class="form-group"><label>Category</label>
<select id="ch-category">
<option value="Text Channels">Text Channels</option>
<option value="Voice Channels">Voice Channels</option>
</select>
</div>
<div class="form-group"><label>Topic</label><input id="ch-topic" type="text"></div>
<div class="form-group"><label>Position</label><input id="ch-position" type="number" value="0"></div>
</div>
@@ -816,6 +821,24 @@ document.getElementById('show-create-channel').onclick = () => {
document.getElementById('create-channel-form').classList.toggle('hidden');
};
// Filter channel type dropdown based on selected category
document.getElementById('ch-category').addEventListener('change', function() {
const typeSelect = document.getElementById('ch-type');
const isVoice = this.value.toLowerCase().includes('voice');
for (const opt of typeSelect.options) {
if (isVoice) {
opt.hidden = opt.value !== 'voice';
} else {
opt.hidden = opt.value === 'voice';
}
}
// Auto-select the first visible option
const visible = [...typeSelect.options].find(o => !o.hidden);
if (visible) typeSelect.value = visible.value;
});
// Trigger initial filter
document.getElementById('ch-category').dispatchEvent(new Event('change'));
document.getElementById('cancel-create-channel').onclick = () => {
document.getElementById('create-channel-form').classList.add('hidden');
};