diff --git a/frontend/components/wiki/TreeItem.vue b/frontend/components/wiki/TreeItem.vue
index a47e453..d4d7da8 100644
--- a/frontend/components/wiki/TreeItem.vue
+++ b/frontend/components/wiki/TreeItem.vue
@@ -31,7 +31,6 @@
-
Neue Seite
@@ -41,7 +40,6 @@
-
Löschen
@@ -59,15 +57,12 @@
import { vOnClickOutside } from '@vueuse/components'
import type { WikiPageItem } from '~/composables/useWikiTree'
-// Rekursiver Component Name (wichtig für Nuxt Auto-Import, oft automatisch 'WikiTreeItem')
-// Falls Recursion Issues auftreten: defineOptions({ name: 'WikiTreeItem' })
-
const props = defineProps<{ item: WikiPageItem; depth?: number }>()
const router = useRouter()
const route = useRoute()
+// NEU: searchQuery importieren für Auto-Expand
+const { searchQuery } = useWikiTree()
-// --- INJECT ---
-// Wir holen uns die Funktion zum Öffnen des Modals von der Hauptseite
const openWikiAction = inject('openWikiAction') as (action: 'create' | 'delete', contextItem: WikiPageItem | null, isFolder?: boolean) => void
const depth = props.depth ?? 0
@@ -76,11 +71,18 @@ const isOpen = ref(false)
const showMenu = ref(false)
const isActive = computed(() => route.params.id === props.item.id)
-// Auto-Open Logic (Ordner öffnen, wenn Kind aktiv ist)
+// Auto-Open Logic 1: Aktive Seite
watch(() => route.params.id, (newId) => {
if (props.item.isFolder && hasActiveChild(props.item, newId as string)) isOpen.value = true
}, { immediate: true })
+// NEU: Auto-Open Logic 2: Suche aktiv -> Alles aufklappen
+watch(searchQuery, (newVal) => {
+ if (newVal.trim().length > 0 && props.item.isFolder) {
+ isOpen.value = true
+ }
+}, { immediate: true })
+
function hasActiveChild(node: WikiPageItem, targetId: string): boolean {
if (node.id === targetId) return true
return node.children?.some(c => hasActiveChild(c, targetId)) ?? false
@@ -89,17 +91,12 @@ function hasActiveChild(node: WikiPageItem, targetId: string): boolean {
function handleClick() {
props.item.isFolder ? (isOpen.value = !isOpen.value) : router.push(`/wiki/${props.item.id}`)
}
-
function toggleFolder() { isOpen.value = !isOpen.value }
-
-// --- ACTIONS ---
-
function triggerCreate(isFolder: boolean) {
showMenu.value = false
- isOpen.value = true // Ordner aufklappen
+ isOpen.value = true
openWikiAction('create', props.item, isFolder)
}
-
function triggerDelete() {
showMenu.value = false
openWikiAction('delete', props.item)
diff --git a/frontend/composables/useWikiTree.ts b/frontend/composables/useWikiTree.ts
index 23f3d19..3f5044a 100644
--- a/frontend/composables/useWikiTree.ts
+++ b/frontend/composables/useWikiTree.ts
@@ -16,15 +16,18 @@ export const useWikiTree = () => {
const isLoading = useState
('wiki-loading', () => false)
const isSidebarOpen = useState('wiki-sidebar-open', () => true)
- // COMPUTED TREE
- const tree = computed(() => {
+ // NEU: Suchbegriff State
+ const searchQuery = useState('wiki-search-query', () => '')
+
+ // 1. Basis-Baum bauen (Hierarchie & Sortierung)
+ const baseTree = computed(() => {
const rawItems = items.value || []
if (!rawItems.length) return []
const roots: WikiPageItem[] = []
const lookup: Record = {}
- // Init Lookup
+ // Init Lookup (Shallow Copy um Originaldaten nicht zu mutieren)
rawItems.forEach(item => {
lookup[item.id] = { ...item, children: [] }
})
@@ -39,7 +42,7 @@ export const useWikiTree = () => {
}
})
- // Sort: Folders first, then Alphabetical
+ // Sort Helper
const sortNodes = (nodes: WikiPageItem[]) => {
nodes.sort((a, b) => {
if (a.isFolder !== b.isFolder) return a.isFolder ? -1 : 1
@@ -54,47 +57,67 @@ export const useWikiTree = () => {
return roots
})
- // ACTIONS
+ // 2. NEU: Gefilterter Baum (basiert auf baseTree + searchQuery)
+ const filteredTree = computed(() => {
+ const query = searchQuery.value.toLowerCase().trim()
+ // Wenn keine Suche: Gib originalen Baum zurück
+ if (!query) return baseTree.value
+
+ // Rekursive Filterfunktion
+ const filterNodes = (nodes: WikiPageItem[]): WikiPageItem[] => {
+ return nodes.reduce((acc: WikiPageItem[], node) => {
+ // Matcht der Knoten selbst?
+ const matchesSelf = node.title.toLowerCase().includes(query)
+
+ // Matchen Kinder? (Rekursion)
+ const filteredChildren = node.children ? filterNodes(node.children) : []
+
+ // Wenn selbst matcht ODER Kinder matchen -> behalten
+ if (matchesSelf || filteredChildren.length > 0) {
+ // Wir erstellen eine Kopie des Knotens mit den gefilterten Kindern
+ acc.push({
+ ...node,
+ children: filteredChildren
+ })
+ }
+
+ return acc
+ }, [])
+ }
+
+ return filterNodes(baseTree.value)
+ })
+
+ // ACTIONS
const loadTree = async () => {
isLoading.value = true
try {
const data = await $api('/api/wiki/tree', { method: 'GET' })
items.value = data
- } catch (e) {
- console.error('Wiki Tree Load Error', e)
- } finally {
- isLoading.value = false
- }
+ } catch (e) { console.error(e) }
+ finally { isLoading.value = false }
}
- // Rein API-basierte Funktion ohne UI-Logik
const createItem = async (title: string, parentId: string | null, isFolder: boolean) => {
try {
- const newItem = await $api('/api/wiki', {
- method: 'POST',
- body: { title, parentId, isFolder }
- })
+ const newItem = await $api('/api/wiki', { method: 'POST', body: { title, parentId, isFolder } })
await loadTree()
return newItem
- } catch (e) {
- throw e // Fehler weiterwerfen, damit UI reagieren kann
- }
+ } catch (e) { throw e }
}
- // Rein API-basierte Funktion ohne UI-Logik
const deleteItem = async (id: string) => {
try {
await $api(`/api/wiki/${id}`, { method: 'DELETE' })
await loadTree()
return true
- } catch (e) {
- throw e
- }
+ } catch (e) { throw e }
}
return {
- tree,
+ tree: filteredTree, // Wir geben jetzt immer den (evtl. gefilterten) Baum zurück
+ searchQuery, // Damit die UI das Input-Feld binden kann
items,
isLoading,
isSidebarOpen,
diff --git a/frontend/pages/wiki/[[id]].vue b/frontend/pages/wiki/[[id]].vue
index 4493db2..9999053 100644
--- a/frontend/pages/wiki/[[id]].vue
+++ b/frontend/pages/wiki/[[id]].vue
@@ -5,7 +5,7 @@
class="flex flex-col border-r border-gray-200 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-900/50 transition-all duration-300 ease-in-out overflow-hidden whitespace-nowrap h-full"
:class="[isSidebarOpen ? 'w-80 min-w-[20rem] translate-x-0' : 'w-0 min-w-0 -translate-x-full opacity-0 border-none']"
>
-
+
📚 Wiki
@@ -14,11 +14,30 @@
-
+
+
+
+
+
+
+
+
-
-
-
-
Keine Seite ausgewählt
-
Wähle eine Notiz aus der Navigation.
-
-
- {{ modalTitle }}
-
+ {{ modalTitle }}
-
-
Möchtest du "{{ modalState.targetItem?.title }}" wirklich löschen?
Alle Unterseiten werden ebenfalls gelöscht.
-
Abbrechen
-
+
{{ modalState.type === 'delete' ? 'Löschen' : 'Erstellen' }}
@@ -141,11 +129,11 @@ import WikiTreeItem from '~/components/wiki/TreeItem.vue'
import WikiEditor from '~/components/wiki/WikiEditor.vue'
import type { WikiPageItem } from '~/composables/useWikiTree'
-// --- SETUP ---
const route = useRoute()
const router = useRouter()
const { $api } = useNuxtApp()
-const { tree, isLoading: isLoadingTree, loadTree, isSidebarOpen, createItem, deleteItem } = useWikiTree()
+// NEU: searchQuery hier entpacken, damit es an UInput gebunden werden kann
+const { tree, isLoading: isLoadingTree, loadTree, isSidebarOpen, createItem, deleteItem, searchQuery } = useWikiTree()
// --- EDITOR LOGIC ---
const pageId = computed(() => route.params.id as string | undefined)
@@ -154,9 +142,7 @@ const lastSaved = ref(false)
const saveTimeout = ref(null)
const contentBuffer = ref(null)
-// Init Load
onMounted(() => loadTree())
-
watch(pageId, () => {
if (saveTimeout.value) clearTimeout(saveTimeout.value)
isSaving.value = false
@@ -170,95 +156,53 @@ const { data: page, pending: pendingPage } = await useAsyncData(
const res = await $api(`/api/wiki/${pageId.value}`)
contentBuffer.value = res.content
return res
- },
- { watch: [pageId], immediate: true }
+ }, { watch: [pageId], immediate: true }
)
-// --- MODAL LOGIC (Zentralisiert) ---
-
+// --- MODAL LOGIC (Identisch wie vorher) ---
const isModalOpen = ref(false)
const modalLoading = ref(false)
-
const modalState = reactive({
type: 'create' as 'create' | 'delete',
- targetItem: null as WikiPageItem | null, // Bei Create: Parent. Bei Delete: Das Item selbst.
+ targetItem: null as WikiPageItem | null,
isFolderCreation: false,
inputValue: ''
})
-
const modalTitle = computed(() => {
if (modalState.type === 'delete') return 'Eintrag löschen'
return modalState.isFolderCreation ? 'Neuen Ordner erstellen' : 'Neue Seite erstellen'
})
-
-// Diese Funktion wird an Kinder via Inject weitergegeben
function openWikiAction(type: 'create' | 'delete', contextItem: WikiPageItem | null, isFolder = false) {
modalState.type = type
modalState.targetItem = contextItem
modalState.isFolderCreation = isFolder
- modalState.inputValue = '' // Reset
-
- if (type === 'create') {
- // Optional: Prefix generieren
- }
-
+ modalState.inputValue = ''
isModalOpen.value = true
}
-
-// Bereitstellen für TreeItems
provide('openWikiAction', openWikiAction)
async function handleModalConfirm() {
modalLoading.value = true
-
try {
if (modalState.type === 'create') {
if (!modalState.inputValue.trim()) return
-
const parentId = modalState.targetItem ? modalState.targetItem.id : null
const newItem = await createItem(modalState.inputValue, parentId, modalState.isFolderCreation)
-
- // Wenn Seite erstellt wurde -> hin navigieren
- if (newItem && !modalState.isFolderCreation) {
- // @ts-ignore
- router.push(`/wiki/${newItem.id}`)
- }
- }
- else if (modalState.type === 'delete' && modalState.targetItem) {
+ if (newItem && !modalState.isFolderCreation) router.push(`/wiki/${newItem.id}`)
+ } else if (modalState.type === 'delete' && modalState.targetItem) {
const success = await deleteItem(modalState.targetItem.id)
-
- // Falls wir die aktuelle Seite löschen, ab zum Root
- if (success && pageId.value === modalState.targetItem.id) {
- router.push('/wiki')
- }
+ if (success && pageId.value === modalState.targetItem.id) router.push('/wiki')
}
-
isModalOpen.value = false
- } catch (e) {
- console.error(e)
- // Hier könnte man einen UNotification Toast anzeigen
- } finally {
- modalLoading.value = false
- }
+ } catch (e) { console.error(e) } finally { modalLoading.value = false }
}
-// --- ROOT MENU ---
-// Konfiguration für UDropdown
const rootMenuItems = [
- [{
- label: 'Neue Seite',
- icon: 'i-heroicons-document-plus',
- click: () => openWikiAction('create', null, false)
- }],
- [{
- label: 'Neuer Ordner',
- icon: 'i-heroicons-folder-plus',
- click: () => openWikiAction('create', null, true)
- }]
+ [{ label: 'Neue Seite', icon: 'i-heroicons-document-plus', click: () => openWikiAction('create', null, false) }],
+ [{ label: 'Neuer Ordner', icon: 'i-heroicons-folder-plus', click: () => openWikiAction('create', null, true) }]
]
-// --- EDITOR ACTIONS ---
-
+// --- EDITOR ACTIONS (Identisch wie vorher) ---
async function saveTitle() {
if (!page.value || !pageId.value) return
isSaving.value = true
@@ -268,7 +212,6 @@ async function saveTitle() {
showSavedFeedback()
} finally { isSaving.value = false }
}
-
function handleContentChange(newContent: any) {
contentBuffer.value = newContent
isSaving.value = true
@@ -282,7 +225,6 @@ function handleContentChange(newContent: any) {
} catch (e) { console.error(e) } finally { isSaving.value = false }
}, 1000)
}
-
function showSavedFeedback() {
lastSaved.value = true
setTimeout(() => { lastSaved.value = false }, 2000)
@@ -294,7 +236,4 @@ function showSavedFeedback() {
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
.custom-scrollbar::-webkit-scrollbar-thumb { background-color: #e5e7eb; border-radius: 10px; }
:deep(.dark) .custom-scrollbar::-webkit-scrollbar-thumb { background-color: #374151; }
-
-.fade-enter-active, .fade-leave-active { transition: opacity 0.5s; }
-.fade-enter-from, .fade-leave-to { opacity: 0; }
\ No newline at end of file