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 @@
- - @@ -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 @@ -
+
+ + + +
+
@@ -29,103 +48,72 @@
- Keine Seiten vorhanden.
Erstelle den ersten Eintrag oben rechts. + Keine Ergebnisse gefunden. + Keine Seiten vorhanden.
Erstelle den ersten Eintrag oben rechts.
-
- - - - + +
-
{{ new Date(page.createdAt).toLocaleDateString() }} - - Speichert... - + + Speichert... + Gespeichert
-
-
Lade Seite...
-
-

Keine Seite ausgewählt

-

Wähle eine Notiz aus der Navigation.

-
-
-

Möchtest du "{{ modalState.targetItem?.title }}" wirklich löschen?
Alle Unterseiten werden ebenfalls gelöscht.

-