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
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).
117 lines
4.1 KiB
TypeScript
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;
|
|
}
|