From d65155061a3a6ce829534fb1dc17d3b1bc61146c Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Fri, 1 May 2026 10:40:21 +0800 Subject: [PATCH] feat(web): api client for project/requirement/issue --- web/src/api/projects.ts | 159 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 web/src/api/projects.ts diff --git a/web/src/api/projects.ts b/web/src/api/projects.ts new file mode 100644 index 0000000..63df433 --- /dev/null +++ b/web/src/api/projects.ts @@ -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 { + items: T[] +} + +export interface RequirementDetail { + requirement: Requirement + issues: Issue[] +} + +const enc = (v: string) => encodeURIComponent(v) + +export const projectsApi = { + // ===== Project ===== + list: (includeArchived = false) => + request>(`/api/v1/projects/?archived=${includeArchived}`), + create: (body: { slug: string; name: string; description?: string; visibility?: Visibility }) => + request('/api/v1/projects/', { method: 'POST', body }), + get: (slug: string) => request(`/api/v1/projects/${enc(slug)}`), + update: ( + slug: string, + body: Partial<{ name: string; description: string; visibility: Visibility }>, + ) => request(`/api/v1/projects/${enc(slug)}`, { method: 'PATCH', body }), + archive: (slug: string) => + request(`/api/v1/projects/${enc(slug)}/archive`, { method: 'POST' }), + unarchive: (slug: string) => + request(`/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>( + `/api/v1/projects/${enc(slug)}/requirements${qs ? '?' + qs : ''}`, + ) + }, + createRequirement: (slug: string, body: { title: string; description?: string; owner_id?: string }) => + request(`/api/v1/projects/${enc(slug)}/requirements`, { + method: 'POST', + body, + }), + getRequirement: (slug: string, number: number) => + request(`/api/v1/projects/${enc(slug)}/requirements/${number}`), + updateRequirement: ( + slug: string, + number: number, + body: Partial<{ title: string; description: string; owner_id: string }>, + ) => + request(`/api/v1/projects/${enc(slug)}/requirements/${number}`, { + method: 'PATCH', + body, + }), + changeRequirementPhase: (slug: string, number: number, phase: Phase) => + request(`/api/v1/projects/${enc(slug)}/requirements/${number}/phase`, { + method: 'POST', + body: { phase }, + }), + closeRequirement: (slug: string, number: number) => + request(`/api/v1/projects/${enc(slug)}/requirements/${number}/close`, { method: 'POST' }), + reopenRequirement: (slug: string, number: number) => + request(`/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>( + `/api/v1/projects/${enc(slug)}/issues${qs ? '?' + qs : ''}`, + ) + }, + createIssue: ( + slug: string, + body: { + title: string + description?: string + requirement_number?: number + assignee_id?: string + }, + ) => request(`/api/v1/projects/${enc(slug)}/issues`, { method: 'POST', body }), + getIssue: (slug: string, number: number) => + request(`/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(`/api/v1/projects/${enc(slug)}/issues/${number}`, { + method: 'PATCH', + body, + }), + assignIssue: (slug: string, number: number, assignee_id: string | null) => + request(`/api/v1/projects/${enc(slug)}/issues/${number}/assign`, { + method: 'POST', + body: { assignee_id }, + }), + closeIssue: (slug: string, number: number) => + request(`/api/v1/projects/${enc(slug)}/issues/${number}/close`, { method: 'POST' }), + reopenIssue: (slug: string, number: number) => + request(`/api/v1/projects/${enc(slug)}/issues/${number}/reopen`, { method: 'POST' }), +}