feat: refonte UX du dashboard (coquille, tri/filtre/pagination, ⌘K, toasts)
Refonte front (packages/web) : coquille applicative partagée (sidebar desktop + barre d'onglets mobile + PageHeader, langue/push/logout centralisés) pilotée par route.meta.layout ; design system (BaseButton/BaseBadge/SegmentedControl/EmptyState/ SkeletonRow, anneau de focus, icônes @lucide/vue) ; tri/filtre/recherche/pagination côté client via useListControls (+ synchro URL) sur sessions/worktrees/groupes ; section « À traiter », palette de commandes ⌘K, toasts. Aucune modif serveur/protocole/DB.
This commit is contained in:
103
packages/web/src/composables/useWorktreeView.ts
Normal file
103
packages/web/src/composables/useWorktreeView.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { computed, 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);
|
||||
|
||||
const accessor = (w: WorktreeSummary): string => `${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 {
|
||||
instance ??= create();
|
||||
return instance;
|
||||
}
|
||||
Reference in New Issue
Block a user