Skip to content
Clone
f0a1552
306 lines 10.5 KB
/**
* 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<Uuid>` / `Option<IssueState>`
* deserialization rejects an empty `milestone_id=` / `state=` with a 400.
*/
const nz = <T,>(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 = <T>(method: PromiseLike<ApiEnvelope<unknown>>): Promise<IssuePage<T>> =>
unwrapApi<IssuePage<T> | null>(method as PromiseLike<ApiEnvelope<IssuePage<T> | null>>).then(p => ({
items: p?.items ?? [],
total: p?.total ?? 0,
limit: p?.limit ?? 0,
offset: p?.offset ?? 0,
}))
const listOf = <T>(method: PromiseLike<ApiEnvelope<unknown>>): Promise<T[]> =>
unwrapApi<T[] | null>(method as PromiseLike<ApiEnvelope<T[] | null>>).then(rows => rows ?? [])
const oneOf = <T>(method: PromiseLike<ApiEnvelope<unknown>>): Promise<T> =>
unwrapApi<T>(method as PromiseLike<ApiEnvelope<T>>)
/* ------------------------------------------------------------------ board */
export const getBoard = ({ space, board }: BoardRef) =>
unwrapApi<Board>(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<Board>(
Apis.issues.update_board({ pathParams: { space_name: space, board_id: board }, data: cmd }),
)
export const deleteBoard = ({ space, board }: BoardRef) =>
unwrapApi<null>(
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<import('./issue').IssueListItem>(
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<IssueDetail>(
Apis.issues.get_issue({ pathParams: { space_name: space, board_id: board, issue_id: issueId } }),
)
export const createIssue = ({ space, board }: BoardRef, cmd: NewIssueCmd) =>
oneOf<IssueDetail>(
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<IssueDetail>(
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<IssueDetail>(
Apis.issues.close_issue({ pathParams: { space_name: space, board_id: board, issue_id: issueId } }),
)
export const reopenIssue = ({ space, board }: BoardRef, issueId: string) =>
oneOf<IssueDetail>(
Apis.issues.reopen_issue({ pathParams: { space_name: space, board_id: board, issue_id: issueId } }),
)
export const deleteIssue = ({ space, board }: BoardRef, issueId: string) =>
unwrapApi<null>(
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<unknown>(
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<CommentNode>(
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<IssueComment>(
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<IssueComment>(
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<null>(
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<IssueEvent>(
Apis.issues.list_events({
pathParams: { space_name: space, board_id: board, issue_id: issueId },
params: { limit },
}),
)
export const listLabels = ({ space, board }: BoardRef) =>
pageOf<IssueLabel>(
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<IssueLabel>(
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<IssueLabel>(
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<null>(
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<IssueLabel>(
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<null>(
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<null>(
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<Milestone>(
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<Milestone>(
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<Milestone>(
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<null>(
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<SpaceMember>(
Apis.space.list_space_members({ pathParams: { space_name: space }, params: { limit: 100 } }),
)