diff --git a/frontend/pages/communication/chat.vue b/frontend/pages/communication/chat.vue index 15af1ca9..80d77455 100644 --- a/frontend/pages/communication/chat.vue +++ b/frontend/pages/communication/chat.vue @@ -13,6 +13,7 @@ const activeRoomKey = ref(requestedRoom()) const loading = ref(true), roomLoading = ref(false), sending = ref(false), creating = ref(false) const showRoomForm = ref(false), messagesViewport = ref(null) const hiddenRoomKeys = ref([]), showHiddenRooms = ref(false), preferenceBusy = ref(''), preferenceError = ref('') +const roomSearch = ref('') const preferencesReady = ref(false) const roomForm = reactive({ name: '', key: '', topic: '' }) const session = computed(() => `${auth.user?.id || auth.user?.user_id || ''}:${auth.activeTenant || ''}`) @@ -42,8 +43,17 @@ const decoratedRooms = computed(() => [ ]) const visibleRooms = computed(() => decoratedRooms.value.filter(room => !hiddenRoomKeys.value.includes(room.key))) const hiddenRooms = computed(() => decoratedRooms.value.filter(room => hiddenRoomKeys.value.includes(room.key))) +const normalizeRoomSearch = value => String(value || '').toLowerCase().normalize('NFKD').replace(/[\u0300-\u036f]/g, '').replace(/ß/g, 'ss') +const roomSearchTerms = computed(() => normalizeRoomSearch(roomSearch.value).trim().split(/\s+/).filter(Boolean)) +function matchesRoomSearch(room) { + const text = normalizeRoomSearch([room.name, room.topic, room.projectNumber, room.email, room.ownerName].filter(value => value != null).join(' ')) + return roomSearchTerms.value.every(term => text.includes(term)) +} +// Filter only the sidebar; the open conversation and its draft keep their selection. +const filteredRooms = computed(() => visibleRooms.value.filter(matchesRoomSearch)) +const filteredHiddenRooms = computed(() => hiddenRooms.value.filter(matchesRoomSearch)) const groupedRooms = computed(() => ['Räume', 'Projekte', 'Direktnachrichten', 'Zusammenarbeit'] - .map(label => ({ label, rooms: visibleRooms.value.filter(room => room.group === label) })) + .map(label => ({ label, rooms: filteredRooms.value.filter(room => room.group === label) })) .filter(group => group.rooms.length)) const activeRoom = computed(() => visibleRooms.value.find(room => room.key === activeRoomKey.value) || null) const isCollaboration = computed(() => activeRoom.value?.source === 'collaboration') @@ -246,6 +256,7 @@ async function initialize() { baseRooms.value = []; projectRooms.value = []; directRooms.value = []; collaborationRooms.value = []; invitations.value = [] unreadRooms.value = {}; invitationBusy.value = ''; collaborationError.value = ''; listError.value = ''; creating.value = false hiddenRoomKeys.value = []; showHiddenRooms.value = false; preferenceBusy.value = ''; preferenceError.value = ''; preferencesReady.value = false + roomSearch.value = '' showRoomForm.value = false; Object.assign(roomForm, { name: '', key: '', topic: '' }) activeRoomKey.value = requestedRoom(); loading.value = true await Promise.all([refreshLists(), loadRoomPreferences(scope)]) @@ -291,6 +302,12 @@ onBeforeUnmount(() => { + + + +
@@ -343,13 +360,14 @@ onBeforeUnmount(() => { -

Keine sichtbaren Räume. Du kannst ausgeblendete Räume unten wieder einblenden.

-
+

Keine Chats für diese Suche gefunden.

+

Keine sichtbaren Räume. Du kannst ausgeblendete Räume unten wieder einblenden.

+
-
+
{{ room.name }} Einblenden
diff --git a/frontend/tests/chat-collaboration.test.cjs b/frontend/tests/chat-collaboration.test.cjs index ccf7be67..00f065d5 100644 --- a/frontend/tests/chat-collaboration.test.cjs +++ b/frontend/tests/chat-collaboration.test.cjs @@ -72,6 +72,34 @@ test('only member rooms enter normal chat, with separate keys, groups and unread assert.equal(ui.run('decoratedRooms.value.some(r => r.id === "admin-only")'), false) }) +test('searching the sidebar preserves the open conversation and draft, including hidden-room matches', async t => { + const ui = setup(fixture(url => { + if (url.endsWith('/project-rooms')) return { rooms: [{ key: 'project_1', name: 'Büro Süd', projectNumber: 'P-2026-42', exists: true, type: 'project' }] } + if (url.endsWith('/direct-rooms')) return { rooms: [{ key: 'direct_1', name: 'Anna Müller', email: 'anna@example.test', exists: true, type: 'direct' }] } + })) + t.after(ui.close) + ui.mount() + await settle() + ui.run('draft.value = "Noch nicht gesendet"; roomSearch.value = " BÜRO 2026-42 "') + assert.equal(ui.run('groupedRooms.value.length'), 1) + assert.equal(ui.run('groupedRooms.value[0].rooms[0].key'), 'project_1') + ui.run('roomSearch.value = "muller ANNA@"') + assert.equal(ui.run('groupedRooms.value[0].rooms[0].key'), 'direct_1') + await ui.run('setRoomHidden(decoratedRooms.value.find(room => room.key === "direct_1"), true)') + assert.equal(ui.run('groupedRooms.value.length'), 0) + assert.equal(ui.run('filteredHiddenRooms.value[0].key'), 'direct_1') + ui.run('roomSearch.value = "kein Treffer"') + await settle() + assert.equal(ui.run('filteredRooms.value.length'), 0) + assert.equal(ui.run('filteredHiddenRooms.value.length'), 0) + assert.equal(ui.run('activeRoom.value.key'), 'allgemein') + assert.equal(ui.run('messages.value[0].body'), 'Interne Nachricht') + assert.equal(ui.run('draft.value'), 'Noch nicht gesendet') + ui.run('roomSearch.value = ""') + assert.equal(ui.run('groupedRooms.value.length'), 3) + assert.equal(ui.run('hiddenRooms.value[0].key'), 'direct_1') +}) + test('opening a collaboration deep link never provisions, reads or sends through native chat APIs', async t => { const ui = setup(fixture(), { room: 'collaboration:shared-room' }) t.after(ui.close)