feat(frontend): description field in task editor + Goal block in viewer
Note type gains description and consolidated_at fields. TaskEditorView adds a Goal textarea above the body editor (wired through dirty/save/ autosave paths). TaskViewerView renders Goal as a subordinate block above the body, plus a subtle 'Auto-summarized from work logs' banner when consolidated_at is set. Also adds a consolidateTask client function for the upcoming re-consolidate button (Task 11).
This commit is contained in:
@@ -705,3 +705,14 @@ export interface ProfileObservationEntry {
|
||||
|
||||
export const listProfileObservations = () =>
|
||||
apiGet<{ observations: ProfileObservationEntry[] }>('/api/profile/observations')
|
||||
|
||||
|
||||
// ── Tasks ────────────────────────────────────────────────────────────────────
|
||||
|
||||
import type { Note as Task } from '../types/note'
|
||||
|
||||
/** Manually trigger a consolidation pass for a task. Returns the freshly-
|
||||
* updated task with new body + consolidated_at. Bypasses the user's
|
||||
* auto_consolidate_tasks setting. */
|
||||
export const consolidateTask = (id: number) =>
|
||||
apiPost<Task>(`/api/tasks/${id}/consolidate`, {})
|
||||
|
||||
@@ -6,6 +6,8 @@ export interface Note {
|
||||
id: number;
|
||||
title: string;
|
||||
body: string;
|
||||
description: string | null;
|
||||
consolidated_at: string | null;
|
||||
tags: string[];
|
||||
parent_id: number | null;
|
||||
parent_title?: string | null;
|
||||
|
||||
@@ -35,6 +35,8 @@ const toast = useToastStore();
|
||||
|
||||
const title = ref("");
|
||||
const body = ref("");
|
||||
const description = ref("");
|
||||
const consolidatedAt = ref<string | null>(null);
|
||||
const tags = ref<string[]>([]);
|
||||
const status = ref<TaskStatus>("todo");
|
||||
const priority = ref<TaskPriority>("none");
|
||||
@@ -186,6 +188,7 @@ const { suggestedTags, appliedTags, suggestingTags, fetchTagSuggestions, applyTa
|
||||
|
||||
let savedTitle = "";
|
||||
let savedBody = "";
|
||||
let savedDescription = "";
|
||||
let savedTags: string[] = [];
|
||||
let savedStatus: TaskStatus = "todo";
|
||||
let savedPriority: TaskPriority = "none";
|
||||
@@ -198,6 +201,7 @@ function markDirty() {
|
||||
dirty.value =
|
||||
title.value !== savedTitle ||
|
||||
body.value !== savedBody ||
|
||||
description.value !== savedDescription ||
|
||||
JSON.stringify(tags.value) !== JSON.stringify(savedTags) ||
|
||||
status.value !== savedStatus ||
|
||||
priority.value !== savedPriority ||
|
||||
@@ -270,6 +274,8 @@ onMounted(async () => {
|
||||
if (store.currentTask) {
|
||||
title.value = store.currentTask.title;
|
||||
body.value = store.currentTask.body;
|
||||
description.value = store.currentTask.description ?? "";
|
||||
consolidatedAt.value = store.currentTask.consolidated_at ?? null;
|
||||
tags.value = [...(store.currentTask.tags || [])];
|
||||
status.value = store.currentTask.status as TaskStatus;
|
||||
priority.value = store.currentTask.priority as TaskPriority;
|
||||
@@ -286,6 +292,7 @@ onMounted(async () => {
|
||||
recurrenceRule.value = noteTask.recurrence_rule ?? null;
|
||||
savedTitle = title.value;
|
||||
savedBody = body.value;
|
||||
savedDescription = description.value;
|
||||
savedTags = [...tags.value];
|
||||
savedStatus = status.value;
|
||||
savedPriority = priority.value;
|
||||
@@ -312,6 +319,7 @@ async function save() {
|
||||
const data = {
|
||||
title: title.value,
|
||||
body: body.value,
|
||||
description: description.value,
|
||||
tags: tags.value,
|
||||
status: status.value,
|
||||
priority: priority.value,
|
||||
@@ -325,6 +333,7 @@ async function save() {
|
||||
await store.updateTask(taskId.value!, data);
|
||||
savedTitle = title.value;
|
||||
savedBody = body.value;
|
||||
savedDescription = description.value;
|
||||
savedTags = [...tags.value];
|
||||
savedStatus = status.value;
|
||||
savedPriority = priority.value;
|
||||
@@ -377,6 +386,7 @@ async function doAutoSave() {
|
||||
await store.updateTask(taskId.value!, {
|
||||
title: title.value,
|
||||
body: body.value,
|
||||
description: description.value,
|
||||
tags: tags.value,
|
||||
status: status.value,
|
||||
priority: priority.value,
|
||||
@@ -388,6 +398,7 @@ async function doAutoSave() {
|
||||
} as Record<string, unknown>);
|
||||
savedTitle = title.value;
|
||||
savedBody = body.value;
|
||||
savedDescription = description.value;
|
||||
savedTags = [...tags.value];
|
||||
savedStatus = status.value;
|
||||
savedPriority = priority.value;
|
||||
@@ -431,7 +442,19 @@ useEditorGuards(dirty, save);
|
||||
@keydown.ctrl.e.prevent="tiptapEditor?.commands.focus()"
|
||||
/>
|
||||
|
||||
</div><!-- /editor-header: title only -->
|
||||
<div class="task-goal">
|
||||
<label for="task-description" class="task-goal-label">Goal</label>
|
||||
<textarea
|
||||
id="task-description"
|
||||
v-model="description"
|
||||
placeholder="What are we trying to do here? (read-only context for the auto-summary)"
|
||||
rows="2"
|
||||
class="task-goal-input"
|
||||
@input="markDirty"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
</div><!-- /editor-header: title + goal -->
|
||||
|
||||
<!-- Two-column body: main (editor+log) | sidebar (metadata) -->
|
||||
<div class="task-body">
|
||||
@@ -971,4 +994,38 @@ useEditorGuards(dirty, save);
|
||||
overflow-y: visible;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Goal (description) input ─────────────────────────────────────────────── */
|
||||
.task-goal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
margin: 0.5rem 0 0.25rem;
|
||||
}
|
||||
.task-goal-label {
|
||||
font-family: var(--font-display, "Fraunces", serif);
|
||||
font-style: italic;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-muted, rgba(255, 255, 255, 0.5));
|
||||
}
|
||||
.task-goal-input {
|
||||
width: 100%;
|
||||
resize: vertical;
|
||||
min-height: 2.4rem;
|
||||
padding: 0.5rem 0.6rem;
|
||||
font: inherit;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.4;
|
||||
color: var(--color-text, inherit);
|
||||
background: var(--color-input-bg, rgba(255, 255, 255, 0.03));
|
||||
border: 1px solid var(--color-border, rgba(255, 255, 255, 0.08));
|
||||
border-radius: var(--radius-md, 8px);
|
||||
}
|
||||
.task-goal-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary, #6366f1);
|
||||
}
|
||||
</style>
|
||||
@@ -357,6 +357,22 @@ const subTaskProgress = computed(() => {
|
||||
@click="onTagClick"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="store.currentTask.description"
|
||||
class="task-goal-display"
|
||||
>
|
||||
<h3 class="goal-label">Goal</h3>
|
||||
<p class="goal-text">{{ store.currentTask.description }}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="store.currentTask.consolidated_at"
|
||||
class="auto-summary-banner"
|
||||
>
|
||||
<span class="auto-summary-icon" aria-hidden="true">✦</span>
|
||||
Auto-summarized from work logs.
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="body prose"
|
||||
v-html="renderedBody"
|
||||
@@ -807,4 +823,42 @@ const subTaskProgress = computed(() => {
|
||||
.skel-line { height: 0.9rem; }
|
||||
.skel-line--short { width: 50%; }
|
||||
.skel-line--medium { width: 78%; }
|
||||
|
||||
/* ── Goal block + auto-summary banner ─────────────────────────────────────── */
|
||||
.task-goal-display {
|
||||
border-left: 2px solid var(--color-border, rgba(255, 255, 255, 0.12));
|
||||
padding: 0.4rem 0 0.4rem 0.9rem;
|
||||
margin: 0.75rem 0 1.25rem;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
.goal-label {
|
||||
font-family: var(--font-display, "Fraunces", serif);
|
||||
font-style: italic;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-muted, rgba(255, 255, 255, 0.5));
|
||||
margin: 0 0 0.25rem;
|
||||
}
|
||||
.goal-text {
|
||||
margin: 0;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.45;
|
||||
color: var(--color-text, inherit);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.auto-summary-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.78rem;
|
||||
font-style: italic;
|
||||
color: var(--color-text-muted, rgba(255, 255, 255, 0.55));
|
||||
margin: 0 0 0.75rem;
|
||||
}
|
||||
.auto-summary-icon {
|
||||
color: var(--color-primary, #6366f1);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user