/** * Issue-board transport: every call the 问题集 pages make, wrapped in the * shared envelope contract so the UI only ever sees unwrapped rows or an * `ApiFailure`. * * The generated client types paged endpoints against the generic `Page` * schema (one shape for every list), so the row type is asserted here — the * single place in the app allowed to do that, which keeps the casts out of the * components and makes a schema drift show up as one localized error. * * Issues are addressable by uuid *or* board-scoped number (`?issue=42`), so * every `issueId` parameter below accepts either form. */ import Apis from '@/shared/api' import { unwrapApi, type ApiEnvelope } from '@/shared/lib' import type { Board, CommentNode, IssueComment, IssueDetail, IssueEvent, IssueLabel, IssuePage, IssuePatchCmd, IssueQuery, IssueState, Milestone, NewIssueCmd, SpaceMember, WatchMode, } from './issue' import { SORT_PARAMS } from './meta' /** * Empty strings are dropped, not sent: Alova omits `undefined` query values but * forwards `''`, and the backend's `Option` / `Option` * deserialization rejects an empty `milestone_id=` / `state=` with a 400. */ const nz = (value: T | '' | null | undefined): T | undefined => (value ? value : undefined) /** Board id path params repeated by almost every issue endpoint. */ export interface BoardRef { space: string board: string } const pageOf = (method: PromiseLike>): Promise> => unwrapApi | null>(method as PromiseLike | null>>).then(p => ({ items: p?.items ?? [], total: p?.total ?? 0, limit: p?.limit ?? 0, offset: p?.offset ?? 0, })) const listOf = (method: PromiseLike>): Promise => unwrapApi(method as PromiseLike>).then(rows => rows ?? []) const oneOf = (method: PromiseLike>): Promise => unwrapApi(method as PromiseLike>) /* ------------------------------------------------------------------ board */ export const getBoard = ({ space, board }: BoardRef) => unwrapApi(Apis.issues.get_board({ pathParams: { space_name: space, board_id: board } })) export const updateBoard = ({ space, board }: BoardRef, cmd: { name?: string; description?: string | null; icon?: string | null }) => oneOf( Apis.issues.update_board({ pathParams: { space_name: space, board_id: board }, data: cmd }), ) export const deleteBoard = ({ space, board }: BoardRef) => unwrapApi( Apis.issues.delete_board({ pathParams: { space_name: space, board_id: board } }), ) /* ------------------------------------------------------------------ issue */ /** One page of issues with the board's filters applied server-side. */ export const listIssues = ({ space, board }: BoardRef, query: IssueQuery = {}) => pageOf( Apis.issues.list_issues({ pathParams: { space_name: space, board_id: board }, params: { // `all` is a UI-only choice: the backend filters "open" or "closed", and // omitting the key at all is what "no state filter" means. state: query.state && query.state !== 'all' ? (query.state as IssueState) : undefined, q: nz(query.q), priority: nz(query.priority), label_id: nz(query.labelId), milestone_id: nz(query.milestoneId), assignee_id: nz(query.assigneeId), author_id: nz(query.authorId), sort: query.sort ? SORT_PARAMS[query.sort] : undefined, limit: query.limit, offset: query.offset ?? 0, }, }), ) export const getIssue = ({ space, board }: BoardRef, issueId: string) => unwrapApi( Apis.issues.get_issue({ pathParams: { space_name: space, board_id: board, issue_id: issueId } }), ) export const createIssue = ({ space, board }: BoardRef, cmd: NewIssueCmd) => oneOf( Apis.issues.create_issue({ pathParams: { space_name: space, board_id: board }, data: { title: cmd.title, description: cmd.description ?? null, content: cmd.content ?? '', priority: cmd.priority ?? 'none', due_at: cmd.due_at ?? null, milestone_id: cmd.milestone_id ?? null, assignee_ids: cmd.assignee_ids ?? [], label_ids: cmd.label_ids ?? [], }, }), ) export const updateIssue = ({ space, board }: BoardRef, issueId: string, cmd: IssuePatchCmd) => oneOf( Apis.issues.update_issue({ pathParams: { space_name: space, board_id: board, issue_id: issueId }, data: cmd, }), ) export const closeIssue = ({ space, board }: BoardRef, issueId: string) => oneOf( Apis.issues.close_issue({ pathParams: { space_name: space, board_id: board, issue_id: issueId } }), ) export const reopenIssue = ({ space, board }: BoardRef, issueId: string) => oneOf( Apis.issues.reopen_issue({ pathParams: { space_name: space, board_id: board, issue_id: issueId } }), ) export const deleteIssue = ({ space, board }: BoardRef, issueId: string) => unwrapApi( Apis.issues.delete_issue({ pathParams: { space_name: space, board_id: board, issue_id: issueId } }), ) export const setIssueWatch = ({ space, board }: BoardRef, issueId: string, mode: WatchMode) => unwrapApi( Apis.issues.upsert_issue_watch({ pathParams: { space_name: space, board_id: board, issue_id: issueId }, data: { mode }, }), ).catch(() => null) /* ---------------------------------------------------------------- comment */ export const listCommentTree = ({ space, board }: BoardRef, issueId: string) => listOf( Apis.issues.list_comments_tree({ pathParams: { space_name: space, board_id: board, issue_id: issueId }, }), ) export const createComment = ({ space, board }: BoardRef, issueId: string, content: string, parentId?: string) => oneOf( Apis.issues.create_comment({ pathParams: { space_name: space, board_id: board, issue_id: issueId }, data: { content, parent_id: parentId ?? null }, }), ) export const updateComment = ( ref: BoardRef, issueId: string, commentId: string, content: string, ) => oneOf( Apis.issues.update_comment({ pathParams: { space_name: ref.space, board_id: ref.board, issue_id: issueId, comment_id: commentId, }, data: { content }, }), ) export const deleteComment = (ref: BoardRef, issueId: string, commentId: string) => unwrapApi( Apis.issues.delete_comment({ pathParams: { space_name: ref.space, board_id: ref.board, issue_id: issueId, comment_id: commentId, }, }), ) /* ------------------------------------------------------------------ meta */ export const listEvents = ({ space, board }: BoardRef, issueId: string, limit = 50) => pageOf( Apis.issues.list_events({ pathParams: { space_name: space, board_id: board, issue_id: issueId }, params: { limit }, }), ) export const listLabels = ({ space, board }: BoardRef) => pageOf( Apis.issues.list_labels({ pathParams: { space_name: space, board_id: board }, params: { limit: 100, is_archived: false }, }), ) export const createLabel = ({ space, board }: BoardRef, cmd: { name: string; color: string; description?: string | null }) => oneOf( Apis.issues.create_label({ pathParams: { space_name: space, board_id: board }, data: { name: cmd.name, color: cmd.color, description: cmd.description ?? null }, }), ) export const updateLabel = ( { space, board }: BoardRef, labelId: string, cmd: { name?: string; color?: string; description?: string | null; is_archived?: boolean }, ) => oneOf( Apis.issues.update_label({ pathParams: { space_name: space, board_id: board, label_id: labelId }, data: cmd, }), ) export const deleteLabel = ({ space, board }: BoardRef, labelId: string) => unwrapApi( Apis.issues.delete_label({ pathParams: { space_name: space, board_id: board, label_id: labelId } }), ) /** Labels currently bound to one issue (`GET …/issues/{id}/labels`). */ export const listIssueLabels = ({ space, board }: BoardRef, issueId: string) => listOf( Apis.issues.list_labels_for_issue({ pathParams: { space_name: space, board_id: board, issue_id: issueId }, }), ) export const bindLabel = ({ space, board }: BoardRef, issueId: string, labelId: string) => unwrapApi( Apis.issues.bind_label({ pathParams: { space_name: space, board_id: board, issue_id: issueId, label_id: labelId }, }), ) export const unbindLabel = ({ space, board }: BoardRef, issueId: string, labelId: string) => unwrapApi( Apis.issues.unbind_label({ pathParams: { space_name: space, board_id: board, issue_id: issueId, label_id: labelId }, }), ) export const listMilestones = ({ space, board }: BoardRef, includeClosed = true) => pageOf( Apis.issues.list_milestones({ pathParams: { space_name: space, board_id: board }, params: { limit: 100, ...(includeClosed ? {} : { is_closed: false }) }, }), ) export const createMilestone = ({ space, board }: BoardRef, cmd: { name: string; description?: string | null; due_at?: string | null }) => oneOf( Apis.issues.create_milestone({ pathParams: { space_name: space, board_id: board }, data: { name: cmd.name, description: cmd.description ?? null, due_at: cmd.due_at ?? null }, }), ) export const updateMilestone = ( { space, board }: BoardRef, milestoneId: string, cmd: { name?: string; description?: string | null; due_at?: string | null; is_closed?: boolean }, ) => oneOf( Apis.issues.update_milestone({ pathParams: { space_name: space, board_id: board, milestone_id: milestoneId }, data: cmd, }), ) export const deleteMilestone = ({ space, board }: BoardRef, milestoneId: string) => unwrapApi( Apis.issues.delete_milestone({ pathParams: { space_name: space, board_id: board, milestone_id: milestoneId }, }), ) /* ---------------------------------------------------------------- members */ /** Assignable people: the space's members (issue assignees must be members). */ export const listSpaceMembers = (space: string) => pageOf( Apis.space.list_space_members({ pathParams: { space_name: space }, params: { limit: 100 } }), )