feat(ui): forge user lookup — assignee combobox, @-mentions, repo-scoped user search
Repo-scoped assignable-user search for GitHub, GitLab, and Gitea, surfaced as
an assignee combobox in the metadata editor and @-mention autocomplete in
forge comment/reply/review surfaces.
- server: GET /api/{provider}/users/search (assignees / project members),
query + directory/override repo resolution, 429 -> 503, connected:false
degradation; GitLab assignee writes resolve login -> ID server-side
- wire: searchUsers (+ searchLabels/milestones/branches/tags) on the three
API clients with tests
- facade: userSearch capability (all three), searchUsers adapters,
mapGithubAssignee/mapGitlabMember/mapGiteaAssignee -> ForgeUser
- ui: ForgeLookupCombobox (keyboard nav, debounced 30s-TTL cache,
connected-only caching), ForgeMentionTextarea (@ token parsing, caret
restore), free-text fallback when lookup is unavailable; i18n in 12 locales
- extras sharing the same infrastructure: GitLab create-issue dialog and
label/milestone/branch/tag lookups in the metadata editor
This commit is contained in:
@@ -817,6 +817,42 @@ export type GitHubUserSummary = {
|
||||
email?: string;
|
||||
};
|
||||
|
||||
// ---- Rich lookup results (repo-scoped search for pickers/mentions) ----
|
||||
// Each result carries the connected repo so the facade can surface cross-repo /
|
||||
// fork contexts, and the items are always arrays (empty on success with no
|
||||
// matches). `connected: false` means the lookup could not be performed and
|
||||
// must not be treated as an authoritative empty list.
|
||||
|
||||
export type GitHubUsersSearchResult = {
|
||||
connected: boolean;
|
||||
repo?: GitHubRepoRef | null;
|
||||
users: GitHubUserSummary[];
|
||||
};
|
||||
|
||||
export type GitHubLabelsSearchResult = {
|
||||
connected: boolean;
|
||||
repo?: GitHubRepoRef | null;
|
||||
labels: GitHubIssueLabel[];
|
||||
};
|
||||
|
||||
export type GitHubMilestonesSearchResult = {
|
||||
connected: boolean;
|
||||
repo?: GitHubRepoRef | null;
|
||||
milestones: Array<{ title: string; state?: string }>;
|
||||
};
|
||||
|
||||
export type GitHubBranchesSearchResult = {
|
||||
connected: boolean;
|
||||
repo?: GitHubRepoRef | null;
|
||||
branches: string[];
|
||||
};
|
||||
|
||||
export type GitHubTagsSearchResult = {
|
||||
connected: boolean;
|
||||
repo?: GitHubRepoRef | null;
|
||||
tags: string[];
|
||||
};
|
||||
|
||||
type GitHubRepoRef = {
|
||||
owner: string;
|
||||
repo: string;
|
||||
@@ -1141,6 +1177,21 @@ export type GitHubIssueCommentResult = {
|
||||
comment?: GitHubIssueComment | null;
|
||||
};
|
||||
|
||||
export type GitHubIssueCreateInput = {
|
||||
directory: string;
|
||||
title: string;
|
||||
body?: string;
|
||||
labels?: string[];
|
||||
owner?: string;
|
||||
repo?: string;
|
||||
};
|
||||
|
||||
export type GitHubIssueCreateResult = {
|
||||
connected: boolean;
|
||||
repo?: { owner: string; repo: string; url?: string } | null;
|
||||
issue?: GitHubIssue | null;
|
||||
};
|
||||
|
||||
export type GitHubIssueUpdateInput = {
|
||||
directory: string;
|
||||
number: number;
|
||||
@@ -1236,6 +1287,12 @@ export interface GitHubAPI {
|
||||
authSetGhCliDisabled(disabled: boolean): Promise<{ disabled: boolean }>;
|
||||
me?(): Promise<GitHubUserSummary>;
|
||||
|
||||
searchUsers?(directory: string, query: string, options?: { sourceRepo?: GitHubRepoSelector | null }): Promise<GitHubUsersSearchResult>;
|
||||
searchLabels?(directory: string, query: string, options?: { sourceRepo?: GitHubRepoSelector | null }): Promise<GitHubLabelsSearchResult>;
|
||||
searchMilestones?(directory: string, query: string, options?: { sourceRepo?: GitHubRepoSelector | null }): Promise<GitHubMilestonesSearchResult>;
|
||||
searchBranches?(directory: string, query: string, options?: { sourceRepo?: GitHubRepoSelector | null }): Promise<GitHubBranchesSearchResult>;
|
||||
searchTags?(directory: string, query: string, options?: { sourceRepo?: GitHubRepoSelector | null }): Promise<GitHubTagsSearchResult>;
|
||||
|
||||
prStatus(directory: string, branch: string, remote?: string, options?: { force?: boolean }): Promise<GitHubPullRequestStatus>;
|
||||
prCreate(payload: GitHubPullRequestCreateInput): Promise<GitHubPullRequest>;
|
||||
prUpdate(payload: GitHubPullRequestUpdateInput): Promise<GitHubPullRequest>;
|
||||
@@ -1257,6 +1314,7 @@ export interface GitHubAPI {
|
||||
repoUpstream(directory: string): Promise<GitHubRepoUpstreamResult>;
|
||||
repoBranches(owner: string, repo: string): Promise<string[]>;
|
||||
issueComment?(input: GitHubIssueCommentInput): Promise<GitHubIssueCommentResult>;
|
||||
issueCreate?(input: GitHubIssueCreateInput): Promise<GitHubIssueCreateResult>;
|
||||
issueUpdate?(input: GitHubIssueUpdateInput): Promise<GitHubIssueUpdateResult>;
|
||||
prComment?(input: GitHubIssueCommentInput): Promise<GitHubIssueCommentResult>;
|
||||
prReviewComment?(input: GitHubReviewCommentInput): Promise<GitHubReviewCommentResult>;
|
||||
@@ -1392,6 +1450,40 @@ export type GitLabBranchesResult = {
|
||||
defaultBranch?: string | null;
|
||||
};
|
||||
|
||||
// ---- Rich lookup results (repo-scoped search for pickers/mentions) ----
|
||||
// `connected: false` means the lookup could not be performed and must not be
|
||||
// treated as an authoritative empty list.
|
||||
|
||||
export type GitLabUsersSearchResult = {
|
||||
connected: boolean;
|
||||
repo?: GitLabRepoRef | null;
|
||||
users: GitLabUserSummary[];
|
||||
};
|
||||
|
||||
export type GitLabLabelsSearchResult = {
|
||||
connected: boolean;
|
||||
repo?: GitLabRepoRef | null;
|
||||
labels: string[];
|
||||
};
|
||||
|
||||
export type GitLabMilestonesSearchResult = {
|
||||
connected: boolean;
|
||||
repo?: GitLabRepoRef | null;
|
||||
milestones: Array<{ title: string; state?: string }>;
|
||||
};
|
||||
|
||||
export type GitLabBranchesSearchResult = {
|
||||
connected: boolean;
|
||||
repo?: GitLabRepoRef | null;
|
||||
branches: string[];
|
||||
};
|
||||
|
||||
export type GitLabTagsSearchResult = {
|
||||
connected: boolean;
|
||||
repo?: GitLabRepoRef | null;
|
||||
tags: string[];
|
||||
};
|
||||
|
||||
export type GitLabMergeRequestCommit = {
|
||||
sha: string;
|
||||
shortSha: string;
|
||||
@@ -1438,6 +1530,8 @@ export type GitLabMergeRequestUpdateInput = {
|
||||
description?: string;
|
||||
state?: 'open' | 'closed';
|
||||
labels?: string[];
|
||||
/** Assignee logins; the server resolves them to user IDs via project members. */
|
||||
assignees?: string[];
|
||||
assigneeIds?: number[];
|
||||
milestone?: string | null;
|
||||
};
|
||||
@@ -1480,6 +1574,21 @@ export type GitLabIssueCommentResult = {
|
||||
comment?: GitLabIssueComment | null;
|
||||
};
|
||||
|
||||
export type GitLabIssueCreateInput = {
|
||||
directory: string;
|
||||
title: string;
|
||||
body?: string;
|
||||
labels?: string[];
|
||||
namespace?: string;
|
||||
project?: string;
|
||||
};
|
||||
|
||||
export type GitLabIssueCreateResult = {
|
||||
connected: boolean;
|
||||
repo?: GitLabRepoRef | null;
|
||||
issue?: GitLabIssue | null;
|
||||
};
|
||||
|
||||
export type GitLabIssueUpdateInput = {
|
||||
directory: string;
|
||||
number: number;
|
||||
@@ -1487,6 +1596,8 @@ export type GitLabIssueUpdateInput = {
|
||||
body?: string;
|
||||
state?: 'open' | 'closed';
|
||||
labels?: string[];
|
||||
/** Assignee logins; the server resolves them to user IDs via project members. */
|
||||
assignees?: string[];
|
||||
assigneeIds?: number[];
|
||||
milestone?: string | null;
|
||||
namespace?: string;
|
||||
@@ -1569,10 +1680,17 @@ export interface GitLabAPI {
|
||||
mrTimeline?(directory: string, number: number, options?: { namespace?: string; project?: string }): Promise<GitLabMergeRequestTimelineResult>;
|
||||
|
||||
issueComment?(input: GitLabIssueCommentInput): Promise<GitLabIssueCommentResult>;
|
||||
issueCreate?(input: GitLabIssueCreateInput): Promise<GitLabIssueCreateResult>;
|
||||
issueUpdate?(input: GitLabIssueUpdateInput): Promise<GitLabIssueUpdateResult>;
|
||||
mrComment?(input: GitLabMrNoteInput): Promise<GitLabMrNoteResult>;
|
||||
mrApprove?(input: GitLabMrApproveInput): Promise<GitLabMrApproveResult>;
|
||||
|
||||
searchUsers?(directory: string, query: string, options?: { namespace?: string; project?: string }): Promise<GitLabUsersSearchResult>;
|
||||
searchLabels?(directory: string, query: string, options?: { namespace?: string; project?: string }): Promise<GitLabLabelsSearchResult>;
|
||||
searchMilestones?(directory: string, query: string, options?: { namespace?: string; project?: string }): Promise<GitLabMilestonesSearchResult>;
|
||||
searchBranches?(directory: string, query: string, options?: { namespace?: string; project?: string }): Promise<GitLabBranchesSearchResult>;
|
||||
searchTags?(directory: string, query: string, options?: { namespace?: string; project?: string }): Promise<GitLabTagsSearchResult>;
|
||||
|
||||
repoBranches(namespace: string, project: string): Promise<GitLabBranchesResult>;
|
||||
}
|
||||
|
||||
@@ -1705,6 +1823,21 @@ export type GiteaIssueCommentResult = {
|
||||
comment?: GiteaComment | null;
|
||||
};
|
||||
|
||||
export type GiteaIssueCreateInput = {
|
||||
directory: string;
|
||||
title: string;
|
||||
body?: string;
|
||||
labels?: string[];
|
||||
owner?: string;
|
||||
repo?: string;
|
||||
};
|
||||
|
||||
export type GiteaIssueCreateResult = {
|
||||
connected: boolean;
|
||||
repo?: { owner: string; repo: string; url?: string } | null;
|
||||
issue?: GiteaIssue | null;
|
||||
};
|
||||
|
||||
export type GiteaIssueUpdateInput = {
|
||||
directory: string;
|
||||
number: number;
|
||||
@@ -1752,6 +1885,40 @@ export type GiteaRepoLabelsResult = {
|
||||
labels: GiteaRepoLabel[];
|
||||
};
|
||||
|
||||
// ---- Rich lookup results (repo-scoped search for pickers/mentions) ----
|
||||
// `connected: false` means the lookup could not be performed and must not be
|
||||
// treated as an authoritative empty list.
|
||||
|
||||
export type GiteaUsersSearchResult = {
|
||||
connected: boolean;
|
||||
repo?: { owner: string; repo: string; url?: string } | null;
|
||||
users: GiteaUserSummary[];
|
||||
};
|
||||
|
||||
export type GiteaLabelsSearchResult = {
|
||||
connected: boolean;
|
||||
repo?: { owner: string; repo: string; url?: string } | null;
|
||||
labels: GiteaRepoLabel[];
|
||||
};
|
||||
|
||||
export type GiteaMilestonesSearchResult = {
|
||||
connected: boolean;
|
||||
repo?: { owner: string; repo: string; url?: string } | null;
|
||||
milestones: Array<{ title: string; state?: string }>;
|
||||
};
|
||||
|
||||
export type GiteaBranchesSearchResult = {
|
||||
connected: boolean;
|
||||
repo?: { owner: string; repo: string; url?: string } | null;
|
||||
branches: string[];
|
||||
};
|
||||
|
||||
export type GiteaTagsSearchResult = {
|
||||
connected: boolean;
|
||||
repo?: { owner: string; repo: string; url?: string } | null;
|
||||
tags: string[];
|
||||
};
|
||||
|
||||
export interface GiteaAPI {
|
||||
authStatus(): Promise<GiteaAuthStatus>;
|
||||
authConnect(input: { accessToken: string; baseUrl: string }): Promise<GiteaAuthStatus>;
|
||||
@@ -1777,11 +1944,18 @@ export interface GiteaAPI {
|
||||
prReviews?(directory: string, number: number, options?: { owner?: string; repo?: string }): Promise<GiteaPullRequestReviewsResult>;
|
||||
|
||||
issueComment?(input: GiteaIssueCommentInput): Promise<GiteaIssueCommentResult>;
|
||||
issueCreate?(input: GiteaIssueCreateInput): Promise<GiteaIssueCreateResult>;
|
||||
issueUpdate?(input: GiteaIssueUpdateInput): Promise<GiteaIssueUpdateResult>;
|
||||
prComment?(input: GiteaIssueCommentInput): Promise<GiteaIssueCommentResult>;
|
||||
prSubmitReview?(input: GiteaPullReviewInput): Promise<GiteaPullReviewResult>;
|
||||
repoLabels?(directory: string, options?: { owner?: string; repo?: string }): Promise<GiteaRepoLabelsResult>;
|
||||
|
||||
searchUsers?(directory: string, query: string, options?: { owner?: string; repo?: string }): Promise<GiteaUsersSearchResult>;
|
||||
searchLabels?(directory: string, query: string, options?: { owner?: string; repo?: string }): Promise<GiteaLabelsSearchResult>;
|
||||
searchMilestones?(directory: string, query: string, options?: { owner?: string; repo?: string }): Promise<GiteaMilestonesSearchResult>;
|
||||
searchBranches?(directory: string, query: string, options?: { owner?: string; repo?: string }): Promise<GiteaBranchesSearchResult>;
|
||||
searchTags?(directory: string, query: string, options?: { owner?: string; repo?: string }): Promise<GiteaTagsSearchResult>;
|
||||
|
||||
repoBranches(owner: string, repo: string): Promise<GiteaBranchesResult>;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user