import { computed, effectScope, ref } from 'vue'; import type { WorktreeSummary } from '@arboretum/shared'; import type { ListToolbarControls, SortDir } from './useListControls'; import { useDebouncedRef } from './useDebouncedRef'; import { worktreeSorts, worktreeFilters } from './listDefs'; import { useWorktreesStore } from '../stores/worktrees'; export interface WorktreeView extends ListToolbarControls { apply: (list: readonly WorktreeSummary[]) => WorktreeSummary[]; isActive: () => boolean; } /** * État de tri/filtre/recherche des worktrees, PARTAGÉ entre toutes les RepoSection du dashboard * (une seule toolbar pilote toutes les sections). Singleton module-level — et non un store Pinia, * car Pinia déballe les refs, ce que ListToolbar attend justement comme refs. Pas de pagination * (liste groupée par repo) ; `apply()` filtre+trie le sous-ensemble d'un repo donné. */ let instance: WorktreeView | null = null; function create(): WorktreeView { const search = ref(''); const debounced = useDebouncedRef(search, 150); const sortKey = ref('main'); const sortDir = ref('asc'); const filters = ref>>({}); const sortDefs = worktreeSorts(); const filterDefList = worktreeFilters(); const filterDefs = computed(() => filterDefList); // label de repo cherchable (en plus de la branche et du chemin) → la recherche couvre le nom du dépôt. const repoLabelById = computed(() => { const m = new Map(); for (const r of useWorktreesStore().repos) m.set(r.id, r.label); return m; }); const accessor = (w: WorktreeSummary): string => `${repoLabelById.value.get(w.repoId) ?? ''} ${w.branch ?? ''} ${w.path}`.toLowerCase(); function apply(list: readonly WorktreeSummary[]): WorktreeSummary[] { let items = [...list]; for (const def of filterDefList) { const active = filters.value[def.key]; if (active && active.size > 0) items = items.filter((it) => def.predicate(it, active)); } const q = debounced.value.trim().toLowerCase(); if (q) items = items.filter((w) => accessor(w).includes(q)); const def = sortDefs.find((s) => s.key === sortKey.value) ?? sortDefs[0]; if (def) { const dir = sortDir.value === 'desc' ? -1 : 1; items.sort((a, b) => def.compare(a, b) * dir); } return items; } const isActive = (): boolean => debounced.value.trim() !== '' || Object.keys(filters.value).length > 0; // total (compteur de la toolbar) : worktrees correspondants, tous repos confondus. const total = computed(() => apply(useWorktreesStore().worktrees).length); const activeFilterCount = computed(() => Object.values(filters.value).reduce((n, set) => n + set.size, 0), ); function setSearch(v: string): void { search.value = v; } function setSort(key: string): void { if (sortKey.value === key) sortDir.value = sortDir.value === 'asc' ? 'desc' : 'asc'; else { sortKey.value = key; sortDir.value = sortDefs.find((s) => s.key === key)?.defaultDir ?? 'asc'; } } function toggleFilter(key: string, value: string): void { const next = { ...filters.value }; const set = new Set(next[key] ?? []); if (set.has(value)) set.delete(value); else set.add(value); if (set.size === 0) delete next[key]; else next[key] = set; filters.value = next; } function clearAll(): void { search.value = ''; filters.value = {}; } return { search, sortKey, sortDir, filters, total, activeFilterCount, setSearch, setSort, toggleFilter, clearAll, sortDefs, filterDefs, apply, isActive, }; } export function useWorktreeView(): WorktreeView { // Effets (watch du debounce, computeds) créés dans un scope DÉTACHÉ : sinon ils seraient rattachés // au scope du premier composant qui appelle ce singleton et stoppés à son démontage (navigation), // gelant le ref débouncé → la recherche ne filtrerait plus après un aller-retour de route. if (!instance) { const scope = effectScope(true); instance = scope.run(() => create())!; } return instance; }