feat(issues): S4b editor controls — Kind selector + Systems multi-select
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 49s
CI & Build / Build & push image (push) Successful in 36s

In the full task editor (TaskEditorView) sidebar:
- Kind selector (Work / Plan / Issue), mirroring the Status/Priority selects.
- Systems multi-select (checkboxes of the project's systems, fetched via the
  systems store), shown when a project is set.
Both wired through load (prefill from task.task_kind / task.systems), dirty
tracking, and save (kind + system_ids via the store's IssueFields). No new
colors — existing sb-field/sb-select tokens.

Deferred: the arose-from (provenance) picker — least-critical control and the
riskiest (task-search UI); the field is already supported by API/store/route for
a later add. NEEDS operator browser verification (CI typechecks only).

Refs plan 825 (S4b editor).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 10:56:53 -04:00
parent 94d32c524a
commit b6d01686d8
+57 -1
View File
@@ -12,6 +12,9 @@ import { useTagSuggestions } from "@/composables/useTagSuggestions";
import { useFloatingAssist } from "@/composables/useFloatingAssist";
import { apiPost, apiGet, apiPatch } from "@/api/client";
import type { TaskStatus, TaskPriority } from "@/types/task";
import type { TaskKind } from "@/types/note";
import { useSystemsStore } from "@/stores/systems";
import type { System } from "@/api/systems";
import type { Note } from "@/types/note";
import type { Editor } from "@tiptap/vue-3";
import MarkdownToolbar from "@/components/MarkdownToolbar.vue";
@@ -31,6 +34,7 @@ import { Trash2 } from "lucide-vue-next";
const route = useRoute();
const router = useRouter();
const store = useTasksStore();
const systemsStore = useSystemsStore();
const notesStore = useNotesStore();
const toast = useToastStore();
@@ -41,6 +45,8 @@ const consolidatedAt = ref<string | null>(null);
const tags = ref<string[]>([]);
const status = ref<TaskStatus>("todo");
const priority = ref<TaskPriority>("none");
const kind = ref<TaskKind>("work");
const systemIds = ref<number[]>([]);
const dueDate = ref("");
const projectId = ref<number | null>(null);
const milestoneId = ref<number | null>(null);
@@ -201,6 +207,8 @@ let savedDueDate = "";
let savedProjectId: number | null = null;
let savedMilestoneId: number | null = null;
let savedParentId: number | null = null;
let savedKind: TaskKind = "work";
let savedSystemIds: number[] = [];
function markDirty() {
dirty.value =
@@ -213,7 +221,22 @@ function markDirty() {
dueDate.value !== savedDueDate ||
projectId.value !== savedProjectId ||
milestoneId.value !== savedMilestoneId ||
parentId.value !== savedParentId;
parentId.value !== savedParentId ||
kind.value !== savedKind ||
JSON.stringify(systemIds.value) !== JSON.stringify(savedSystemIds);
}
const projectSystems = computed<System[]>(() =>
projectId.value ? (systemsStore.systemsByProject[projectId.value] ?? []) : [],
);
async function loadSystems() {
if (!projectId.value) return;
try {
await systemsStore.fetchSystems(projectId.value);
} catch {
/* non-fatal — the systems picker just won't populate */
}
}
function onBodyUpdate(newVal: string) {
@@ -288,6 +311,8 @@ onMounted(async () => {
const taskRec = store.currentTask as Record<string, unknown>;
projectId.value = (taskRec.project_id as number | null) ?? null;
milestoneId.value = (taskRec.milestone_id as number | null) ?? null;
kind.value = (taskRec.task_kind as TaskKind) ?? "work";
systemIds.value = ((taskRec.systems as Array<{ id: number }> | undefined) ?? []).map((s) => s.id);
parentId.value = (taskRec.parent_id as number | null) ?? null;
parentTitle.value = (taskRec.parent_title as string | null) ?? "";
parentSearchQuery.value = parentTitle.value;
@@ -305,6 +330,8 @@ onMounted(async () => {
savedProjectId = projectId.value;
savedMilestoneId = milestoneId.value;
savedParentId = parentId.value;
savedKind = kind.value;
savedSystemIds = [...systemIds.value];
// Start in preview mode only if the task already has body content
showPreview.value = body.value.trim().length > 0;
}
@@ -315,6 +342,7 @@ onMounted(async () => {
if (route.query.milestoneId) milestoneId.value = Number(route.query.milestoneId);
if (route.query.parentId) parentId.value = Number(route.query.parentId);
}
loadSystems();
});
async function save() {
@@ -333,6 +361,8 @@ async function save() {
milestone_id: milestoneId.value,
parent_id: parentId.value,
recurrence_rule: recurrenceRule.value,
kind: kind.value,
system_ids: systemIds.value,
};
if (isEditing.value) {
await store.updateTask(taskId.value!, data);
@@ -346,6 +376,8 @@ async function save() {
savedProjectId = projectId.value;
savedMilestoneId = milestoneId.value;
savedParentId = parentId.value;
savedKind = kind.value;
savedSystemIds = [...systemIds.value];
dirty.value = false;
toast.show("Task saved");
router.push(`/tasks/${taskId.value}`);
@@ -543,6 +575,14 @@ useEditorGuards(dirty, save);
<option value="cancelled">Cancelled</option>
</select>
</div>
<div class="sb-field">
<label class="sb-label">Kind</label>
<select v-model="kind" @change="markDirty" class="sb-select">
<option value="work">Work</option>
<option value="plan">Plan</option>
<option value="issue">Issue</option>
</select>
</div>
<div v-if="startedAt || completedAt" class="sb-timestamps">
<div v-if="startedAt" class="sb-timestamp">
<span class="sb-ts-label">Started</span>
@@ -578,6 +618,16 @@ useEditorGuards(dirty, save);
<label class="sb-label">Milestone</label>
<MilestoneSelector :projectId="projectId" v-model="milestoneId" @update:modelValue="markDirty" />
</div>
<div v-if="projectId" class="sb-field">
<label class="sb-label">Systems</label>
<div v-if="projectSystems.length" class="sb-systems">
<label v-for="s in projectSystems" :key="s.id" class="sb-system-opt">
<input type="checkbox" :value="s.id" v-model="systemIds" @change="markDirty" />
<span>{{ s.name }}</span>
</label>
</div>
<p v-else class="sb-systems-empty">No systems in this project yet.</p>
</div>
<!-- Parent task -->
<div class="sb-field">
@@ -955,6 +1005,12 @@ useEditorGuards(dirty, save);
min-height: 0;
}
/* Systems multi-select (in sidebar) */
.sb-systems { display: flex; flex-direction: column; gap: 0.25rem; max-height: 160px; overflow-y: auto; }
.sb-system-opt { display: flex; align-items: center; gap: 0.45rem; font-size: 0.85rem; color: var(--color-text); cursor: pointer; }
.sb-system-opt input { accent-color: var(--color-primary); cursor: pointer; }
.sb-systems-empty { margin: 0; font-size: 0.8rem; color: var(--color-text-muted); }
/* Writing Assistant section (in sidebar) */
.assist-section {
display: flex;