Files
arboretum/packages/web/src/composables/useWorktreeView.ts
Johan LEROY 65ef616867
Some checks failed
CI / Build & test (Node 24) (push) Has been cancelled
CI / Pack & boot smoke (Node 22) (push) Has been cancelled
CI / No em/en dashes (push) Has been cancelled
CI / Build & test (Node 22) (push) Has been cancelled
Deploy site (production) / build-and-deploy (push) Successful in 20s
chore(typo): retire tous les tirets cadratins/demi-cadratins + garde CI
Remplace les 547 tirets cadratins (U+2014) et demi-cadratins (U+2013) des fichiers versionnés par la ponctuation contextuelle adaptée (point médian, deux-points, virgule, parenthèses ; tiret simple pour les plages), sur 122 fichiers (appli, vitrine, doc, tests, workflows, scripts).

Ajoute le job CI « lint-dashes » (git grep -P) qui échoue si un tiret cadratin/demi-cadratin réapparaît, hors logo binaire et captures brutes du terminal (fidélité des fixtures de détection de dialogue).
2026-07-17 16:44:00 +02:00

117 lines
4.1 KiB
TypeScript

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<WorktreeSummary> {
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<SortDir>('asc');
const filters = ref<Record<string, Set<string>>>({});
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<string, string>();
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;
}