Add issue comment editing
This commit is contained in:
@@ -466,14 +466,16 @@ pub async fn load_issue(
|
||||
let client =
|
||||
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
|
||||
let configuration = client.configuration();
|
||||
let (issue, comments) = tokio::join!(
|
||||
let (issue, comments, viewer) = tokio::join!(
|
||||
apis::issue_api::issue_get_issue(&configuration, owner, repository, number),
|
||||
load_issue_comments(&configuration, owner, repository, number, page),
|
||||
client.current_user(),
|
||||
);
|
||||
Ok(IssueDetails {
|
||||
issue: issue.map_err(|error| error.to_string())?,
|
||||
has_more: false,
|
||||
comments: comments?,
|
||||
viewer_id: viewer.map_err(|error| error.to_string())?.id,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -492,6 +494,67 @@ async fn load_issue_comments(
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
pub async fn save_issue_comment(
|
||||
server: &Server,
|
||||
owner: &str,
|
||||
repository: &str,
|
||||
number: i64,
|
||||
comment_id: Option<i64>,
|
||||
body: String,
|
||||
) -> Result<(), String> {
|
||||
let client =
|
||||
Client::new(&server.url, Some(&server.token)).map_err(|error| error.to_string())?;
|
||||
let configuration = client.configuration();
|
||||
match comment_id {
|
||||
Some(id) => {
|
||||
let (comment, viewer) = tokio::join!(
|
||||
apis::issue_api::issue_get_comment(&configuration, owner, repository, id),
|
||||
client.current_user(),
|
||||
);
|
||||
let comment = comment.map_err(|error| error.to_string())?;
|
||||
let viewer = viewer.map_err(|error| error.to_string())?;
|
||||
let owned = matches!(
|
||||
(comment.user.as_ref().and_then(|user| user.id), viewer.id),
|
||||
(Some(author), Some(viewer)) if author == viewer
|
||||
);
|
||||
if !owned || !comment_belongs_to_issue(&comment, number) {
|
||||
return Err("You can only edit your own comments.".into());
|
||||
}
|
||||
apis::issue_api::issue_edit_comment(
|
||||
&configuration,
|
||||
owner,
|
||||
repository,
|
||||
id,
|
||||
Some(models::EditIssueCommentOption::new(body)),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| error.to_string())?;
|
||||
}
|
||||
None => {
|
||||
apis::issue_api::issue_create_comment(
|
||||
&configuration,
|
||||
owner,
|
||||
repository,
|
||||
number,
|
||||
Some(models::CreateIssueCommentOption::new(body)),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| error.to_string())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn comment_belongs_to_issue(comment: &models::Comment, number: i64) -> bool {
|
||||
comment
|
||||
.issue_url
|
||||
.as_deref()
|
||||
.map(|url| url.trim_end_matches('/'))
|
||||
.and_then(|url| url.rsplit('/').next())
|
||||
.and_then(|index| index.parse().ok())
|
||||
== Some(number)
|
||||
}
|
||||
|
||||
pub async fn load_issue_editor(
|
||||
server: &Server,
|
||||
owner: &str,
|
||||
@@ -1041,4 +1104,15 @@ mod tests {
|
||||
assert_eq!(edit.milestone, Some(0));
|
||||
assert_eq!(edit.unset_due_date, Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scopes_comment_edits_to_the_selected_issue() {
|
||||
let comment = models::Comment {
|
||||
issue_url: Some("https://gitea.example/api/v1/repos/o/r/issues/4/".into()),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(comment_belongs_to_issue(&comment, 4));
|
||||
assert!(!comment_belongs_to_issue(&comment, 5));
|
||||
assert!(!comment_belongs_to_issue(&models::Comment::default(), 4));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +154,7 @@ pub struct HomeData {
|
||||
pub struct IssueDetails {
|
||||
pub issue: models::Issue,
|
||||
pub comments: Vec<models::Comment>,
|
||||
pub viewer_id: Option<i64>,
|
||||
pub has_more: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -353,6 +353,25 @@ impl GotchaCore {
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn save_issue_comment(
|
||||
&self,
|
||||
owner: String,
|
||||
repository: String,
|
||||
number: i64,
|
||||
comment_id: Option<i64>,
|
||||
body: String,
|
||||
) -> Result<(), GotchaError> {
|
||||
let (owner, repository) = validate_repository(&owner, &repository)?;
|
||||
if number <= 0 || comment_id.is_some_and(|id| id <= 0) {
|
||||
return Err("Invalid issue comment selection.".into());
|
||||
}
|
||||
if body.trim().is_empty() {
|
||||
return Err("Enter a comment.".into());
|
||||
}
|
||||
save_issue_comment(&self.server()?, owner, repository, number, comment_id, body).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn issue_editor(
|
||||
&self,
|
||||
owner: String,
|
||||
|
||||
@@ -64,9 +64,11 @@ pub struct PullRow {
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
pub struct CommentRow {
|
||||
pub id: i64,
|
||||
pub author: String,
|
||||
pub body: String,
|
||||
pub meta: String,
|
||||
pub can_edit: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, uniffi::Record)]
|
||||
@@ -514,7 +516,11 @@ pub fn issue_page(details: IssueDetails) -> IssuePage {
|
||||
.body
|
||||
.filter(|body| !body.is_empty())
|
||||
.unwrap_or_else(|| "No description provided.".into()),
|
||||
comments: details.comments.iter().map(comment_row).collect(),
|
||||
comments: details
|
||||
.comments
|
||||
.iter()
|
||||
.map(|comment| comment_row(comment, details.viewer_id))
|
||||
.collect(),
|
||||
has_more: details.has_more,
|
||||
}
|
||||
}
|
||||
@@ -627,7 +633,11 @@ pub fn pull_page(details: PullDetails) -> PullPage {
|
||||
.unwrap_or_else(|| "No description provided.".into()),
|
||||
files_ref: pull_files_ref(pull),
|
||||
files: details.files.iter().filter_map(file_row).collect(),
|
||||
comments: details.comments.iter().map(comment_row).collect(),
|
||||
comments: details
|
||||
.comments
|
||||
.iter()
|
||||
.map(|comment| comment_row(comment, None))
|
||||
.collect(),
|
||||
has_more: details.has_more,
|
||||
}
|
||||
}
|
||||
@@ -985,8 +995,9 @@ fn label_row(label: &models::Label) -> Option<LabelRow> {
|
||||
})
|
||||
}
|
||||
|
||||
fn comment_row(comment: &models::Comment) -> CommentRow {
|
||||
fn comment_row(comment: &models::Comment, viewer_id: Option<i64>) -> CommentRow {
|
||||
CommentRow {
|
||||
id: comment.id.unwrap_or_default(),
|
||||
author: comment
|
||||
.user
|
||||
.as_ref()
|
||||
@@ -1005,6 +1016,14 @@ fn comment_row(comment: &models::Comment) -> CommentRow {
|
||||
.as_deref()
|
||||
.or(comment.created_at.as_deref()),
|
||||
),
|
||||
can_edit: matches!(
|
||||
(
|
||||
comment.id,
|
||||
comment.user.as_ref().and_then(|user| user.id),
|
||||
viewer_id,
|
||||
),
|
||||
(Some(_), Some(author), Some(viewer)) if author == viewer
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1264,17 +1283,41 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_page_exposes_state() {
|
||||
fn issue_page_exposes_state_and_owned_comment_editing() {
|
||||
let page = issue_page(IssueDetails {
|
||||
issue: models::Issue {
|
||||
state: Some("closed".into()),
|
||||
..Default::default()
|
||||
},
|
||||
comments: Vec::new(),
|
||||
comments: vec![
|
||||
models::Comment {
|
||||
id: Some(7),
|
||||
user: Some(Box::new(models::User {
|
||||
id: Some(3),
|
||||
login: Some("viewer".into()),
|
||||
..Default::default()
|
||||
})),
|
||||
body: Some("Comment".into()),
|
||||
..Default::default()
|
||||
},
|
||||
models::Comment {
|
||||
id: Some(8),
|
||||
user: Some(Box::new(models::User {
|
||||
id: Some(4),
|
||||
login: Some("someone-else".into()),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
viewer_id: Some(3),
|
||||
has_more: false,
|
||||
});
|
||||
|
||||
assert_eq!(page.state, "closed");
|
||||
assert_eq!(page.comments[0].id, 7);
|
||||
assert!(page.comments[0].can_edit);
|
||||
assert!(!page.comments[1].can_edit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user