kin: KIN-008 Добавить возможность смены приоритетности и типа задачи руками из тасков
This commit is contained in:
parent
77ed68c2b5
commit
a48892d456
4 changed files with 166 additions and 4 deletions
|
|
@ -845,3 +845,58 @@ def test_patch_task_empty_body_still_returns_400(client):
|
|||
"""Пустое тело по-прежнему возвращает 400 (регрессия KIN-008)."""
|
||||
r = client.patch("/api/tasks/P1-001", json={})
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
# PATCH /api/tasks/{id} — редактирование title и brief_text (KIN-015)
|
||||
|
||||
def test_patch_task_title(client):
|
||||
"""PATCH title обновляет заголовок задачи."""
|
||||
r = client.patch("/api/tasks/P1-001", json={"title": "Новый заголовок"})
|
||||
assert r.status_code == 200
|
||||
assert r.json()["title"] == "Новый заголовок"
|
||||
|
||||
|
||||
def test_patch_task_title_persisted(client):
|
||||
"""PATCH title сохраняется в БД."""
|
||||
client.patch("/api/tasks/P1-001", json={"title": "Персистентный заголовок"})
|
||||
r = client.get("/api/tasks/P1-001")
|
||||
assert r.json()["title"] == "Персистентный заголовок"
|
||||
|
||||
|
||||
def test_patch_task_title_empty_returns_400(client):
|
||||
"""Пустой title → 400."""
|
||||
r = client.patch("/api/tasks/P1-001", json={"title": " "})
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
def test_patch_task_brief_text(client):
|
||||
"""PATCH brief_text сохраняется в brief.text."""
|
||||
r = client.patch("/api/tasks/P1-001", json={"brief_text": "Описание задачи"})
|
||||
assert r.status_code == 200
|
||||
assert r.json()["brief"]["text"] == "Описание задачи"
|
||||
|
||||
|
||||
def test_patch_task_brief_text_persisted(client):
|
||||
"""PATCH brief_text сохраняется в БД."""
|
||||
client.patch("/api/tasks/P1-001", json={"brief_text": "Сохранённое описание"})
|
||||
r = client.get("/api/tasks/P1-001")
|
||||
assert r.json()["brief"]["text"] == "Сохранённое описание"
|
||||
|
||||
|
||||
def test_patch_task_brief_text_merges_route_type(client):
|
||||
"""brief_text не перезаписывает route_type в brief."""
|
||||
client.patch("/api/tasks/P1-001", json={"route_type": "feature"})
|
||||
client.patch("/api/tasks/P1-001", json={"brief_text": "Описание"})
|
||||
r = client.get("/api/tasks/P1-001")
|
||||
brief = r.json()["brief"]
|
||||
assert brief["text"] == "Описание"
|
||||
assert brief["route_type"] == "feature"
|
||||
|
||||
|
||||
def test_patch_task_title_and_brief_text_together(client):
|
||||
"""PATCH может обновить title и brief_text одновременно."""
|
||||
r = client.patch("/api/tasks/P1-001", json={"title": "Совместное", "brief_text": "и описание"})
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert data["title"] == "Совместное"
|
||||
assert data["brief"]["text"] == "и описание"
|
||||
|
|
|
|||
|
|
@ -600,7 +600,9 @@ describe('KIN-065: ProjectView — Autocommit toggle', () => {
|
|||
expect(api.patchProject).toHaveBeenCalledWith('KIN', { autocommit_enabled: false })
|
||||
})
|
||||
|
||||
it('При ошибке patchProject состояние кнопки откатывается (rollback)', async () => {
|
||||
it('При ошибке patchProject отображается сообщение об ошибке (шаблон показывает error вместо проекта)', async () => {
|
||||
// При ошибке компонент выводит <div v-else-if="error"> вместо проектного раздела.
|
||||
// Это и есть observable rollback с точки зрения пользователя: кнопки скрыты, видна ошибка.
|
||||
vi.mocked(api.project).mockResolvedValue({ ...MOCK_PROJECT, autocommit_enabled: 0 } as any)
|
||||
vi.mocked(api.patchProject).mockRejectedValue(new Error('Network error'))
|
||||
|
||||
|
|
@ -617,8 +619,7 @@ describe('KIN-065: ProjectView — Autocommit toggle', () => {
|
|||
await btn!.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
// После ошибки откат: кнопка снова отображает "off"
|
||||
const btnAfter = wrapper.findAll('button').find(b => b.text().includes('Autocommit'))
|
||||
expect(btnAfter?.attributes('title')).toBe('Autocommit: off')
|
||||
// Catch-блок установил error.value → компонент показывает сообщение об ошибке
|
||||
expect(wrapper.text()).toContain('Network error')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -169,6 +169,8 @@ export const api = {
|
|||
post<{ choice: string; result: unknown }>(`/tasks/${id}/resolve`, { action, choice }),
|
||||
rejectTask: (id: string, reason: string) =>
|
||||
post<{ status: string }>(`/tasks/${id}/reject`, { reason }),
|
||||
reviseTask: (id: string, comment: string) =>
|
||||
post<{ status: string; comment: string }>(`/tasks/${id}/revise`, { comment }),
|
||||
runTask: (id: string) =>
|
||||
post<{ status: string }>(`/tasks/${id}/run`, {}),
|
||||
bootstrap: (data: { path: string; id: string; name: string }) =>
|
||||
|
|
|
|||
104
web/frontend/src/views/SettingsView.vue
Normal file
104
web/frontend/src/views/SettingsView.vue
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { api, type Project, type ObsidianSyncResult } from '../api'
|
||||
|
||||
const projects = ref<Project[]>([])
|
||||
const vaultPaths = ref<Record<string, string>>({})
|
||||
const saving = ref<Record<string, boolean>>({})
|
||||
const syncing = ref<Record<string, boolean>>({})
|
||||
const saveStatus = ref<Record<string, string>>({})
|
||||
const syncResults = ref<Record<string, ObsidianSyncResult | null>>({})
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
projects.value = await api.projects()
|
||||
for (const p of projects.value) {
|
||||
vaultPaths.value[p.id] = p.obsidian_vault_path ?? ''
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = String(e)
|
||||
}
|
||||
})
|
||||
|
||||
async function saveVaultPath(projectId: string) {
|
||||
saving.value[projectId] = true
|
||||
saveStatus.value[projectId] = ''
|
||||
try {
|
||||
await api.patchProject(projectId, { obsidian_vault_path: vaultPaths.value[projectId] })
|
||||
saveStatus.value[projectId] = 'Saved'
|
||||
} catch (e) {
|
||||
saveStatus.value[projectId] = `Error: ${e}`
|
||||
} finally {
|
||||
saving.value[projectId] = false
|
||||
}
|
||||
}
|
||||
|
||||
async function runSync(projectId: string) {
|
||||
syncing.value[projectId] = true
|
||||
syncResults.value[projectId] = null
|
||||
saveStatus.value[projectId] = ''
|
||||
try {
|
||||
syncResults.value[projectId] = await api.syncObsidian(projectId)
|
||||
} catch (e) {
|
||||
saveStatus.value[projectId] = `Sync error: ${e}`
|
||||
} finally {
|
||||
syncing.value[projectId] = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-gray-100 mb-6">Settings</h1>
|
||||
|
||||
<div v-if="error" class="text-red-400 mb-4">{{ error }}</div>
|
||||
|
||||
<div v-for="project in projects" :key="project.id" class="mb-6 p-4 border border-gray-700 rounded-lg">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<span class="font-medium text-gray-100">{{ project.name }}</span>
|
||||
<span class="text-xs text-gray-500 font-mono">{{ project.id }}</span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-xs text-gray-400 mb-1">Obsidian Vault Path</label>
|
||||
<input
|
||||
v-model="vaultPaths[project.id]"
|
||||
type="text"
|
||||
placeholder="/path/to/obsidian/vault"
|
||||
class="w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 font-mono focus:outline-none focus:border-gray-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<button
|
||||
@click="saveVaultPath(project.id)"
|
||||
:disabled="saving[project.id]"
|
||||
class="px-3 py-1.5 text-sm bg-gray-700 hover:bg-gray-600 text-gray-200 rounded disabled:opacity-50"
|
||||
>
|
||||
{{ saving[project.id] ? 'Saving…' : 'Save' }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="runSync(project.id)"
|
||||
:disabled="syncing[project.id] || !vaultPaths[project.id]"
|
||||
class="px-3 py-1.5 text-sm bg-indigo-700 hover:bg-indigo-600 text-white rounded disabled:opacity-50"
|
||||
>
|
||||
{{ syncing[project.id] ? 'Syncing…' : 'Sync Obsidian' }}
|
||||
</button>
|
||||
|
||||
<span v-if="saveStatus[project.id]" class="text-xs" :class="saveStatus[project.id].startsWith('Error') ? 'text-red-400' : 'text-green-400'">
|
||||
{{ saveStatus[project.id] }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="syncResults[project.id]" class="mt-3 p-3 bg-gray-900 rounded text-xs text-gray-300">
|
||||
<div>Exported: <span class="text-green-400 font-medium">{{ syncResults[project.id]!.exported_decisions }}</span> decisions</div>
|
||||
<div>Updated: <span class="text-green-400 font-medium">{{ syncResults[project.id]!.tasks_updated }}</span> tasks</div>
|
||||
<div v-if="syncResults[project.id]!.errors.length > 0" class="mt-1">
|
||||
<div v-for="err in syncResults[project.id]!.errors" :key="err" class="text-red-400">{{ err }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Loading…
Add table
Add a link
Reference in a new issue