Improve tools reliability and add milestone management to ProjectView
tools.py: - Default tag_mode changed from 'replace' to 'add' — existing tags are preserved unless the user explicitly requests replacement - create_task, create_note, update_note, create_milestone: project and milestone lookup now uses get_by_title with fuzzy fallback; returns a clear error (with a hint to use list_projects/list_milestones) instead of silently creating duplicate projects or milestones via get_or_create - create_task: project/milestone resolved before note creation so a bad project name fails fast without leaving an orphaned task behind - update_note return value now includes item_type, tags, project_id, and an 'updated' summary string so the LLM can confirm what was modified - research_topic stub now logs an error and returns failure instead of silently returning success when reached via wrong code path ProjectView.vue: - Milestone headers now show edit (✎) and delete (✕) action buttons on hover - Edit triggers inline rename input; Enter/blur commits, Escape cancels - Delete opens a confirmation modal clarifying tasks are unlinked not deleted Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -65,6 +65,11 @@ const showNewMilestone = ref(false);
|
||||
const newMilestoneTitle = ref("");
|
||||
const creatingMilestone = ref(false);
|
||||
|
||||
// Milestone edit/delete state
|
||||
const renamingMilestoneId = ref<number | null>(null);
|
||||
const renamingMilestoneTitle = ref("");
|
||||
const deletingMilestone = ref<Milestone | null>(null);
|
||||
|
||||
// Edit state
|
||||
const editTitle = ref("");
|
||||
const editDescription = ref("");
|
||||
@@ -156,6 +161,38 @@ async function createMilestone() {
|
||||
}
|
||||
}
|
||||
|
||||
function startRenameMilestone(ms: Milestone) {
|
||||
renamingMilestoneId.value = ms.id;
|
||||
renamingMilestoneTitle.value = ms.title;
|
||||
}
|
||||
|
||||
async function commitRenameMilestone(ms: Milestone) {
|
||||
const newTitle = renamingMilestoneTitle.value.trim();
|
||||
renamingMilestoneId.value = null;
|
||||
if (!newTitle || newTitle === ms.title) return;
|
||||
try {
|
||||
await apiPatch(`/api/projects/${projectId.value}/milestones/${ms.id}`, { title: newTitle });
|
||||
await loadMilestones();
|
||||
toast.show("Milestone renamed");
|
||||
} catch {
|
||||
toast.show("Failed to rename milestone", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmDeleteMilestone() {
|
||||
const ms = deletingMilestone.value;
|
||||
if (!ms) return;
|
||||
deletingMilestone.value = null;
|
||||
try {
|
||||
await apiDelete(`/api/projects/${projectId.value}/milestones/${ms.id}`);
|
||||
await loadMilestones();
|
||||
await loadTasks();
|
||||
toast.show("Milestone deleted");
|
||||
} catch {
|
||||
toast.show("Failed to delete milestone", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTasks() {
|
||||
tasksLoading.value = true;
|
||||
try {
|
||||
@@ -384,13 +421,25 @@ function formatDate(dateStr?: string | null): string {
|
||||
<!-- Milestone header -->
|
||||
<div
|
||||
class="milestone-header"
|
||||
@click="group.milestone && toggleMilestoneCollapse(group.milestone.id)"
|
||||
:class="{ clickable: !!group.milestone }"
|
||||
@click="group.milestone && renamingMilestoneId !== group.milestone.id && toggleMilestoneCollapse(group.milestone.id)"
|
||||
:class="{ clickable: !!group.milestone && renamingMilestoneId !== group.milestone?.id }"
|
||||
>
|
||||
<span class="ms-chevron" v-if="group.milestone">
|
||||
{{ collapsedMilestones.has(group.milestone.id) ? '▶' : '▼' }}
|
||||
</span>
|
||||
<span class="ms-name">{{ group.milestone?.title ?? "No Milestone" }}</span>
|
||||
<!-- Inline rename input -->
|
||||
<template v-if="group.milestone && renamingMilestoneId === group.milestone.id">
|
||||
<input
|
||||
class="ms-rename-input"
|
||||
v-model="renamingMilestoneTitle"
|
||||
@click.stop
|
||||
@keydown.enter.stop="commitRenameMilestone(group.milestone)"
|
||||
@keydown.escape.stop="renamingMilestoneId = null"
|
||||
@blur="commitRenameMilestone(group.milestone)"
|
||||
autofocus
|
||||
/>
|
||||
</template>
|
||||
<span v-else class="ms-name">{{ group.milestone?.title ?? "No Milestone" }}</span>
|
||||
<span class="ms-count">{{ group.tasks.length }}</span>
|
||||
<template v-if="group.milestone">
|
||||
<div class="ms-progress-track">
|
||||
@@ -400,6 +449,19 @@ function formatDate(dateStr?: string | null): string {
|
||||
></div>
|
||||
</div>
|
||||
<span class="ms-pct">{{ group.milestone.pct }}%</span>
|
||||
<!-- Edit / delete actions -->
|
||||
<div class="ms-actions" @click.stop>
|
||||
<button
|
||||
class="ms-action-btn"
|
||||
title="Rename milestone"
|
||||
@click="startRenameMilestone(group.milestone)"
|
||||
>✎</button>
|
||||
<button
|
||||
class="ms-action-btn ms-action-delete"
|
||||
title="Delete milestone"
|
||||
@click="deletingMilestone = group.milestone"
|
||||
>✕</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -497,7 +559,24 @@ function formatDate(dateStr?: string | null): string {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Delete confirmation modal -->
|
||||
<!-- Milestone delete confirmation modal -->
|
||||
<teleport to="body">
|
||||
<div v-if="deletingMilestone" class="modal-overlay" @click.self="deletingMilestone = null">
|
||||
<div class="modal-card">
|
||||
<h3 class="modal-title">Delete Milestone</h3>
|
||||
<p class="modal-message">
|
||||
Delete <strong>{{ deletingMilestone.title }}</strong>?
|
||||
Tasks will be unlinked from this milestone but not deleted.
|
||||
</p>
|
||||
<div class="modal-actions">
|
||||
<button class="modal-btn" @click="deletingMilestone = null">Cancel</button>
|
||||
<button class="modal-btn modal-btn-danger" @click="confirmDeleteMilestone">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
|
||||
<!-- Project delete confirmation modal -->
|
||||
<teleport to="body">
|
||||
<div v-if="showDeleteConfirm" class="modal-overlay" @click.self="showDeleteConfirm = false">
|
||||
<div class="modal-card">
|
||||
@@ -1044,6 +1123,53 @@ function formatDate(dateStr?: string | null): string {
|
||||
}
|
||||
.modal-btn-danger:hover { opacity: 0.9; }
|
||||
|
||||
/* Milestone rename input */
|
||||
.ms-rename-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 0.1rem 0.35rem;
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-size: 0.85rem;
|
||||
font-family: inherit;
|
||||
font-weight: 600;
|
||||
}
|
||||
.ms-rename-input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Milestone action buttons */
|
||||
.ms-actions {
|
||||
display: flex;
|
||||
gap: 0.15rem;
|
||||
margin-left: 0.25rem;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.milestone-header:hover .ms-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
.ms-action-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.8rem;
|
||||
padding: 0.15rem 0.3rem;
|
||||
border-radius: var(--radius-sm);
|
||||
line-height: 1;
|
||||
font-family: inherit;
|
||||
}
|
||||
.ms-action-btn:hover {
|
||||
background: var(--color-bg-card);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.ms-action-delete:hover {
|
||||
color: var(--color-danger, #e74c3c);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.project-body {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
Reference in New Issue
Block a user