- 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).
120 lines
4.4 KiB
Vue
120 lines
4.4 KiB
Vue
<template>
|
|
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4" @click.self="emit('close')">
|
|
<div class="flex max-h-[90vh] w-full max-w-lg flex-col gap-3 overflow-y-auto rounded-lg border border-zinc-800 bg-zinc-900 p-4">
|
|
<header class="flex items-center gap-2">
|
|
<h2 class="font-semibold text-zinc-100">{{ t('project.title') }}</h2>
|
|
<button class="btn ml-auto text-xs" @click="emit('close')">{{ t('common.cancel') }}</button>
|
|
</header>
|
|
<p class="text-xs text-zinc-500">{{ t('project.intro') }}</p>
|
|
|
|
<form class="flex flex-col gap-3" @submit.prevent="onSubmit">
|
|
<label class="flex flex-col gap-1 text-xs text-zinc-400">
|
|
{{ t('project.nameLabel') }}
|
|
<input v-model="name" class="input font-mono" :placeholder="t('project.namePlaceholder')" required :disabled="creating" />
|
|
</label>
|
|
|
|
<label class="flex flex-col gap-1 text-xs text-zinc-400">
|
|
{{ t('project.rootLabel') }}
|
|
<input v-model="root" class="input font-mono" :placeholder="t('project.rootPlaceholder')" required :disabled="creating" />
|
|
</label>
|
|
<div>
|
|
<button type="button" class="btn text-xs" :disabled="creating" @click="showPicker = !showPicker">{{ t('fs.browse') }}</button>
|
|
</div>
|
|
<DirectoryPicker v-if="showPicker" mode="dir" :initial-path="root" @select="onPickRoot" @close="showPicker = false" />
|
|
|
|
<label class="flex items-center gap-2 text-xs text-zinc-300">
|
|
<input v-model="gitInit" type="checkbox" :disabled="creating" /> {{ t('project.gitInit') }}
|
|
</label>
|
|
|
|
<label class="flex flex-col gap-1 text-xs text-zinc-400">
|
|
{{ t('project.commandLabel') }}
|
|
<select v-model="command" class="input" :disabled="creating">
|
|
<option value="claude">claude</option>
|
|
<option value="bash">bash</option>
|
|
</select>
|
|
</label>
|
|
|
|
<p class="font-mono text-[11px] text-zinc-500">{{ previewPath }}</p>
|
|
<p v-if="errorMsg" class="text-xs text-red-400">{{ errorMsg }}</p>
|
|
|
|
<div class="flex items-center gap-2">
|
|
<button type="submit" class="btn-primary" :disabled="creating || name.trim() === '' || root.trim() === ''">
|
|
{{ creating ? t('project.creating') : t('project.create') }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onMounted, ref } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useSessionsStore } from '../stores/sessions';
|
|
import { useSettingsStore } from '../stores/settings';
|
|
import { useToastsStore } from '../stores/toasts';
|
|
import DirectoryPicker from './DirectoryPicker.vue';
|
|
|
|
const emit = defineEmits<{ close: [] }>();
|
|
|
|
const { t } = useI18n();
|
|
const router = useRouter();
|
|
const sessions = useSessionsStore();
|
|
const settings = useSettingsStore();
|
|
const toasts = useToastsStore();
|
|
|
|
const name = ref('');
|
|
// Racine par défaut : la 1ère racine de scan (réglage Découverte) ; modifiable via le picker.
|
|
const root = ref('');
|
|
const gitInit = ref(false);
|
|
const command = ref<'claude' | 'bash'>('claude');
|
|
const showPicker = ref(false);
|
|
const creating = ref(false);
|
|
const errorMsg = ref<string | null>(null);
|
|
|
|
// Aperçu du chemin final (indicatif ; jointure naïve suffisante pour l'affichage).
|
|
const previewPath = computed(() => {
|
|
const r = root.value.trim().replace(/\/+$/, '');
|
|
const n = name.value.trim();
|
|
return r && n ? `${r}/${n}` : '';
|
|
});
|
|
|
|
onMounted(async () => {
|
|
if (!settings.loaded) {
|
|
try {
|
|
await settings.fetch();
|
|
} catch {
|
|
/* le préremplissage de la racine est facultatif */
|
|
}
|
|
}
|
|
root.value = settings.scanRoots[0] ?? '';
|
|
});
|
|
|
|
function onPickRoot(path: string): void {
|
|
root.value = path;
|
|
showPicker.value = false;
|
|
}
|
|
|
|
async function onSubmit(): Promise<void> {
|
|
if (creating.value || name.value.trim() === '' || root.value.trim() === '') return;
|
|
creating.value = true;
|
|
errorMsg.value = null;
|
|
try {
|
|
const res = await sessions.createProject({
|
|
root: root.value.trim(),
|
|
name: name.value.trim(),
|
|
gitInit: gitInit.value,
|
|
command: command.value,
|
|
});
|
|
toasts.success(t('toast.projectCreated'));
|
|
emit('close');
|
|
await router.push({ name: 'session', params: { id: res.session.id } });
|
|
} catch (err) {
|
|
errorMsg.value = err instanceof Error ? err.message : String(err);
|
|
} finally {
|
|
creating.value = false;
|
|
}
|
|
}
|
|
</script>
|