refactor: migrate to github gql api
This commit is contained in:
@@ -8,9 +8,10 @@ anyhow = "1"
|
|||||||
futures = "0.3.32"
|
futures = "0.3.32"
|
||||||
futures-lite = "2.6.1"
|
futures-lite = "2.6.1"
|
||||||
gpui = { version = "*" }
|
gpui = { version = "*" }
|
||||||
|
graphql_client = { version = "0.16.0", features = ["reqwest"] }
|
||||||
paste = "1.0"
|
paste = "1.0"
|
||||||
rand = "0.10.1"
|
rand = "0.10.1"
|
||||||
reqwest = { version = "0.13.2", features = ["form", "query"] }
|
reqwest = { version = "0.13.2", features = ["form", "json", "query"] }
|
||||||
serde = "1.0.228"
|
serde = "1.0.228"
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.149"
|
||||||
tokio = { version = "1.52.1", features = ["rt-multi-thread", "net", "time"] }
|
tokio = { version = "1.52.1", features = ["rt-multi-thread", "net", "time"] }
|
||||||
|
|||||||
85
build.rs
85
build.rs
@@ -145,7 +145,7 @@ fn render_github_fixtures(fixture_root: &Path) -> String {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
issue_fixtures.insert((filter, page), read_json_fixture(&entry.path()));
|
issue_fixtures.insert((filter, page), read_issue_fixture(&entry.path()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut output = String::new();
|
let mut output = String::new();
|
||||||
@@ -160,7 +160,9 @@ fn render_github_fixtures(fixture_root: &Path) -> String {
|
|||||||
output.push_str(&string_literal(&repo_list));
|
output.push_str(&string_literal(&repo_list));
|
||||||
output.push_str("\n}\n\n");
|
output.push_str("\n}\n\n");
|
||||||
|
|
||||||
output.push_str("pub fn issues_pull_requests(filter: &str, page: u32) -> Option<&'static str> {\n");
|
output.push_str(
|
||||||
|
"pub fn issues_pull_requests(filter: &str, page: u32) -> Option<&'static str> {\n",
|
||||||
|
);
|
||||||
output.push_str(" match (filter, page) {\n");
|
output.push_str(" match (filter, page) {\n");
|
||||||
for ((filter, page), json) in issue_fixtures {
|
for ((filter, page), json) in issue_fixtures {
|
||||||
output.push_str(" (");
|
output.push_str(" (");
|
||||||
@@ -187,6 +189,85 @@ fn read_json_fixture(path: &Path) -> String {
|
|||||||
.unwrap_or_else(|err| panic!("failed to serialize fixture {}: {err}", path.display()))
|
.unwrap_or_else(|err| panic!("failed to serialize fixture {}: {err}", path.display()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_issue_fixture(path: &Path) -> String {
|
||||||
|
let raw = fs::read_to_string(path)
|
||||||
|
.unwrap_or_else(|err| panic!("failed to read fixture {}: {err}", path.display()));
|
||||||
|
let value: serde_json::Value = serde_json::from_str(&raw)
|
||||||
|
.unwrap_or_else(|err| panic!("invalid json fixture {}: {err}", path.display()));
|
||||||
|
let issues = value
|
||||||
|
.as_array()
|
||||||
|
.unwrap_or_else(|| panic!("issue fixture {} must be a json array", path.display()));
|
||||||
|
|
||||||
|
let items = issues.iter().map(map_issue_fixture).collect::<Vec<_>>();
|
||||||
|
let start_cursor = issues.first().and_then(issue_fixture_cursor);
|
||||||
|
let end_cursor = issues.last().and_then(issue_fixture_cursor);
|
||||||
|
|
||||||
|
serde_json::to_string(&serde_json::json!({
|
||||||
|
"items": items,
|
||||||
|
"start_cursor": start_cursor,
|
||||||
|
"end_cursor": end_cursor,
|
||||||
|
}))
|
||||||
|
.unwrap_or_else(|err| {
|
||||||
|
panic!(
|
||||||
|
"failed to serialize mapped issue fixture {}: {err}",
|
||||||
|
path.display()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_issue_fixture(issue: &serde_json::Value) -> serde_json::Value {
|
||||||
|
serde_json::json!({
|
||||||
|
"title": required_string(issue, &["title"]),
|
||||||
|
"state": issue_fixture_state(issue),
|
||||||
|
"is_draft": issue.get("draft").and_then(serde_json::Value::as_bool).unwrap_or(false),
|
||||||
|
"repo_slug": required_string(issue, &["repository", "full_name"]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn issue_fixture_state(issue: &serde_json::Value) -> &'static str {
|
||||||
|
if issue
|
||||||
|
.get("pull_request")
|
||||||
|
.and_then(|pull_request| pull_request.get("merged_at"))
|
||||||
|
.and_then(serde_json::Value::as_str)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return "Merged";
|
||||||
|
}
|
||||||
|
|
||||||
|
match required_string(issue, &["state"]) {
|
||||||
|
"open" => "Open",
|
||||||
|
"closed" => "Closed",
|
||||||
|
_ => "Unknown",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn issue_fixture_cursor(issue: &serde_json::Value) -> Option<String> {
|
||||||
|
issue
|
||||||
|
.get("node_id")
|
||||||
|
.and_then(serde_json::Value::as_str)
|
||||||
|
.map(str::to_owned)
|
||||||
|
.or_else(|| {
|
||||||
|
issue
|
||||||
|
.get("id")
|
||||||
|
.and_then(serde_json::Value::as_u64)
|
||||||
|
.map(|id| id.to_string())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn required_string<'a>(value: &'a serde_json::Value, path: &[&str]) -> &'a str {
|
||||||
|
let mut current = value;
|
||||||
|
|
||||||
|
for key in path {
|
||||||
|
current = current
|
||||||
|
.get(*key)
|
||||||
|
.unwrap_or_else(|| panic!("missing key {} in fixture", path.join(".")));
|
||||||
|
}
|
||||||
|
|
||||||
|
current
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or_else(|| panic!("expected string at {} in fixture", path.join(".")))
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_issue_fixture_name(file_name: &str) -> Option<(String, u32)> {
|
fn parse_issue_fixture_name(file_name: &str) -> Option<(String, u32)> {
|
||||||
let name = file_name.strip_suffix(".json")?;
|
let name = file_name.strip_suffix(".json")?;
|
||||||
let rest = name.strip_prefix("issues.pull_requests.")?;
|
let rest = name.strip_prefix("issues.pull_requests.")?;
|
||||||
|
|||||||
10
src/api.rs
10
src/api.rs
@@ -115,3 +115,13 @@ where
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn parse_graphql_response<T>(
|
||||||
|
res: reqwest::Response,
|
||||||
|
) -> Result<graphql_client::Response<T>, Error>
|
||||||
|
where
|
||||||
|
T: serde::de::DeserializeOwned,
|
||||||
|
{
|
||||||
|
let data: graphql_client::Response<T> = res.json().await?;
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ impl query::QueryFn for CreateDeviceCode {
|
|||||||
let res = c
|
let res = c
|
||||||
.http
|
.http
|
||||||
.post(format!(
|
.post(format!(
|
||||||
"https://github.com/login/device/code?client_id={}",
|
"https://github.com/login/device/code?client_id={}&scope=repo,read:user",
|
||||||
c.github.client_id
|
c.github.client_id
|
||||||
))
|
))
|
||||||
.header("Accept", "application/json")
|
.header("Accept", "application/json")
|
||||||
|
|||||||
28
src/api/graphql/list_pull_requests.graphql
Normal file
28
src/api/graphql/list_pull_requests.graphql
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
query PullRequestQuery($query: String!) {
|
||||||
|
search(query: $query, first: 10, type: ISSUE) {
|
||||||
|
issueCount
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
__typename
|
||||||
|
... on PullRequest {
|
||||||
|
isDraft
|
||||||
|
title
|
||||||
|
state
|
||||||
|
repository {
|
||||||
|
name
|
||||||
|
owner {
|
||||||
|
__typename
|
||||||
|
login
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursor
|
||||||
|
}
|
||||||
|
pageInfo {
|
||||||
|
startCursor
|
||||||
|
endCursor
|
||||||
|
hasNextPage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
177175
src/api/graphql/schema.json
Normal file
177175
src/api/graphql/schema.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,168 +1,51 @@
|
|||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use graphql_client::{GraphQLQuery, Response};
|
||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{self, user},
|
api::{
|
||||||
|
self,
|
||||||
|
issues::pull_request_query::{PullRequestQuerySearchEdgesNode, PullRequestState},
|
||||||
|
},
|
||||||
query,
|
query,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Deserialize)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
#[repr(transparent)]
|
#[repr(transparent)]
|
||||||
pub(crate) struct Id(u64);
|
pub(crate) struct Id(String);
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub(crate) struct Issue {
|
|
||||||
pub(crate) id: Id,
|
|
||||||
pub(crate) node_id: String,
|
|
||||||
pub(crate) url: String,
|
|
||||||
pub(crate) repository_url: String,
|
|
||||||
pub(crate) labels_url: String,
|
|
||||||
pub(crate) comments_url: String,
|
|
||||||
pub(crate) events_url: String,
|
|
||||||
pub(crate) html_url: String,
|
|
||||||
pub(crate) number: u64,
|
|
||||||
pub(crate) state: String,
|
|
||||||
pub(crate) state_reason: Option<String>,
|
|
||||||
pub(crate) title: String,
|
|
||||||
pub(crate) body: Option<String>,
|
|
||||||
pub(crate) body_text: Option<String>,
|
|
||||||
pub(crate) body_html: Option<String>,
|
|
||||||
pub(crate) user: Option<user::User>,
|
|
||||||
pub(crate) labels: Vec<Label>,
|
|
||||||
pub(crate) assignee: Option<user::User>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub(crate) assignees: Vec<user::User>,
|
|
||||||
pub(crate) milestone: Option<Milestone>,
|
|
||||||
pub(crate) locked: bool,
|
|
||||||
pub(crate) active_lock_reason: Option<String>,
|
|
||||||
pub(crate) comments: u64,
|
|
||||||
pub(crate) pull_request: Option<PullRequest>,
|
|
||||||
pub(crate) closed_at: Option<String>,
|
|
||||||
pub(crate) created_at: String,
|
|
||||||
pub(crate) updated_at: String,
|
|
||||||
pub(crate) closed_by: Option<user::User>,
|
|
||||||
pub(crate) author_association: String,
|
|
||||||
pub(crate) draft: Option<bool>,
|
|
||||||
pub(crate) timeline_url: Option<String>,
|
|
||||||
pub(crate) repository: Option<Repository>,
|
|
||||||
pub(crate) performed_via_github_app: Option<serde_json::Value>,
|
|
||||||
pub(crate) reactions: Option<Reactions>,
|
|
||||||
pub(crate) pinned_comment: Option<serde_json::Value>,
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub(crate) issue_type: Option<IssueType>,
|
|
||||||
pub(crate) sub_issues_summary: Option<SubIssuesSummary>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub(crate) enum Label {
|
|
||||||
Name(String),
|
|
||||||
Detail(LabelDetail),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub(crate) struct LabelDetail {
|
|
||||||
pub(crate) id: u64,
|
|
||||||
pub(crate) node_id: String,
|
|
||||||
pub(crate) url: String,
|
|
||||||
pub(crate) name: String,
|
|
||||||
pub(crate) description: Option<String>,
|
|
||||||
pub(crate) color: String,
|
|
||||||
pub(crate) default: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub(crate) struct Milestone {
|
|
||||||
pub(crate) url: String,
|
|
||||||
pub(crate) html_url: String,
|
|
||||||
pub(crate) labels_url: String,
|
|
||||||
pub(crate) id: u64,
|
|
||||||
pub(crate) node_id: String,
|
|
||||||
pub(crate) number: u64,
|
|
||||||
pub(crate) state: String,
|
|
||||||
pub(crate) title: String,
|
|
||||||
pub(crate) description: Option<String>,
|
|
||||||
pub(crate) creator: Option<user::User>,
|
|
||||||
pub(crate) open_issues: u64,
|
|
||||||
pub(crate) closed_issues: u64,
|
|
||||||
pub(crate) created_at: String,
|
|
||||||
pub(crate) updated_at: String,
|
|
||||||
pub(crate) closed_at: Option<String>,
|
|
||||||
pub(crate) due_on: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub(crate) struct PullRequest {
|
|
||||||
pub(crate) url: String,
|
|
||||||
pub(crate) html_url: String,
|
|
||||||
pub(crate) diff_url: String,
|
|
||||||
pub(crate) patch_url: String,
|
|
||||||
pub(crate) merged_at: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub(crate) struct Repository {
|
|
||||||
pub(crate) id: u64,
|
|
||||||
pub(crate) node_id: String,
|
|
||||||
pub(crate) name: String,
|
|
||||||
pub(crate) full_name: String,
|
|
||||||
pub(crate) owner: user::User,
|
|
||||||
pub(crate) private: bool,
|
|
||||||
pub(crate) html_url: String,
|
|
||||||
pub(crate) description: Option<String>,
|
|
||||||
pub(crate) fork: bool,
|
|
||||||
pub(crate) url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub(crate) struct Reactions {
|
|
||||||
pub(crate) url: String,
|
|
||||||
pub(crate) total_count: u64,
|
|
||||||
#[serde(rename = "+1")]
|
|
||||||
pub(crate) plus_one: u64,
|
|
||||||
#[serde(rename = "-1")]
|
|
||||||
pub(crate) minus_one: u64,
|
|
||||||
pub(crate) laugh: u64,
|
|
||||||
pub(crate) confused: u64,
|
|
||||||
pub(crate) heart: u64,
|
|
||||||
pub(crate) hooray: u64,
|
|
||||||
pub(crate) rocket: u64,
|
|
||||||
pub(crate) eyes: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub(crate) struct IssueType {
|
|
||||||
pub(crate) id: u64,
|
|
||||||
pub(crate) node_id: String,
|
|
||||||
pub(crate) name: String,
|
|
||||||
pub(crate) description: Option<String>,
|
|
||||||
pub(crate) color: Option<String>,
|
|
||||||
pub(crate) created_at: Option<String>,
|
|
||||||
pub(crate) updated_at: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub(crate) struct SubIssuesSummary {
|
|
||||||
pub(crate) total: u64,
|
|
||||||
pub(crate) completed: u64,
|
|
||||||
pub(crate) percent_completed: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for Id {
|
impl Deref for Id {
|
||||||
type Target = u64;
|
type Target = String;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<u64> for Id {
|
#[derive(Debug, Deserialize)]
|
||||||
fn from(id: u64) -> Self {
|
pub(crate) struct PullRequestPaginatedResponse {
|
||||||
Self(id)
|
pub(crate) items: Vec<PullRequest>,
|
||||||
}
|
pub(crate) start_cursor: Option<String>,
|
||||||
|
pub(crate) end_cursor: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(crate) struct PullRequest {
|
||||||
|
pub(crate) title: String,
|
||||||
|
pub(crate) state: IssueState,
|
||||||
|
pub(crate) is_draft: bool,
|
||||||
|
pub(crate) repo_slug: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Deserialize)]
|
||||||
|
pub(crate) enum IssueState {
|
||||||
|
Open,
|
||||||
|
Closed,
|
||||||
|
Merged,
|
||||||
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Id {
|
impl std::fmt::Display for Id {
|
||||||
@@ -171,15 +54,24 @@ impl std::fmt::Display for Id {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Issue {
|
impl From<PullRequestState> for IssueState {
|
||||||
pub(crate) const FILTER_ASSIGNED: &'static str = "assigned";
|
fn from(state: PullRequestState) -> Self {
|
||||||
pub(crate) const FILTER_CREATED: &'static str = "created";
|
match state {
|
||||||
pub(crate) const FILTER_MENTIONED: &'static str = "mentioned";
|
PullRequestState::OPEN => Self::Open,
|
||||||
pub(crate) const FILTER_SUBSCRIBED: &'static str = "subscribed";
|
PullRequestState::CLOSED => Self::Closed,
|
||||||
pub(crate) const FILTER_REPOS: &'static str = "repos";
|
PullRequestState::MERGED => Self::Merged,
|
||||||
pub(crate) const FILTER_ALL: &'static str = "all";
|
_ => Self::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(graphql_client::GraphQLQuery)]
|
||||||
|
#[graphql(
|
||||||
|
schema_path = "src/api/graphql/schema.json",
|
||||||
|
query_path = "src/api/graphql/list_pull_requests.graphql"
|
||||||
|
)]
|
||||||
|
struct PullRequestQuery;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct ListPullRequests {
|
pub(crate) struct ListPullRequests {
|
||||||
pub filter: Option<&'static str>,
|
pub filter: Option<&'static str>,
|
||||||
@@ -187,7 +79,7 @@ pub(crate) struct ListPullRequests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl query::QueryFn for ListPullRequests {
|
impl query::QueryFn for ListPullRequests {
|
||||||
type Data = Vec<Issue>;
|
type Data = PullRequestPaginatedResponse;
|
||||||
type Error = api::Error;
|
type Error = api::Error;
|
||||||
type Context = api::QueryContext;
|
type Context = api::QueryContext;
|
||||||
|
|
||||||
@@ -206,21 +98,54 @@ impl query::QueryFn for ListPullRequests {
|
|||||||
return super::mock::list_pull_requests(self.filter, self.page);
|
return super::mock::list_pull_requests(self.filter, self.page);
|
||||||
}
|
}
|
||||||
|
|
||||||
let page_string = self.page.to_string();
|
let query_string = match self.filter {
|
||||||
let mut params: Vec<(&str, &str)> = Vec::with_capacity(4);
|
Some(filter) => format!("is:pr archived:false sort:updated-desc {}", filter),
|
||||||
params.push(("pulls", "true"));
|
None => "is:pr archived:false sort:updated-desc".into(),
|
||||||
params.push(("page", &page_string));
|
};
|
||||||
params.push(("direction", "desc"));
|
|
||||||
if let Some(filter) = self.filter {
|
let gql = PullRequestQuery::build_query(pull_request_query::Variables {
|
||||||
params.push(("filter", filter))
|
query: query_string,
|
||||||
}
|
});
|
||||||
|
|
||||||
let res = c
|
let res = c
|
||||||
.github_request(Method::GET, "/issues")?
|
.github_request(Method::POST, "/graphql")?
|
||||||
.query(¶ms)
|
.json(&gql)
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
api::parse_response::<Vec<Issue>>(res).await
|
let data = api::parse_graphql_response::<pull_request_query::ResponseData>(res)
|
||||||
|
.await?
|
||||||
|
.data
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(PullRequestPaginatedResponse {
|
||||||
|
items: data
|
||||||
|
.search
|
||||||
|
.edges
|
||||||
|
.map(|it| {
|
||||||
|
it.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.filter_map(|edge| {
|
||||||
|
edge.node.and_then(|n| match n {
|
||||||
|
PullRequestQuerySearchEdgesNode::PullRequest(p) => {
|
||||||
|
Some(PullRequest {
|
||||||
|
title: p.title,
|
||||||
|
state: p.state.into(),
|
||||||
|
is_draft: p.is_draft,
|
||||||
|
repo_slug: format!(
|
||||||
|
"{}/{}",
|
||||||
|
p.repository.owner.login, p.repository.name
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
|
start_cursor: data.search.page_info.start_cursor,
|
||||||
|
end_cursor: data.search.page_info.end_cursor,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ pub(crate) fn list_repos() -> Result<Vec<repo::Repository>, api::Error> {
|
|||||||
pub(crate) fn list_pull_requests(
|
pub(crate) fn list_pull_requests(
|
||||||
filter: Option<&str>,
|
filter: Option<&str>,
|
||||||
page: u32,
|
page: u32,
|
||||||
) -> Result<Vec<issues::Issue>, api::Error> {
|
) -> Result<issues::PullRequestPaginatedResponse, api::Error> {
|
||||||
let filter = filter.unwrap_or_default();
|
let filter = filter.unwrap_or_default();
|
||||||
let json = issues_pull_requests(filter, page).ok_or_else(|| {
|
let json = issues_pull_requests(filter, page).ok_or_else(|| {
|
||||||
api::Error::MissingMockFixture(format!("issues.pull_requests filter={filter} page={page}"))
|
api::Error::MissingMockFixture(format!("issues.pull_requests filter={filter} page={page}"))
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ fn setup_application(cx: &mut gpui::App) {
|
|||||||
auth: None,
|
auth: None,
|
||||||
github: api::GithubCredentials {
|
github: api::GithubCredentials {
|
||||||
base_url: "https://api.github.com",
|
base_url: "https://api.github.com",
|
||||||
client_id: "Iv23liZD4bMQpGJICsR7",
|
client_id: "Ov23linXmiNkn2gOvOmi",
|
||||||
},
|
},
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
|
|||||||
@@ -29,15 +29,16 @@ struct IssueListItem {
|
|||||||
repo_name: Option<gpui::SharedString>,
|
repo_name: Option<gpui::SharedString>,
|
||||||
title: gpui::SharedString,
|
title: gpui::SharedString,
|
||||||
description: Option<gpui::SharedString>,
|
description: Option<gpui::SharedString>,
|
||||||
status: IssueStatus,
|
status: api::issues::IssueState,
|
||||||
is_last: bool,
|
is_last: bool,
|
||||||
|
is_draft: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new(cx: &mut gpui::Context<IssueList>) -> IssueList {
|
pub(crate) fn new(cx: &mut gpui::Context<IssueList>) -> IssueList {
|
||||||
let mut list = IssueList {
|
let mut list = IssueList {
|
||||||
pr_query: use_query(
|
pr_query: use_query(
|
||||||
api::issues::ListPullRequests {
|
api::issues::ListPullRequests {
|
||||||
filter: Some(api::issues::Issue::FILTER_ALL),
|
filter: Some("author:@me state:open"),
|
||||||
page: 1,
|
page: 1,
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
@@ -54,33 +55,20 @@ impl IssueList {
|
|||||||
fn on_create(&mut self, cx: &mut gpui::Context<Self>) {
|
fn on_create(&mut self, cx: &mut gpui::Context<Self>) {
|
||||||
cx.observe(&self.pr_query, |this, _, cx| {
|
cx.observe(&self.pr_query, |this, _, cx| {
|
||||||
let data = read_query(&this.pr_query, cx);
|
let data = read_query(&this.pr_query, cx);
|
||||||
if let QueryStatus::Loaded(issues) = data {
|
if let QueryStatus::Loaded(res) = data {
|
||||||
let old_len = this.list_state.item_count();
|
let old_len = this.list_state.item_count();
|
||||||
let new_len = issues.len();
|
let new_len = res.items.len();
|
||||||
|
|
||||||
this.list_items = issues
|
let new_items = res.items.iter().enumerate().map(|(i, it)| IssueListItem {
|
||||||
.iter()
|
repo_name: Some(gpui::SharedString::new(it.repo_slug.as_str())),
|
||||||
.enumerate()
|
title: gpui::SharedString::new(it.title.as_str()),
|
||||||
.map(|(i, it)| IssueListItem {
|
description: None,
|
||||||
repo_name: it.repository.as_ref().map(|it| {
|
status: it.state,
|
||||||
gpui::SharedString::new(format!("{}/{}", it.owner.login, it.name))
|
is_last: i == new_len - 1,
|
||||||
}),
|
is_draft: it.is_draft,
|
||||||
title: gpui::SharedString::new(it.title.as_str()),
|
});
|
||||||
description: it
|
|
||||||
.body_text
|
|
||||||
.as_ref()
|
|
||||||
.map(|it| gpui::SharedString::new(it.as_str())),
|
|
||||||
status: if it.state == "open" {
|
|
||||||
IssueStatus::Open
|
|
||||||
} else if it.state == "closed" {
|
|
||||||
IssueStatus::Closed
|
|
||||||
} else {
|
|
||||||
IssueStatus::Draft
|
|
||||||
},
|
|
||||||
is_last: i == new_len - 1,
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
|
this.list_items.splice(old_len..old_len, new_items);
|
||||||
this.list_state.splice(old_len..old_len, new_len);
|
this.list_state.splice(old_len..old_len, new_len);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -114,15 +102,19 @@ impl gpui::RenderOnce for IssueListItem {
|
|||||||
.text_xs()
|
.text_xs()
|
||||||
.opacity(0.5);
|
.opacity(0.5);
|
||||||
|
|
||||||
let icon = match self.status {
|
let icon = if self.is_draft {
|
||||||
IssueStatus::Draft => font_icon(FontIcon::PullRequestDraft)
|
font_icon(FontIcon::PullRequestDraft)
|
||||||
.text_color(theme.colors.text)
|
.text_color(theme.colors.text)
|
||||||
.opacity(0.5),
|
.opacity(0.5)
|
||||||
IssueStatus::Open => {
|
} else {
|
||||||
font_icon(FontIcon::PullRequestArrow).text_color(theme.colors.success)
|
match self.status {
|
||||||
}
|
api::issues::IssueState::Closed => {
|
||||||
IssueStatus::Closed => {
|
font_icon(FontIcon::PullRequestClosed).text_color(theme.colors.danger)
|
||||||
font_icon(FontIcon::PullRequestClosed).text_color(theme.colors.danger)
|
}
|
||||||
|
api::issues::IssueState::Merged => {
|
||||||
|
font_icon(FontIcon::PullRequestClosed).text_color(theme.colors.success)
|
||||||
|
}
|
||||||
|
_ => font_icon(FontIcon::PullRequestArrow).text_color(theme.colors.success),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api, app,
|
app,
|
||||||
component::{
|
component::{
|
||||||
font_icon::{FontIcon, font_icon},
|
font_icon::{FontIcon, font_icon},
|
||||||
text::text,
|
text::text,
|
||||||
@@ -34,29 +34,19 @@ pub enum SidebarItemValue {
|
|||||||
|
|
||||||
pub fn new() -> Sidebar {
|
pub fn new() -> Sidebar {
|
||||||
Sidebar {
|
Sidebar {
|
||||||
selected_item_id: Some("all"),
|
selected_item_id: Some("authored"),
|
||||||
on_item_change: None,
|
on_item_change: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sidebar {
|
impl Sidebar {
|
||||||
const ALL_ITEMS: [SidebarItem; 3] = [
|
const ALL_ITEMS: [SidebarItem; 2] = [
|
||||||
SidebarItem {
|
|
||||||
id: "all",
|
|
||||||
title: "All",
|
|
||||||
icon: FontIcon::List,
|
|
||||||
value: SidebarItemValue::PullRequest {
|
|
||||||
filter: api::issues::Issue::FILTER_ALL,
|
|
||||||
},
|
|
||||||
is_selected: false,
|
|
||||||
on_click: None,
|
|
||||||
},
|
|
||||||
SidebarItem {
|
SidebarItem {
|
||||||
id: "authored",
|
id: "authored",
|
||||||
title: "Authored",
|
title: "Authored",
|
||||||
icon: FontIcon::PencilLine,
|
icon: FontIcon::PencilLine,
|
||||||
value: SidebarItemValue::PullRequest {
|
value: SidebarItemValue::PullRequest {
|
||||||
filter: api::issues::Issue::FILTER_CREATED,
|
filter: "author:@me",
|
||||||
},
|
},
|
||||||
is_selected: false,
|
is_selected: false,
|
||||||
on_click: None,
|
on_click: None,
|
||||||
@@ -66,20 +56,12 @@ impl Sidebar {
|
|||||||
title: "Assigned",
|
title: "Assigned",
|
||||||
icon: FontIcon::UserPlus,
|
icon: FontIcon::UserPlus,
|
||||||
value: SidebarItemValue::PullRequest {
|
value: SidebarItemValue::PullRequest {
|
||||||
filter: api::issues::Issue::FILTER_ASSIGNED,
|
filter: "review-requested:@me",
|
||||||
},
|
},
|
||||||
is_selected: false,
|
is_selected: false,
|
||||||
on_click: None,
|
on_click: None,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
fn select_sidebar_item(&mut self, id: &str, cx: &mut gpui::Context<Self>) {
|
|
||||||
let Some(item) = Sidebar::ALL_ITEMS.iter().find(|item| item.id == id) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
self.selected_item_id = Some(item.id);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sidebar {
|
impl Sidebar {
|
||||||
|
|||||||
Reference in New Issue
Block a user