feat(chat): add sidebar search for conversations
All checks were successful
Build and Release FEDEO / verify (push) Successful in 1m2s
Build and Release FEDEO / build-and-release (push) Successful in 6m4s

This commit is contained in:
2026-10-15 17:54:22 +02:00
parent 7b61857fb8
commit f56a07f8e3
2 changed files with 51 additions and 5 deletions

View File

@@ -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(() => {
<UButton icon="i-heroicons-plus" color="neutral" variant="ghost" size="sm" @click="showRoomForm = !showRoomForm" />
</div>
<UInput v-model="roomSearch" icon="i-heroicons-magnifying-glass" placeholder="Chats suchen …" aria-label="Chats suchen" class="mb-4 w-full" @keydown.esc="roomSearch = ''">
<template v-if="roomSearch" #trailing>
<UButton icon="i-heroicons-x-mark" color="neutral" variant="link" size="xs" aria-label="Chatsuche löschen" @click="roomSearch = ''" />
</template>
</UInput>
<form v-if="showRoomForm" class="mb-4 space-y-2 rounded-lg bg-gray-50 p-3" @submit.prevent="createRoom">
<UInput v-model="roomForm.name" placeholder="Raumname" autofocus />
<UInput v-model="roomForm.key" :placeholder="roomKeyPreview || 'raumschluessel'" />
@@ -343,13 +360,14 @@ onBeforeUnmount(() => {
<UButton icon="i-heroicons-eye-slash" color="neutral" variant="ghost" size="xs" class="mr-1 shrink-0" :aria-label="`${room.name} ausblenden`" title="Für mich ausblenden" :disabled="!preferencesReady || !!preferenceBusy" @click="setRoomHidden(room, true)" />
</div>
</section>
<p v-if="!visibleRooms.length" class="px-2 text-sm text-gray-500">Keine sichtbaren Räume. Du kannst ausgeblendete Räume unten wieder einblenden.</p>
<section v-if="hiddenRooms.length" aria-label="Ausgeblendete Räume">
<p v-if="roomSearchTerms.length && !filteredRooms.length && !filteredHiddenRooms.length" role="status" class="px-2 text-sm text-gray-500">Keine Chats für diese Suche gefunden.</p>
<p v-else-if="!roomSearchTerms.length && !visibleRooms.length" class="px-2 text-sm text-gray-500">Keine sichtbaren Räume. Du kannst ausgeblendete Räume unten wieder einblenden.</p>
<section v-if="filteredHiddenRooms.length" aria-label="Ausgeblendete Räume">
<button type="button" class="flex w-full items-center justify-between px-2 py-2 text-left text-xs font-semibold text-gray-500" :aria-expanded="showHiddenRooms" @click="showHiddenRooms = !showHiddenRooms">
<span>Ausgeblendete Räume ({{ hiddenRooms.length }})</span><UIcon :name="showHiddenRooms ? 'i-heroicons-chevron-up' : 'i-heroicons-chevron-down'" />
<span>Ausgeblendete Räume ({{ filteredHiddenRooms.length }})</span><UIcon :name="showHiddenRooms ? 'i-heroicons-chevron-up' : 'i-heroicons-chevron-down'" />
</button>
<div v-if="showHiddenRooms" class="space-y-1">
<div v-for="room in hiddenRooms" :key="room.key" class="flex items-center gap-2 px-2 py-1">
<div v-for="room in filteredHiddenRooms" :key="room.key" class="flex items-center gap-2 px-2 py-1">
<span class="min-w-0 flex-1 truncate text-sm text-gray-500">{{ room.name }}</span>
<UButton icon="i-heroicons-eye" variant="ghost" color="neutral" size="xs" :aria-label="`${room.name} einblenden`" :disabled="!!preferenceBusy" @click="setRoomHidden(room, false)">Einblenden</UButton>
</div>