kin: KIN-UI-003 Консистентная обработка ошибок в del() — использовать throwApiError
This commit is contained in:
parent
fc13245c93
commit
531275e4ce
5 changed files with 112 additions and 35 deletions
|
|
@ -48,7 +48,8 @@ async function post<T>(path: string, body: unknown): Promise<T> {
|
|||
|
||||
async function del<T>(path: string): Promise<T> {
|
||||
const res = await fetch(`${BASE}${path}`, { method: 'DELETE' })
|
||||
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
|
||||
if (!res.ok) await throwApiError(res)
|
||||
if (res.status === 204) return undefined as T
|
||||
return res.json()
|
||||
}
|
||||
|
||||
|
|
@ -270,6 +271,8 @@ export const api = {
|
|||
post<DeployResult>(`/projects/${projectId}/deploy`, {}),
|
||||
syncObsidian: (projectId: string) =>
|
||||
post<ObsidianSyncResult>(`/projects/${projectId}/sync/obsidian`, {}),
|
||||
deleteProject: (id: string) =>
|
||||
del<void>(`/projects/${id}`),
|
||||
deleteDecision: (projectId: string, decisionId: number) =>
|
||||
del<{ deleted: number }>(`/projects/${projectId}/decisions/${decisionId}`),
|
||||
createDecision: (data: { project_id: string; type: string; title: string; description: string; category?: string; tags?: string[] }) =>
|
||||
|
|
|
|||
|
|
@ -139,6 +139,21 @@ function toggleNpRole(key: string) {
|
|||
else npRoles.value.push(key)
|
||||
}
|
||||
|
||||
// Delete project
|
||||
const confirmDeleteId = ref<string | null>(null)
|
||||
const deleteError = ref('')
|
||||
|
||||
async function deleteProject(id: string) {
|
||||
deleteError.value = ''
|
||||
try {
|
||||
await api.deleteProject(id)
|
||||
projects.value = projects.value.filter(p => p.id !== id)
|
||||
confirmDeleteId.value = null
|
||||
} catch (e: any) {
|
||||
deleteError.value = e.message
|
||||
}
|
||||
}
|
||||
|
||||
async function createNewProject() {
|
||||
npError.value = ''
|
||||
if (!npRoles.value.length) {
|
||||
|
|
@ -197,39 +212,66 @@ async function createNewProject() {
|
|||
<p v-else-if="error" class="text-red-400 text-sm">{{ error }}</p>
|
||||
|
||||
<div v-else class="grid gap-3">
|
||||
<router-link
|
||||
v-for="p in projects" :key="p.id"
|
||||
:to="`/project/${p.id}`"
|
||||
class="block border border-gray-800 rounded-lg p-4 hover:border-gray-600 transition-colors no-underline"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-semibold text-gray-200">{{ p.id }}</span>
|
||||
<Badge :text="p.status" :color="statusColor(p.status)" />
|
||||
<Badge v-if="p.project_type && p.project_type !== 'development'"
|
||||
:text="p.project_type"
|
||||
:color="p.project_type === 'operations' ? 'orange' : 'green'" />
|
||||
<span class="text-sm text-gray-400">{{ p.name }}</span>
|
||||
<div v-for="p in projects" :key="p.id">
|
||||
<!-- Inline delete confirmation -->
|
||||
<div v-if="confirmDeleteId === p.id"
|
||||
class="border border-red-800 rounded-lg p-4 bg-red-950/20">
|
||||
<p class="text-sm text-gray-200 mb-3">Удалить проект «{{ p.name }}»? Это действие необратимо.</p>
|
||||
<div class="flex gap-2">
|
||||
<button @click="deleteProject(p.id)"
|
||||
title="Подтвердить удаление"
|
||||
class="px-3 py-1.5 text-xs bg-red-900/50 text-red-400 border border-red-800 rounded hover:bg-red-900">
|
||||
Да, удалить
|
||||
</button>
|
||||
<button @click="confirmDeleteId = null"
|
||||
title="Отмена удаления"
|
||||
class="px-3 py-1.5 text-xs bg-gray-800 text-gray-400 border border-gray-700 rounded hover:bg-gray-700">
|
||||
Отмена
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-xs text-gray-500">
|
||||
<span v-if="costMap[p.id]">${{ costMap[p.id]?.toFixed(2) }}/wk</span>
|
||||
<span>pri {{ p.priority }}</span>
|
||||
<p v-if="deleteError" class="text-red-400 text-xs mt-2">{{ deleteError }}</p>
|
||||
</div>
|
||||
<!-- Normal project card -->
|
||||
<router-link v-else
|
||||
:to="`/project/${p.id}`"
|
||||
class="block border border-gray-800 rounded-lg p-4 hover:border-gray-600 transition-colors no-underline"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-semibold text-gray-200">{{ p.id }}</span>
|
||||
<Badge :text="p.status" :color="statusColor(p.status)" />
|
||||
<Badge v-if="p.project_type && p.project_type !== 'development'"
|
||||
:text="p.project_type"
|
||||
:color="p.project_type === 'operations' ? 'orange' : 'green'" />
|
||||
<span class="text-sm text-gray-400">{{ p.name }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-xs text-gray-500">
|
||||
<span v-if="costMap[p.id]">${{ costMap[p.id]?.toFixed(2) }}/wk</span>
|
||||
<span>pri {{ p.priority }}</span>
|
||||
<button @click.prevent.stop="confirmDeleteId = p.id"
|
||||
title="Удалить проект"
|
||||
class="text-gray-600 hover:text-red-400 transition-colors">
|
||||
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-4 text-xs">
|
||||
<span class="text-gray-500">{{ p.total_tasks }} tasks</span>
|
||||
<span v-if="p.active_tasks" class="text-blue-400">
|
||||
<span class="inline-block w-1.5 h-1.5 bg-blue-500 rounded-full animate-pulse mr-0.5"></span>
|
||||
{{ p.active_tasks }} active
|
||||
</span>
|
||||
<span v-if="p.review_tasks" class="text-yellow-400">{{ p.review_tasks }} awaiting review</span>
|
||||
<span v-if="p.blocked_tasks" class="text-red-400">{{ p.blocked_tasks }} blocked</span>
|
||||
<span v-if="p.done_tasks" class="text-green-500">{{ p.done_tasks }} done</span>
|
||||
<span v-if="p.total_tasks - p.done_tasks - p.active_tasks - p.blocked_tasks - (p.review_tasks || 0) > 0" class="text-gray-500">
|
||||
{{ p.total_tasks - p.done_tasks - p.active_tasks - p.blocked_tasks - (p.review_tasks || 0) }} pending
|
||||
</span>
|
||||
</div>
|
||||
</router-link>
|
||||
<div class="flex gap-4 text-xs">
|
||||
<span class="text-gray-500">{{ p.total_tasks }} tasks</span>
|
||||
<span v-if="p.active_tasks" class="text-blue-400">
|
||||
<span class="inline-block w-1.5 h-1.5 bg-blue-500 rounded-full animate-pulse mr-0.5"></span>
|
||||
{{ p.active_tasks }} active
|
||||
</span>
|
||||
<span v-if="p.review_tasks" class="text-yellow-400">{{ p.review_tasks }} awaiting review</span>
|
||||
<span v-if="p.blocked_tasks" class="text-red-400">{{ p.blocked_tasks }} blocked</span>
|
||||
<span v-if="p.done_tasks" class="text-green-500">{{ p.done_tasks }} done</span>
|
||||
<span v-if="p.total_tasks - p.done_tasks - p.active_tasks - p.blocked_tasks - (p.review_tasks || 0) > 0" class="text-gray-500">
|
||||
{{ p.total_tasks - p.done_tasks - p.active_tasks - p.blocked_tasks - (p.review_tasks || 0) }} pending
|
||||
</span>
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Project Modal -->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue