Projekt löschen in drei Stufen mit Artefakt-Vorschau; Listen-Scroll gefixt
Eskalationsleiter: 'integration' nimmt nur die ai-control-Spuren zurück (Registry, .ai-control/, Archiv-Rechte + Todo-Hook in settings.json, Panel-Dateien, .desktop — Ordner, memory/ und Claude-Code-Grundgerüst bleiben), 'archive' löscht zusätzlich das Archiv, 'full' den Projekt- ordner mit Arbeitsordner-Flag. delete_preview liefert die konkrete Artefaktliste; der Dialog zeigt sie je Stufe. Ersetzt delete_project/ remove_project (Handler, App-ACL-Manifest und Capability nachgezogen). Außerdem: main scrollt nicht mehr — Toolbars und Fehlerzeile stehen, nur die Listen scrollen (alle drei Tabs, .list-scroll).
This commit is contained in:
@@ -201,7 +201,8 @@ onMounted(refresh);
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
|
||||
<table v-if="pools.length" class="grid">
|
||||
<div v-if="pools.length" class="list-scroll">
|
||||
<table class="grid">
|
||||
<colgroup>
|
||||
<col />
|
||||
<col class="col-type" />
|
||||
@@ -268,6 +269,7 @@ onMounted(refresh);
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p v-else class="empty">{{ $t("pools.empty") }}</p>
|
||||
|
||||
<div v-if="dialog" class="overlay" @click.self="closeDialog">
|
||||
|
||||
+122
-40
@@ -379,10 +379,25 @@ async function createProject() {
|
||||
}
|
||||
}
|
||||
|
||||
interface DeletePreview {
|
||||
name: string;
|
||||
projectDir: string;
|
||||
aiControlDir: boolean;
|
||||
archivePermission: boolean;
|
||||
todoHook: boolean;
|
||||
panelFiles: number;
|
||||
archiveHome: string | null;
|
||||
archiveDocs: number;
|
||||
workDirs: string[];
|
||||
}
|
||||
|
||||
// Eskalationsleiter: jede Stufe schließt die vorige ein.
|
||||
type DeleteScope = "integration" | "archive" | "full";
|
||||
|
||||
interface PendingDelete {
|
||||
id: string;
|
||||
name: string;
|
||||
workDirs: string[];
|
||||
preview: DeletePreview;
|
||||
scope: DeleteScope;
|
||||
deleteWorkDirs: boolean;
|
||||
}
|
||||
|
||||
@@ -391,10 +406,8 @@ const pendingDelete = ref<PendingDelete | null>(null);
|
||||
async function askDelete(p: Project) {
|
||||
error.value = "";
|
||||
try {
|
||||
const workDirs = await invoke<string[]>("project_work_dirs", {
|
||||
project: p.id,
|
||||
});
|
||||
pendingDelete.value = { id: p.id, name: p.name, workDirs, deleteWorkDirs: false };
|
||||
const preview = await invoke<DeletePreview>("delete_preview", { project: p.id });
|
||||
pendingDelete.value = { id: p.id, preview, scope: "integration", deleteWorkDirs: false };
|
||||
} catch (e) {
|
||||
error.value = String(e);
|
||||
}
|
||||
@@ -403,9 +416,10 @@ async function askDelete(p: Project) {
|
||||
async function confirmDelete() {
|
||||
const d = pendingDelete.value!;
|
||||
try {
|
||||
await invoke("delete_project", {
|
||||
await invoke("delete_project_scoped", {
|
||||
project: d.id,
|
||||
deleteWorkDirs: d.deleteWorkDirs,
|
||||
scope: d.scope,
|
||||
deleteWorkDirs: d.scope === "full" && d.deleteWorkDirs,
|
||||
});
|
||||
pendingDelete.value = null;
|
||||
await refresh();
|
||||
@@ -415,19 +429,6 @@ async function confirmDelete() {
|
||||
}
|
||||
}
|
||||
|
||||
// Nur den Registry-Eintrag entfernen; der Ordner bleibt.
|
||||
async function unlinkProject() {
|
||||
const d = pendingDelete.value!;
|
||||
try {
|
||||
await invoke("remove_project", { project: d.id });
|
||||
pendingDelete.value = null;
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
error.value = String(e);
|
||||
pendingDelete.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
let timer: number;
|
||||
onMounted(() => {
|
||||
refresh();
|
||||
@@ -447,6 +448,7 @@ onUnmounted(() => window.clearInterval(timer));
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<div class="list-scroll">
|
||||
<table class="grid">
|
||||
<colgroup>
|
||||
<col class="col-dot" />
|
||||
@@ -516,6 +518,7 @@ onUnmounted(() => window.clearInterval(timer));
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div v-if="wizard" class="overlay" @click.self="wizard = null">
|
||||
<form class="dialog" @submit.prevent="createProject">
|
||||
@@ -619,30 +622,76 @@ onUnmounted(() => window.clearInterval(timer));
|
||||
|
||||
<div v-if="pendingDelete" class="overlay" @click.self="pendingDelete = null">
|
||||
<div class="dialog">
|
||||
<h3>{{ $t("projects.deleteTitle", { name: pendingDelete.name }) }}</h3>
|
||||
<p class="hint">{{ $t("projects.deleteWarning", { name: pendingDelete.name }) }}</p>
|
||||
<template v-if="pendingDelete.workDirs.length">
|
||||
<label class="checkline">
|
||||
<input v-model="pendingDelete.deleteWorkDirs" type="checkbox" />
|
||||
{{ $t("projects.deleteWorkDirs") }}
|
||||
</label>
|
||||
<ul class="affected">
|
||||
<li v-for="wd in pendingDelete.workDirs" :key="wd">{{ wd }}</li>
|
||||
</ul>
|
||||
<p v-if="!pendingDelete.deleteWorkDirs" class="hint">
|
||||
{{ $t("projects.workDirsStay") }}
|
||||
</p>
|
||||
</template>
|
||||
<p class="hint">{{ $t("projects.unlinkHint") }}</p>
|
||||
<h3>{{ $t("projects.deleteTitle", { name: pendingDelete.preview.name }) }}</h3>
|
||||
|
||||
<label class="scope">
|
||||
<input v-model="pendingDelete.scope" type="radio" value="integration" />
|
||||
<span>
|
||||
<strong>{{ $t("projects.scopeIntegration") }}</strong>
|
||||
<small>{{ $t("projects.scopeIntegrationDesc") }}</small>
|
||||
</span>
|
||||
</label>
|
||||
<label class="scope">
|
||||
<input
|
||||
v-model="pendingDelete.scope"
|
||||
type="radio"
|
||||
value="archive"
|
||||
:disabled="!pendingDelete.preview.archiveHome"
|
||||
/>
|
||||
<span>
|
||||
<strong>{{ $t("projects.scopeArchive") }}</strong>
|
||||
<small v-if="pendingDelete.preview.archiveHome">
|
||||
{{ $t("projects.scopeArchiveDesc", { path: pendingDelete.preview.archiveHome }) }}
|
||||
({{ $t("projects.docCount", pendingDelete.preview.archiveDocs) }})
|
||||
</small>
|
||||
<small v-else>{{ $t("projects.archiveNone") }}</small>
|
||||
</span>
|
||||
</label>
|
||||
<label class="scope">
|
||||
<input v-model="pendingDelete.scope" type="radio" value="full" />
|
||||
<span>
|
||||
<strong>{{ $t("projects.scopeFull") }}</strong>
|
||||
<small>{{ $t("projects.scopeFullDesc", { path: pendingDelete.preview.projectDir }) }}</small>
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
v-if="pendingDelete.scope === 'full' && pendingDelete.preview.workDirs.length"
|
||||
class="checkline"
|
||||
>
|
||||
<input v-model="pendingDelete.deleteWorkDirs" type="checkbox" />
|
||||
{{ $t("projects.deleteWorkDirs") }}
|
||||
</label>
|
||||
|
||||
<p class="hint">{{ $t("projects.deletePreviewTitle") }}</p>
|
||||
<ul class="affected">
|
||||
<li>{{ $t("projects.artRegistry") }}</li>
|
||||
<li v-if="pendingDelete.preview.aiControlDir">{{ $t("projects.artAiControl") }}</li>
|
||||
<li v-if="pendingDelete.preview.archivePermission">{{ $t("projects.artArchivePerm") }}</li>
|
||||
<li v-if="pendingDelete.preview.todoHook">{{ $t("projects.artTodoHook") }}</li>
|
||||
<li v-if="pendingDelete.preview.panelFiles">
|
||||
{{ $t("projects.artPanelFiles", pendingDelete.preview.panelFiles) }}
|
||||
</li>
|
||||
<li>{{ $t("projects.artDesktop") }}</li>
|
||||
<li v-if="pendingDelete.scope !== 'integration' && pendingDelete.preview.archiveHome">
|
||||
{{ $t("projects.artArchive", { path: pendingDelete.preview.archiveHome }) }}
|
||||
({{ $t("projects.docCount", pendingDelete.preview.archiveDocs) }})
|
||||
</li>
|
||||
<li v-if="pendingDelete.scope === 'full'">
|
||||
{{ $t("projects.artProjectDir", { path: pendingDelete.preview.projectDir }) }}
|
||||
</li>
|
||||
<template v-if="pendingDelete.scope === 'full' && pendingDelete.deleteWorkDirs">
|
||||
<li v-for="wd in pendingDelete.preview.workDirs" :key="wd">
|
||||
{{ $t("projects.artWorkDir", { path: wd }) }}
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
|
||||
<div class="actions">
|
||||
<button type="button" @click="pendingDelete = null">
|
||||
{{ $t("projects.cancel") }}
|
||||
</button>
|
||||
<button @click="unlinkProject">
|
||||
{{ $t("projects.unlink") }}
|
||||
</button>
|
||||
<button class="danger" @click="confirmDelete">
|
||||
{{ $t("projects.delete") }}
|
||||
{{ $t("projects.deleteConfirm") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -784,6 +833,39 @@ onUnmounted(() => window.clearInterval(timer));
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Lösch-Dialog: drei Stufen als Radio-Zeilen mit Beschreibung. */
|
||||
.scope {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.6rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.scope input {
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.scope span {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
|
||||
.scope strong {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.scope small {
|
||||
color: var(--overlay);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.scope:has(input:disabled) {
|
||||
opacity: 0.55;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Settings-Dialog: festes Grid Label | Wert | Aktionen in drei Gruppen. */
|
||||
.settings-dialog {
|
||||
position: relative;
|
||||
|
||||
@@ -113,7 +113,8 @@ function cost(n: number): string {
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
|
||||
<table v-if="groups.length" class="grid">
|
||||
<div v-if="groups.length" class="list-scroll">
|
||||
<table class="grid">
|
||||
<colgroup>
|
||||
<col />
|
||||
<col class="col-num" />
|
||||
@@ -164,5 +165,6 @@ function cost(n: number): string {
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<p v-else-if="!loading" class="empty">{{ $t("usage.empty") }}</p>
|
||||
</template>
|
||||
|
||||
+38
-12
@@ -71,13 +71,26 @@ const de = {
|
||||
delete: "Projekt löschen",
|
||||
deleteRunning: "Läuft noch — erst beenden",
|
||||
deleteTitle: "{name} löschen",
|
||||
deleteWarning:
|
||||
"Der Projektordner (inkl. memory/, Settings und Pool-Zuordnung) wird endgültig gelöscht.",
|
||||
deleteConfirm: "Entfernen",
|
||||
deleteWorkDirs: "Arbeitsordner mitlöschen:",
|
||||
workDirsStay: "Die Arbeitsordner bleiben unangetastet.",
|
||||
unlink: "Nur aus Liste nehmen",
|
||||
unlinkHint:
|
||||
"Nur aus Liste nehmen entfernt den Eintrag; der Ordner bleibt unangetastet.",
|
||||
scopeIntegration: "Nur Integration entfernen",
|
||||
scopeIntegrationDesc:
|
||||
"ai-control-Spuren werden entfernt; Projektordner, memory/ und Todoliste bleiben.",
|
||||
scopeArchive: "Integration & Archiv",
|
||||
scopeArchiveDesc: "zusätzlich {path} — endgültig.",
|
||||
scopeFull: "Projekt komplett löschen",
|
||||
scopeFullDesc: "zusätzlich der Projektordner {path} — endgültig.",
|
||||
deletePreviewTitle: "Wird entfernt:",
|
||||
docCount: "kein Dokument | 1 Dokument | {count} Dokumente",
|
||||
artRegistry: "Registry-Eintrag",
|
||||
artAiControl: ".ai-control/ (Config + Icon)",
|
||||
artArchivePerm: "Archiv-Berechtigung in .claude/settings.json",
|
||||
artTodoHook: "Todo-Hook in .claude/settings.json",
|
||||
artPanelFiles: "Panel-Kanaldatei | {count} Panel-Kanaldateien",
|
||||
artDesktop: ".desktop-Starter",
|
||||
artArchive: "Archiv {path}",
|
||||
artProjectDir: "Projektordner {path}",
|
||||
artWorkDir: "Arbeitsordner {path}",
|
||||
todo: "Todoliste",
|
||||
todoDesc: "OFFENE-PUNKTE.md bei jedem Sessionstart einspielen",
|
||||
groupAppearance: "Darstellung",
|
||||
@@ -201,13 +214,26 @@ const en: typeof de = {
|
||||
delete: "Delete project",
|
||||
deleteRunning: "Still running — stop it first",
|
||||
deleteTitle: "Delete {name}",
|
||||
deleteWarning:
|
||||
"The project folder (incl. memory/, settings and pool assignment) will be deleted permanently.",
|
||||
deleteConfirm: "Remove",
|
||||
deleteWorkDirs: "Also delete working folders:",
|
||||
workDirsStay: "The working folders stay untouched.",
|
||||
unlink: "Remove from list only",
|
||||
unlinkHint:
|
||||
"Remove from list only drops the entry; the folder stays untouched.",
|
||||
scopeIntegration: "Remove integration only",
|
||||
scopeIntegrationDesc:
|
||||
"ai-control traces are removed; project folder, memory/ and todo list stay.",
|
||||
scopeArchive: "Integration & archive",
|
||||
scopeArchiveDesc: "additionally {path} — permanent.",
|
||||
scopeFull: "Delete project completely",
|
||||
scopeFullDesc: "additionally the project folder {path} — permanent.",
|
||||
deletePreviewTitle: "Will be removed:",
|
||||
docCount: "no documents | 1 document | {count} documents",
|
||||
artRegistry: "registry entry",
|
||||
artAiControl: ".ai-control/ (config + icon)",
|
||||
artArchivePerm: "archive permission in .claude/settings.json",
|
||||
artTodoHook: "todo hook in .claude/settings.json",
|
||||
artPanelFiles: "panel channel file | {count} panel channel files",
|
||||
artDesktop: ".desktop launcher",
|
||||
artArchive: "archive {path}",
|
||||
artProjectDir: "project folder {path}",
|
||||
artWorkDir: "working folder {path}",
|
||||
todo: "Todo list",
|
||||
todoDesc: "Inject OFFENE-PUNKTE.md at every session start",
|
||||
groupAppearance: "Appearance",
|
||||
|
||||
+11
-1
@@ -123,10 +123,20 @@ input[type="checkbox"] {
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Tab-Inhalte: Toolbar und Fehlerzeile stehen, nur die Liste scrollt. */
|
||||
.list-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* ---------- Controls ---------- */
|
||||
|
||||
button,
|
||||
|
||||
Reference in New Issue
Block a user