From b6d01686d820a484200103112756446ecfe27a7e Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sun, 14 Jun 2026 10:56:53 -0400 Subject: [PATCH] =?UTF-8?q?feat(issues):=20S4b=20editor=20controls=20?= =?UTF-8?q?=E2=80=94=20Kind=20selector=20+=20Systems=20multi-select?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- frontend/src/views/TaskEditorView.vue | 58 ++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/frontend/src/views/TaskEditorView.vue b/frontend/src/views/TaskEditorView.vue index 6bd2707..dbcab0b 100644 --- a/frontend/src/views/TaskEditorView.vue +++ b/frontend/src/views/TaskEditorView.vue @@ -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(null); const tags = ref([]); const status = ref("todo"); const priority = ref("none"); +const kind = ref("work"); +const systemIds = ref([]); const dueDate = ref(""); const projectId = ref(null); const milestoneId = ref(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(() => + 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; 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); +
+ + +
Started @@ -578,6 +618,16 @@ useEditorGuards(dirty, save);
+
+ +
+ +
+

No systems in this project yet.

+
@@ -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;