dev #247

Merged
flfeders merged 10 commits from dev into main 2026-10-15 16:47:32 +00:00
2 changed files with 51 additions and 5 deletions
Showing only changes of commit f56a07f8e3 - Show all commits

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>

View File

@@ -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)