use crate::schema::schema;
use serde::{Deserialize, Serialize};
use crate::api::client::{ApiError, GitHub};
use crate::api::model::{Capped, NodeId, RepoId, count};
use crate::api::queries::{NumberVariables, NumberVariablesFields, fragments, nodes};
#[derive(cynic::Enum, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ReactionContent {
ThumbsUp,
ThumbsDown,
Laugh,
Hooray,
Confused,
Heart,
Rocket,
Eyes,
}
impl ReactionContent {
pub const ALL: [Self; 8] = [
Self::ThumbsUp,
Self::ThumbsDown,
Self::Laugh,
Self::Hooray,
Self::Confused,
Self::Heart,
Self::Rocket,
Self::Eyes,
];
pub fn emoji(self) -> &'static str {
match self {
Self::ThumbsUp => "👍",
Self::ThumbsDown => "👎",
Self::Laugh => "😄",
Self::Hooray => "🎉",
Self::Confused => "😕",
Self::Heart => "❤",
Self::Rocket => "🚀",
Self::Eyes => "👀",
}
}
pub fn label(self) -> &'static str {
match self {
Self::ThumbsUp => "+1",
Self::ThumbsDown => "-1",
Self::Laugh => "Laugh",
Self::Hooray => "Hooray",
Self::Confused => "Confused",
Self::Heart => "Heart",
Self::Rocket => "Rocket",
Self::Eyes => "Eyes",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Reaction {
pub content: ReactionContent,
pub count: u64,
pub mine: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Editable {
pub id: NodeId,
pub can_update: bool,
pub can_delete: bool,
pub can_react: bool,
pub reactions: Vec<Reaction>,
}
impl Editable {
pub fn reacted(&self, content: ReactionContent) -> bool {
self.reactions
.iter()
.any(|r| r.content == content && r.mine)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SidebarPermits {
pub labels: bool,
pub assignees: bool,
pub milestone: bool,
pub reviewers: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Named {
pub id: NodeId,
pub name: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Written {
Issue,
Pr,
IssueComment,
ReviewComment,
Review,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Edit {
Body {
of: Written,
id: NodeId,
body: String,
},
Delete { of: Written, id: NodeId },
Title { pr: bool, id: NodeId, title: String },
React {
subject: NodeId,
content: ReactionContent,
on: bool,
},
Labels {
subject: NodeId,
add: Vec<Named>,
remove: Vec<Named>,
},
Assignees {
subject: NodeId,
add: Vec<Named>,
remove: Vec<Named>,
},
Reviewers {
pr: NodeId,
users: Vec<Named>,
teams: Vec<Named>,
bots: Vec<Named>,
},
Milestone {
pr: bool,
id: NodeId,
milestone: Option<Named>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Sidebar {
pub labels: Capped<Named>,
pub assignees: Capped<Named>,
pub milestone: Option<Named>,
pub requested: Requested,
pub author: Option<String>,
pub all_labels: Capped<Named>,
pub users: Capped<Named>,
pub milestones: Capped<Named>,
pub teams: Option<Result<Capped<Named>, String>>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Requested {
pub users: Vec<Named>,
pub teams: Vec<Named>,
pub bots: Vec<Named>,
pub left_out: u64,
}
impl GitHub {
pub async fn edit(&self, edit: &Edit) -> Result<(), ApiError> {
use cynic::MutationBuilder;
let ids = |list: &[Named]| list.iter().map(|n| n.id.gql()).collect::<Vec<_>>();
match edit {
Edit::Body { of, id, body } => {
let vars = BodyVariables {
id: id.gql(),
body: body.clone(),
};
match of {
Written::Issue => self.mutate(UpdateIssueBody::build(vars)).await.map(drop),
Written::Pr => self.mutate(UpdatePrBody::build(vars)).await.map(drop),
Written::IssueComment => {
self.mutate(UpdateIssueComment::build(vars)).await.map(drop)
}
Written::ReviewComment => self
.mutate(UpdateReviewComment::build(vars))
.await
.map(drop),
Written::Review => self.mutate(UpdateReview::build(vars)).await.map(drop),
}
}
Edit::Delete { of, id } => {
let vars = EditIdVariables { id: id.gql() };
match of {
Written::ReviewComment => self
.mutate(DeleteReviewComment::build(vars))
.await
.map(drop),
Written::Review => self.mutate(DeleteReview::build(vars)).await.map(drop),
_ => self.mutate(DeleteIssueComment::build(vars)).await.map(drop),
}
}
Edit::Title { pr, id, title } => {
let vars = TitleVariables {
id: id.gql(),
title: title.clone(),
};
if *pr {
self.mutate(UpdatePrTitle::build(vars)).await.map(drop)
} else {
self.mutate(UpdateIssueTitle::build(vars)).await.map(drop)
}
}
Edit::React {
subject,
content,
on,
} => {
let vars = ReactVariables {
subject: subject.gql(),
content: *content,
};
if *on {
self.mutate(AddReaction::build(vars)).await.map(drop)
} else {
self.mutate(RemoveReaction::build(vars)).await.map(drop)
}
}
Edit::Labels {
subject,
add,
remove,
} => {
if !add.is_empty() {
let vars = AddVariables {
subject: subject.gql(),
ids: Some(ids(add)),
};
self.mutate(AddLabels::build(vars)).await?;
}
if !remove.is_empty() {
let vars = RemoveVariables {
subject: subject.gql(),
ids: ids(remove),
};
self.mutate(RemoveLabels::build(vars)).await?;
}
Ok(())
}
Edit::Assignees {
subject,
add,
remove,
} => {
if !add.is_empty() {
let vars = AddVariables {
subject: subject.gql(),
ids: Some(ids(add)),
};
self.mutate(AddAssignees::build(vars)).await?;
}
if !remove.is_empty() {
let vars = RemoveVariables {
subject: subject.gql(),
ids: ids(remove),
};
self.mutate(RemoveAssignees::build(vars)).await?;
}
Ok(())
}
Edit::Reviewers {
pr,
users,
teams,
bots,
} => {
let op = RequestReviews::build(ReviewersVariables {
pr: pr.gql(),
users: Some(ids(users)),
teams: Some(ids(teams)),
bots: Some(ids(bots)),
});
self.mutate(op).await.map(drop)
}
Edit::Milestone { pr, id, milestone } => {
let vars = MilestoneVariables {
id: id.gql(),
milestone: milestone.as_ref().map(|m| m.id.gql()),
};
if *pr {
self.mutate(SetPrMilestone::build(vars)).await.map(drop)
} else {
self.mutate(SetIssueMilestone::build(vars)).await.map(drop)
}
}
}
}
pub async fn sidebar(&self, repo: &RepoId, number: u64) -> Result<Sidebar, ApiError> {
use cynic::QueryBuilder;
let subject = format!("{repo}#{number}");
let op = SidebarQuery::build(NumberVariables {
owner: repo.owner.clone(),
name: repo.name.clone(),
number: i32::try_from(number).map_err(|_| ApiError::NotFound(subject.clone()))?,
});
let (mut sidebar, org) = self
.find(op, subject, |q| q.repository?.into_sidebar())
.await?;
if let Some(org) = org {
let op = TeamsQuery::build(TeamsVariables { org: org.clone() });
let teams = self.find(op, &org, |q| {
let t = q.organization?.teams;
Some(Capped::from_nodes(t.total_count, t.nodes, |t| {
named(t.id, t.combined_slug)
}))
});
sidebar.teams = Some(teams.await.map_err(|err| err.to_string()));
}
Ok(sidebar)
}
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "ReactionGroup")]
pub struct WireReactionGroup {
pub content: ReactionContent,
pub viewer_has_reacted: bool,
pub reactors: ReactorCount,
}
fragments! {
total_count: i32;
ReactorCount = "ReactorConnection",
}
pub(crate) fn reactions(groups: Option<Vec<WireReactionGroup>>) -> Vec<Reaction> {
(groups.into_iter().flatten())
.map(|g| Reaction {
content: g.content,
count: count(g.reactors.total_count),
mine: g.viewer_has_reacted,
})
.filter(|r| r.count > 0 || r.mine)
.collect()
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "NumberVariables")]
pub struct SidebarQuery {
#[arguments(owner: $owner, name: $name)]
pub repository: Option<SidebarRepo>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Repository", variables = "NumberVariables")]
pub struct SidebarRepo {
#[arguments(number: $number)]
pub issue_or_pull_request: Option<SidebarOf>,
#[arguments(first: 100)]
pub labels: Option<LabelNames>,
#[arguments(first: 100)]
pub assignable_users: UserNames,
#[arguments(first: 100, states: [OPEN])]
pub milestones: Option<MilestoneNames>,
pub owner: SidebarOwner,
}
#[derive(cynic::InlineFragments, Debug)]
#[cynic(graphql_type = "IssueOrPullRequest")]
pub enum SidebarOf {
Issue(SidebarIssue),
PullRequest(SidebarPr),
#[cynic(fallback)]
Other,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Issue")]
pub struct SidebarIssue {
#[arguments(first: 100)]
pub labels: Option<LabelNames>,
#[arguments(first: 100)]
pub assignees: UserNames,
pub milestone: Option<MilestoneNamed>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "PullRequest")]
pub struct SidebarPr {
#[arguments(first: 100)]
pub labels: Option<LabelNames>,
#[arguments(first: 100)]
pub assignees: UserNames,
pub milestone: Option<MilestoneNamed>,
#[arguments(first: 100)]
pub review_requests: Option<RequestNames>,
pub author: Option<crate::api::queries::Actor>,
}
#[derive(cynic::InlineFragments, Debug)]
#[cynic(graphql_type = "RepositoryOwner")]
pub enum SidebarOwner {
Organization(OrgLogin),
#[cynic(fallback)]
Other,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Organization")]
pub struct OrgLogin {
pub login: String,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct TeamsVariables {
pub org: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "TeamsVariables")]
pub struct TeamsQuery {
#[arguments(login: $org)]
pub organization: Option<OrgTeams>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Organization")]
pub struct OrgTeams {
#[arguments(first: 100)]
pub teams: TeamNames,
}
fragments! {
counted:
LabelNames = "LabelConnection" => LabelNamed,
UserNames = "UserConnection" => UserNamed,
MilestoneNames = "MilestoneConnection" => MilestoneNamed,
TeamNames = "TeamConnection" => TeamNamed,
RequestNames = "ReviewRequestConnection" => RequestNamed,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Label")]
pub struct LabelNamed {
pub id: cynic::Id,
pub name: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "User")]
pub struct UserNamed {
pub id: cynic::Id,
pub login: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Bot")]
pub struct BotNamed {
pub id: cynic::Id,
pub login: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Milestone")]
pub struct MilestoneNamed {
pub id: cynic::Id,
pub title: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Team")]
pub struct TeamNamed {
pub id: cynic::Id,
pub combined_slug: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "ReviewRequest")]
pub struct RequestNamed {
pub requested_reviewer: Option<RequestedNamed>,
}
#[derive(cynic::InlineFragments, Debug)]
#[cynic(graphql_type = "RequestedReviewer")]
pub enum RequestedNamed {
User(UserNamed),
Bot(BotNamed),
Team(TeamNamed),
#[cynic(fallback)]
Other,
}
fn named(id: cynic::Id, name: String) -> Named {
Named {
id: id.into(),
name,
}
}
fn label_names(list: Option<LabelNames>) -> Capped<Named> {
list.map_or_else(Capped::default, |l| {
Capped::from_nodes(l.total_count, l.nodes, |l| named(l.id, l.name))
})
}
fn user_names(list: UserNames) -> Capped<Named> {
Capped::from_nodes(list.total_count, list.nodes, |u| named(u.id, u.login))
}
impl SidebarRepo {
fn into_sidebar(self) -> Option<(Sidebar, Option<String>)> {
let (labels, assignees, milestone, requests, author) = match self.issue_or_pull_request? {
SidebarOf::Issue(i) => (i.labels, i.assignees, i.milestone, None, None),
SidebarOf::PullRequest(p) => (
p.labels,
p.assignees,
p.milestone,
p.review_requests,
Some(crate::api::model::author(p.author)),
),
SidebarOf::Other => return None,
};
let mut requested = Requested::default();
if let Some(r) = requests {
let mut shown = 0;
for asked in nodes(r.nodes).filter_map(|r| r.requested_reviewer) {
shown += 1;
match asked {
RequestedNamed::User(u) => requested.users.push(named(u.id, u.login)),
RequestedNamed::Bot(b) => requested.bots.push(named(b.id, b.login)),
RequestedNamed::Team(t) => requested.teams.push(named(t.id, t.combined_slug)),
RequestedNamed::Other => shown -= 1,
}
}
requested.left_out = count(r.total_count).saturating_sub(shown);
}
let org = match self.owner {
SidebarOwner::Organization(o) if author.is_some() => Some(o.login),
_ => None,
};
let sidebar = Sidebar {
labels: label_names(labels),
assignees: user_names(assignees),
milestone: milestone.map(|m| named(m.id, m.title)),
requested,
author,
all_labels: label_names(self.labels),
users: user_names(self.assignable_users),
milestones: self.milestones.map_or_else(Capped::default, |m| {
Capped::from_nodes(m.total_count, m.nodes, |m| named(m.id, m.title))
}),
teams: None,
};
Some((sidebar, org))
}
}
fragments! {
client_mutation_id: Option<String>;
UpdateIssuePayload = "UpdateIssuePayload",
UpdatePrPayload = "UpdatePullRequestPayload",
UpdateIssueCommentPayload = "UpdateIssueCommentPayload",
UpdateReviewCommentPayload = "UpdatePullRequestReviewCommentPayload",
DeleteIssueCommentPayload = "DeleteIssueCommentPayload",
DeleteReviewCommentPayload = "DeletePullRequestReviewCommentPayload",
UpdateReviewPayload = "UpdatePullRequestReviewPayload",
DeleteReviewPayload = "DeletePullRequestReviewPayload",
AddReactionPayload = "AddReactionPayload",
RemoveReactionPayload = "RemoveReactionPayload",
AddLabelsPayload = "AddLabelsToLabelablePayload",
RemoveLabelsPayload = "RemoveLabelsFromLabelablePayload",
AddAssigneesPayload = "AddAssigneesToAssignablePayload",
RemoveAssigneesPayload = "RemoveAssigneesFromAssignablePayload",
RequestReviewsPayload = "RequestReviewsPayload",
}
#[derive(cynic::QueryVariables, Debug)]
pub struct EditIdVariables {
pub id: cynic::Id,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct BodyVariables {
pub id: cynic::Id,
pub body: String,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct TitleVariables {
pub id: cynic::Id,
pub title: String,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct ReactVariables {
pub subject: cynic::Id,
pub content: ReactionContent,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct AddVariables {
pub subject: cynic::Id,
pub ids: Option<Vec<cynic::Id>>,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct RemoveVariables {
pub subject: cynic::Id,
pub ids: Vec<cynic::Id>,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct ReviewersVariables {
pub pr: cynic::Id,
pub users: Option<Vec<cynic::Id>>,
pub teams: Option<Vec<cynic::Id>>,
pub bots: Option<Vec<cynic::Id>>,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct MilestoneVariables {
pub id: cynic::Id,
pub milestone: Option<cynic::Id>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "BodyVariables")]
pub struct UpdateIssueBody {
#[arguments(input: { id: $id, body: $body })]
pub update_issue: Option<UpdateIssuePayload>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "BodyVariables")]
pub struct UpdatePrBody {
#[arguments(input: { pullRequestId: $id, body: $body })]
pub update_pull_request: Option<UpdatePrPayload>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "BodyVariables")]
pub struct UpdateIssueComment {
#[arguments(input: { id: $id, body: $body })]
pub update_issue_comment: Option<UpdateIssueCommentPayload>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "BodyVariables")]
pub struct UpdateReviewComment {
#[arguments(input: { pullRequestReviewCommentId: $id, body: $body })]
pub update_pull_request_review_comment: Option<UpdateReviewCommentPayload>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "EditIdVariables")]
pub struct DeleteIssueComment {
#[arguments(input: { id: $id })]
pub delete_issue_comment: Option<DeleteIssueCommentPayload>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "EditIdVariables")]
pub struct DeleteReviewComment {
#[arguments(input: { id: $id })]
pub delete_pull_request_review_comment: Option<DeleteReviewCommentPayload>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "BodyVariables")]
pub struct UpdateReview {
#[arguments(input: { pullRequestReviewId: $id, body: $body })]
pub update_pull_request_review: Option<UpdateReviewPayload>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "EditIdVariables")]
pub struct DeleteReview {
#[arguments(input: { pullRequestReviewId: $id })]
pub delete_pull_request_review: Option<DeleteReviewPayload>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "TitleVariables")]
pub struct UpdateIssueTitle {
#[arguments(input: { id: $id, title: $title })]
pub update_issue: Option<UpdateIssuePayload>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "TitleVariables")]
pub struct UpdatePrTitle {
#[arguments(input: { pullRequestId: $id, title: $title })]
pub update_pull_request: Option<UpdatePrPayload>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "ReactVariables")]
pub struct AddReaction {
#[arguments(input: { subjectId: $subject, content: $content })]
pub add_reaction: Option<AddReactionPayload>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "ReactVariables")]
pub struct RemoveReaction {
#[arguments(input: { subjectId: $subject, content: $content })]
pub remove_reaction: Option<RemoveReactionPayload>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "AddVariables")]
pub struct AddLabels {
#[arguments(input: { labelableId: $subject, labelIds: $ids })]
pub add_labels_to_labelable: Option<AddLabelsPayload>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "RemoveVariables")]
pub struct RemoveLabels {
#[arguments(input: { labelableId: $subject, labelIds: $ids })]
pub remove_labels_from_labelable: Option<RemoveLabelsPayload>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "AddVariables")]
pub struct AddAssignees {
#[arguments(input: { assignableId: $subject, assigneeIds: $ids })]
pub add_assignees_to_assignable: Option<AddAssigneesPayload>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "RemoveVariables")]
pub struct RemoveAssignees {
#[arguments(input: { assignableId: $subject, assigneeIds: $ids })]
pub remove_assignees_from_assignable: Option<RemoveAssigneesPayload>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "ReviewersVariables")]
pub struct RequestReviews {
#[arguments(input: { pullRequestId: $pr, userIds: $users, teamIds: $teams, botIds: $bots })]
pub request_reviews: Option<RequestReviewsPayload>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "MilestoneVariables")]
pub struct SetIssueMilestone {
#[arguments(input: { id: $id, milestoneId: $milestone })]
pub update_issue: Option<UpdateIssuePayload>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "MilestoneVariables")]
pub struct SetPrMilestone {
#[arguments(input: { pullRequestId: $id, milestoneId: $milestone })]
pub update_pull_request: Option<UpdatePrPayload>,
}