You've already forked agentic-coding-workflow
feat(web): api client for project/requirement/issue
This commit is contained in:
@@ -0,0 +1,159 @@
|
|||||||
|
import { request } from './client'
|
||||||
|
|
||||||
|
export type Phase = 'planning' | 'auditing' | 'implementing' | 'reviewing' | 'done'
|
||||||
|
export type Status = 'open' | 'closed'
|
||||||
|
export type Visibility = 'private' | 'internal'
|
||||||
|
|
||||||
|
export interface Project {
|
||||||
|
id: string
|
||||||
|
slug: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
visibility: Visibility
|
||||||
|
owner_id: string
|
||||||
|
archived_at?: string | null
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Requirement {
|
||||||
|
id: string
|
||||||
|
project_id: string
|
||||||
|
number: number
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
phase: Phase
|
||||||
|
status: Status
|
||||||
|
owner_id: string
|
||||||
|
closed_at?: string | null
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Issue {
|
||||||
|
id: string
|
||||||
|
project_id: string
|
||||||
|
requirement_id: string | null
|
||||||
|
number: number
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
status: Status
|
||||||
|
assignee_id: string | null
|
||||||
|
created_by: string
|
||||||
|
closed_at?: string | null
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListWrapper<T> {
|
||||||
|
items: T[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequirementDetail {
|
||||||
|
requirement: Requirement
|
||||||
|
issues: Issue[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const enc = (v: string) => encodeURIComponent(v)
|
||||||
|
|
||||||
|
export const projectsApi = {
|
||||||
|
// ===== Project =====
|
||||||
|
list: (includeArchived = false) =>
|
||||||
|
request<ListWrapper<Project>>(`/api/v1/projects/?archived=${includeArchived}`),
|
||||||
|
create: (body: { slug: string; name: string; description?: string; visibility?: Visibility }) =>
|
||||||
|
request<Project>('/api/v1/projects/', { method: 'POST', body }),
|
||||||
|
get: (slug: string) => request<Project>(`/api/v1/projects/${enc(slug)}`),
|
||||||
|
update: (
|
||||||
|
slug: string,
|
||||||
|
body: Partial<{ name: string; description: string; visibility: Visibility }>,
|
||||||
|
) => request<Project>(`/api/v1/projects/${enc(slug)}`, { method: 'PATCH', body }),
|
||||||
|
archive: (slug: string) =>
|
||||||
|
request<void>(`/api/v1/projects/${enc(slug)}/archive`, { method: 'POST' }),
|
||||||
|
unarchive: (slug: string) =>
|
||||||
|
request<void>(`/api/v1/projects/${enc(slug)}/unarchive`, { method: 'POST' }),
|
||||||
|
|
||||||
|
// ===== Requirement =====
|
||||||
|
listRequirements: (slug: string, params: { phase?: Phase; status?: Status } = {}) => {
|
||||||
|
const q = new URLSearchParams()
|
||||||
|
if (params.phase) q.set('phase', params.phase)
|
||||||
|
if (params.status) q.set('status', params.status)
|
||||||
|
const qs = q.toString()
|
||||||
|
return request<ListWrapper<Requirement>>(
|
||||||
|
`/api/v1/projects/${enc(slug)}/requirements${qs ? '?' + qs : ''}`,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
createRequirement: (slug: string, body: { title: string; description?: string; owner_id?: string }) =>
|
||||||
|
request<Requirement>(`/api/v1/projects/${enc(slug)}/requirements`, {
|
||||||
|
method: 'POST',
|
||||||
|
body,
|
||||||
|
}),
|
||||||
|
getRequirement: (slug: string, number: number) =>
|
||||||
|
request<RequirementDetail>(`/api/v1/projects/${enc(slug)}/requirements/${number}`),
|
||||||
|
updateRequirement: (
|
||||||
|
slug: string,
|
||||||
|
number: number,
|
||||||
|
body: Partial<{ title: string; description: string; owner_id: string }>,
|
||||||
|
) =>
|
||||||
|
request<Requirement>(`/api/v1/projects/${enc(slug)}/requirements/${number}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body,
|
||||||
|
}),
|
||||||
|
changeRequirementPhase: (slug: string, number: number, phase: Phase) =>
|
||||||
|
request<Requirement>(`/api/v1/projects/${enc(slug)}/requirements/${number}/phase`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: { phase },
|
||||||
|
}),
|
||||||
|
closeRequirement: (slug: string, number: number) =>
|
||||||
|
request<void>(`/api/v1/projects/${enc(slug)}/requirements/${number}/close`, { method: 'POST' }),
|
||||||
|
reopenRequirement: (slug: string, number: number) =>
|
||||||
|
request<void>(`/api/v1/projects/${enc(slug)}/requirements/${number}/reopen`, { method: 'POST' }),
|
||||||
|
|
||||||
|
// ===== Issue =====
|
||||||
|
listIssues: (
|
||||||
|
slug: string,
|
||||||
|
params: { requirement?: number | 'none'; status?: Status; assignee?: string } = {},
|
||||||
|
) => {
|
||||||
|
const q = new URLSearchParams()
|
||||||
|
if (params.requirement !== undefined) q.set('requirement', String(params.requirement))
|
||||||
|
if (params.status) q.set('status', params.status)
|
||||||
|
if (params.assignee) q.set('assignee', params.assignee)
|
||||||
|
const qs = q.toString()
|
||||||
|
return request<ListWrapper<Issue>>(
|
||||||
|
`/api/v1/projects/${enc(slug)}/issues${qs ? '?' + qs : ''}`,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
createIssue: (
|
||||||
|
slug: string,
|
||||||
|
body: {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
requirement_number?: number
|
||||||
|
assignee_id?: string
|
||||||
|
},
|
||||||
|
) => request<Issue>(`/api/v1/projects/${enc(slug)}/issues`, { method: 'POST', body }),
|
||||||
|
getIssue: (slug: string, number: number) =>
|
||||||
|
request<Issue>(`/api/v1/projects/${enc(slug)}/issues/${number}`),
|
||||||
|
updateIssue: (
|
||||||
|
slug: string,
|
||||||
|
number: number,
|
||||||
|
body: Partial<{
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
// 三态:未传字段 / 显式 null(detach) / number(attach/move)
|
||||||
|
requirement_number: number | null
|
||||||
|
}>,
|
||||||
|
) =>
|
||||||
|
request<Issue>(`/api/v1/projects/${enc(slug)}/issues/${number}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body,
|
||||||
|
}),
|
||||||
|
assignIssue: (slug: string, number: number, assignee_id: string | null) =>
|
||||||
|
request<Issue>(`/api/v1/projects/${enc(slug)}/issues/${number}/assign`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: { assignee_id },
|
||||||
|
}),
|
||||||
|
closeIssue: (slug: string, number: number) =>
|
||||||
|
request<void>(`/api/v1/projects/${enc(slug)}/issues/${number}/close`, { method: 'POST' }),
|
||||||
|
reopenIssue: (slug: string, number: number) =>
|
||||||
|
request<void>(`/api/v1/projects/${enc(slug)}/issues/${number}/reopen`, { method: 'POST' }),
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user