- backend: garde-fou conflit d'édition par mtime (GET renvoie mtime, PUT refuse en 409 STALE_FILE si baseMtime périmé) - lib: wt-key (base64url du chemin worktree), diff-parse (parseur pur du diff unifié → hunks) - composables: useMonaco (import dynamique → chunk isolé), useWorkspaceLayout (largeurs/panneau persistés) - components/workspace: GitStatusBadge, FileTree+FileTreeNode (lazy fs/list includeFiles), ChangedFilesPanel, DiffViewer (coloration +/-), MonacoEditor (Ctrl+S + bannière conflit reload/overwrite) - views/WorkspaceView: 3 colonnes desktop redimensionnables + SegmentedControl mobile, header live (GitStatusBadge), watchWorktree → refresh diff/changements, terminal de la session corrélée - router meta 'ide' + route /workspace/:repoId/:wt ; App.vue plein écran sans AppShell ; WorktreeCard bouton « Ouvrir l'IDE » - deps: monaco-editor (lazy, vendor-monaco isolé, 0 impact bundle initial) ; vite manualChunks - i18n EN+FR (workspace/editor/diff/git) ; tests diff-parse + wt-key ; acceptance-p8.mjs (mtime/409 + diff)
58 lines
2.0 KiB
Vue
58 lines
2.0 KiB
Vue
<template>
|
|
<div class="flex h-full flex-col">
|
|
<div class="flex items-center gap-1 px-2 py-1 text-[11px] font-semibold uppercase tracking-wide text-zinc-500">
|
|
<FolderTree :size="13" /> {{ t('workspace.files') }}
|
|
<button type="button" class="ml-auto rounded p-0.5 hover:bg-zinc-800 hover:text-zinc-300" :title="t('common.refresh')" @click="load">
|
|
<RefreshCw :size="12" />
|
|
</button>
|
|
</div>
|
|
<div class="min-h-0 flex-1 overflow-auto px-1 pb-2">
|
|
<p v-if="loading" class="px-2 py-1 text-xs text-zinc-600">{{ t('fs.loading') }}</p>
|
|
<p v-else-if="error" class="px-2 py-1 text-xs text-rose-400">{{ error }}</p>
|
|
<p v-else-if="entries.length === 0" class="px-2 py-1 text-xs text-zinc-600">{{ t('fs.empty') }}</p>
|
|
<FileTreeNode
|
|
v-for="e in entries"
|
|
:key="e.path"
|
|
:entry="e"
|
|
:wt="wt"
|
|
:depth="0"
|
|
:active="active"
|
|
@open="(p) => emit('open', p)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { onMounted, ref, watch } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { FolderTree, RefreshCw } from '@lucide/vue';
|
|
import type { FsEntry, FsListResponse } from '@arboretum/shared';
|
|
import { api } from '../../lib/api';
|
|
import FileTreeNode from './FileTreeNode.vue';
|
|
|
|
const props = defineProps<{ wt: string; active: string | null }>();
|
|
const emit = defineEmits<{ open: [relPath: string] }>();
|
|
const { t } = useI18n();
|
|
|
|
const entries = ref<FsEntry[]>([]);
|
|
const loading = ref(false);
|
|
const error = ref<string | null>(null);
|
|
|
|
async function load(): Promise<void> {
|
|
loading.value = true;
|
|
error.value = null;
|
|
try {
|
|
const res = await api.get<FsListResponse>(`/api/v1/fs/list?path=${encodeURIComponent(props.wt)}&includeFiles=1`);
|
|
entries.value = [...res.entries].sort((a, b) => Number(!!a.isFile) - Number(!!b.isFile) || a.name.localeCompare(b.name));
|
|
} catch (err) {
|
|
error.value = err instanceof Error ? err.message : String(err);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
onMounted(load);
|
|
watch(() => props.wt, load);
|
|
</script>
|