feat: création de projet (nouveau dossier + session) & en-tête de session enrichi
- POST /api/v1/projects : crée <root>/<name> (mkdir non récursif, 409 si existant), git init optionnel, puis lance une session dedans. Anti-traversal sur le nom (core/project.ts). - Web : NewProjectModal (racine préremplie scanRoots[0], case git init) + bouton dans SessionsListView. - SessionContextBar : badge du groupe (cliquable) + repos couverts avec leur branche dans l'en-tête de session ; session simple = repo+branche déduits du cwd. - 14 tests (unitaires + route).
This commit is contained in:
82
packages/web/src/components/SessionContextBar.vue
Normal file
82
packages/web/src/components/SessionContextBar.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<!-- Barre de contexte sous l'en-tête : groupe + repos couverts (avec branche). Masquée s'il n'y a
|
||||
rien d'utile à montrer (session simple dans un dossier non reconnu → le cwd du header suffit). -->
|
||||
<div v-if="shouldShow" class="flex flex-wrap items-center gap-x-3 gap-y-1 border-b border-zinc-800 bg-zinc-900/40 px-3 py-1.5 text-xs">
|
||||
<RouterLink
|
||||
v-if="group"
|
||||
:to="{ name: 'group', params: { id: group.id } }"
|
||||
class="flex items-center gap-1.5 rounded px-1.5 py-0.5 text-zinc-200 transition-colors hover:bg-zinc-800"
|
||||
:title="t('terminal.group')"
|
||||
>
|
||||
<span class="h-2.5 w-2.5 shrink-0 rounded-full" :style="{ backgroundColor: group.color ?? '#52525b' }" />
|
||||
<span class="font-medium">{{ group.label }}</span>
|
||||
<span class="text-zinc-500">→</span>
|
||||
</RouterLink>
|
||||
|
||||
<span v-if="group" class="text-zinc-600">{{ t('terminal.covers') }}</span>
|
||||
|
||||
<span
|
||||
v-for="e in entries"
|
||||
:key="e.dir"
|
||||
class="flex items-center gap-1.5 rounded bg-zinc-950/60 px-1.5 py-0.5"
|
||||
:title="e.dir"
|
||||
>
|
||||
<span class="font-mono text-zinc-300">{{ e.label }}</span>
|
||||
<span v-if="e.branch" class="badge bg-zinc-800 font-mono text-zinc-400">{{ e.branch }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { SessionSummary } from '@arboretum/shared';
|
||||
import { useGroupsStore } from '../stores/groups';
|
||||
import { useWorktreesStore } from '../stores/worktrees';
|
||||
|
||||
const props = defineProps<{ session: SessionSummary }>();
|
||||
const { t } = useI18n();
|
||||
const groups = useGroupsStore();
|
||||
const worktrees = useWorktreesStore();
|
||||
|
||||
const group = computed(() => (props.session.groupId ? (groups.byId(props.session.groupId) ?? null) : null));
|
||||
|
||||
function basename(dir: string): string {
|
||||
return dir.split('/').filter(Boolean).pop() ?? dir;
|
||||
}
|
||||
|
||||
interface CoverEntry {
|
||||
dir: string;
|
||||
label: string;
|
||||
branch: string | null;
|
||||
resolved: boolean;
|
||||
}
|
||||
|
||||
// Résout un répertoire couvert vers repo + branche : worktree exact d'abord (branche connue),
|
||||
// sinon checkout principal d'un repo, sinon dernier segment du chemin (non résolu).
|
||||
function resolve(dir: string): CoverEntry {
|
||||
const wt = worktrees.worktrees.find((w) => w.path === dir);
|
||||
if (wt) {
|
||||
const repo = worktrees.repos.find((r) => r.id === wt.repoId);
|
||||
return { dir, label: repo?.label ?? basename(dir), branch: wt.branch, resolved: true };
|
||||
}
|
||||
const repo = worktrees.repos.find((r) => r.path === dir);
|
||||
if (repo) return { dir, label: repo.label, branch: null, resolved: true };
|
||||
return { dir, label: basename(dir), branch: null, resolved: false };
|
||||
}
|
||||
|
||||
const entries = computed<CoverEntry[]>(() => {
|
||||
const dirs = [props.session.cwd, ...(props.session.addedDirs ?? [])];
|
||||
const seen = new Set<string>();
|
||||
const out: CoverEntry[] = [];
|
||||
for (const d of dirs) {
|
||||
if (seen.has(d)) continue;
|
||||
seen.add(d);
|
||||
out.push(resolve(d));
|
||||
}
|
||||
return out;
|
||||
});
|
||||
|
||||
// Afficher si on est dans un groupe, ou si au moins un répertoire correspond à un repo connu.
|
||||
const shouldShow = computed(() => group.value !== null || entries.value.some((e) => e.resolved));
|
||||
</script>
|
||||
Reference in New Issue
Block a user