You've already forked agentic-coding-workflow
feat(web): workspace selector on issue/requirement detail
This commit is contained in:
+11
-1
@@ -25,6 +25,7 @@ export interface Requirement {
|
|||||||
phase: Phase
|
phase: Phase
|
||||||
status: Status
|
status: Status
|
||||||
owner_id: string
|
owner_id: string
|
||||||
|
workspace_id: string | null
|
||||||
closed_at?: string | null
|
closed_at?: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
@@ -34,6 +35,7 @@ export interface Issue {
|
|||||||
id: string
|
id: string
|
||||||
project_id: string
|
project_id: string
|
||||||
requirement_id: string | null
|
requirement_id: string | null
|
||||||
|
workspace_id: string | null
|
||||||
number: number
|
number: number
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
@@ -92,7 +94,13 @@ export const projectsApi = {
|
|||||||
updateRequirement: (
|
updateRequirement: (
|
||||||
slug: string,
|
slug: string,
|
||||||
number: number,
|
number: number,
|
||||||
body: Partial<{ title: string; description: string; owner_id: string }>,
|
body: Partial<{
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
owner_id: string
|
||||||
|
// 三态:未传字段 / 显式 null(detach) / "<uuid>"(attach)
|
||||||
|
workspace_id: string | null
|
||||||
|
}>,
|
||||||
) =>
|
) =>
|
||||||
request<Requirement>(`/api/v1/projects/${enc(slug)}/requirements/${number}`, {
|
request<Requirement>(`/api/v1/projects/${enc(slug)}/requirements/${number}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
@@ -141,6 +149,8 @@ export const projectsApi = {
|
|||||||
description: string
|
description: string
|
||||||
// 三态:未传字段 / 显式 null(detach) / number(attach/move)
|
// 三态:未传字段 / 显式 null(detach) / number(attach/move)
|
||||||
requirement_number: number | null
|
requirement_number: number | null
|
||||||
|
// 三态:未传字段 / 显式 null(detach) / "<uuid>"(attach)
|
||||||
|
workspace_id: string | null
|
||||||
}>,
|
}>,
|
||||||
) =>
|
) =>
|
||||||
request<Issue>(`/api/v1/projects/${enc(slug)}/issues/${number}`, {
|
request<Issue>(`/api/v1/projects/${enc(slug)}/issues/${number}`, {
|
||||||
|
|||||||
@@ -58,6 +58,17 @@
|
|||||||
/>
|
/>
|
||||||
</NCard>
|
</NCard>
|
||||||
|
|
||||||
|
<!-- Workspace card -->
|
||||||
|
<NCard title="工作区">
|
||||||
|
<NSelect
|
||||||
|
:value="issue.workspace_id"
|
||||||
|
:options="workspaceOptions"
|
||||||
|
placeholder="未关联"
|
||||||
|
clearable
|
||||||
|
@update:value="onWorkspaceChange"
|
||||||
|
/>
|
||||||
|
</NCard>
|
||||||
|
|
||||||
<!-- Assignee card -->
|
<!-- Assignee card -->
|
||||||
<NCard title="指派">
|
<NCard title="指派">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
@@ -118,11 +129,13 @@ import { NCard, NButton, NTag, NSpin, NSelect } from 'naive-ui'
|
|||||||
import AppShell from '@/layouts/AppShell.vue'
|
import AppShell from '@/layouts/AppShell.vue'
|
||||||
import { projectsApi } from '@/api/projects'
|
import { projectsApi } from '@/api/projects'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { useWorkspacesStore } from '@/stores/workspaces'
|
||||||
import type { Issue, Requirement } from '@/api/projects'
|
import type { Issue, Requirement } from '@/api/projects'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
const wsStore = useWorkspacesStore()
|
||||||
|
|
||||||
const slug = route.params.slug as string
|
const slug = route.params.slug as string
|
||||||
const number = Number(route.params.number)
|
const number = Number(route.params.number)
|
||||||
@@ -138,6 +151,10 @@ const requirementOptions = computed(() =>
|
|||||||
requirements.value.map((r) => ({ label: `R-${r.number}: ${r.title}`, value: r.number })),
|
requirements.value.map((r) => ({ label: `R-${r.number}: ${r.title}`, value: r.number })),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const workspaceOptions = computed(() =>
|
||||||
|
wsStore.list.map((w) => ({ label: `${w.name} (${w.slug})`, value: w.id })),
|
||||||
|
)
|
||||||
|
|
||||||
const mountedRequirementNumber = computed<number | null>(() => {
|
const mountedRequirementNumber = computed<number | null>(() => {
|
||||||
if (!issue.value?.requirement_id) return null
|
if (!issue.value?.requirement_id) return null
|
||||||
const req = requirements.value.find((r) => r.id === issue.value!.requirement_id)
|
const req = requirements.value.find((r) => r.id === issue.value!.requirement_id)
|
||||||
@@ -150,6 +167,7 @@ async function fetchData() {
|
|||||||
const [fetchedIssue, reqs] = await Promise.all([
|
const [fetchedIssue, reqs] = await Promise.all([
|
||||||
projectsApi.getIssue(slug, number),
|
projectsApi.getIssue(slug, number),
|
||||||
projectsApi.listRequirements(slug),
|
projectsApi.listRequirements(slug),
|
||||||
|
wsStore.fetchByProject(slug),
|
||||||
])
|
])
|
||||||
issue.value = fetchedIssue
|
issue.value = fetchedIssue
|
||||||
requirements.value = reqs.items
|
requirements.value = reqs.items
|
||||||
@@ -163,6 +181,11 @@ async function onMountChange(v: number | null) {
|
|||||||
issue.value = await projectsApi.updateIssue(slug, number, { requirement_number: v })
|
issue.value = await projectsApi.updateIssue(slug, number, { requirement_number: v })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onWorkspaceChange(v: string | null) {
|
||||||
|
if (!issue.value) return
|
||||||
|
issue.value = await projectsApi.updateIssue(slug, number, { workspace_id: v })
|
||||||
|
}
|
||||||
|
|
||||||
async function onClose() {
|
async function onClose() {
|
||||||
toggling.value = true
|
toggling.value = true
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -85,6 +85,17 @@
|
|||||||
</p>
|
</p>
|
||||||
</NCard>
|
</NCard>
|
||||||
|
|
||||||
|
<!-- Workspace card -->
|
||||||
|
<NCard title="工作区">
|
||||||
|
<NSelect
|
||||||
|
:value="data.requirement.workspace_id"
|
||||||
|
:options="workspaceOptions"
|
||||||
|
placeholder="未关联"
|
||||||
|
clearable
|
||||||
|
@update:value="onWorkspaceChange"
|
||||||
|
/>
|
||||||
|
</NCard>
|
||||||
|
|
||||||
<!-- Linked Issues -->
|
<!-- Linked Issues -->
|
||||||
<NCard title="关联 Issue">
|
<NCard title="关联 Issue">
|
||||||
<NList
|
<NList
|
||||||
@@ -129,7 +140,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useDialog } from 'naive-ui'
|
import { useDialog } from 'naive-ui'
|
||||||
import {
|
import {
|
||||||
@@ -137,6 +148,7 @@ import {
|
|||||||
NButton,
|
NButton,
|
||||||
NTag,
|
NTag,
|
||||||
NSpin,
|
NSpin,
|
||||||
|
NSelect,
|
||||||
NList,
|
NList,
|
||||||
NListItem,
|
NListItem,
|
||||||
NThing,
|
NThing,
|
||||||
@@ -145,6 +157,7 @@ import {
|
|||||||
import AppShell from '@/layouts/AppShell.vue'
|
import AppShell from '@/layouts/AppShell.vue'
|
||||||
import { projectsApi } from '@/api/projects'
|
import { projectsApi } from '@/api/projects'
|
||||||
import { useRequirementsStore } from '@/stores/requirements'
|
import { useRequirementsStore } from '@/stores/requirements'
|
||||||
|
import { useWorkspacesStore } from '@/stores/workspaces'
|
||||||
import type { Phase, RequirementDetail } from '@/api/projects'
|
import type { Phase, RequirementDetail } from '@/api/projects'
|
||||||
|
|
||||||
const PHASES: Phase[] = ['planning', 'auditing', 'implementing', 'reviewing', 'done']
|
const PHASES: Phase[] = ['planning', 'auditing', 'implementing', 'reviewing', 'done']
|
||||||
@@ -161,6 +174,7 @@ const route = useRoute()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const store = useRequirementsStore()
|
const store = useRequirementsStore()
|
||||||
|
const wsStore = useWorkspacesStore()
|
||||||
|
|
||||||
const slug = route.params.slug as string
|
const slug = route.params.slug as string
|
||||||
const number = Number(route.params.number)
|
const number = Number(route.params.number)
|
||||||
@@ -169,15 +183,30 @@ const loading = ref(false)
|
|||||||
const toggling = ref(false)
|
const toggling = ref(false)
|
||||||
const data = ref<RequirementDetail | null>(null)
|
const data = ref<RequirementDetail | null>(null)
|
||||||
|
|
||||||
|
const workspaceOptions = computed(() =>
|
||||||
|
wsStore.list.map((w) => ({ label: `${w.name} (${w.slug})`, value: w.id })),
|
||||||
|
)
|
||||||
|
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
data.value = await projectsApi.getRequirement(slug, number)
|
const [detail] = await Promise.all([
|
||||||
|
projectsApi.getRequirement(slug, number),
|
||||||
|
wsStore.fetchByProject(slug),
|
||||||
|
])
|
||||||
|
data.value = detail
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onWorkspaceChange(v: string | null) {
|
||||||
|
if (!data.value) return
|
||||||
|
await projectsApi.updateRequirement(slug, number, { workspace_id: v })
|
||||||
|
// Re-fetch to keep data.requirement and data.issues consistent (mirrors onPhaseClick)
|
||||||
|
data.value = await projectsApi.getRequirement(slug, number)
|
||||||
|
}
|
||||||
|
|
||||||
function onPhaseClick(target: Phase) {
|
function onPhaseClick(target: Phase) {
|
||||||
if (!data.value || data.value.requirement.phase === target) return
|
if (!data.value || data.value.requirement.phase === target) return
|
||||||
const targetLabel = PHASE_LABELS[target]
|
const targetLabel = PHASE_LABELS[target]
|
||||||
|
|||||||
Reference in New Issue
Block a user