diff --git a/appview/issues/issues.go b/appview/issues/issues.go index 028052fd..bbe1036b 100644 --- a/appview/issues/issues.go +++ b/appview/issues/issues.go @@ -402,56 +402,38 @@ func (rp *Issues) EditIssueComment(w http.ResponseWriter, r *http.Request) { } // rkey is optional, it was introduced later - if comment.Rkey != "" { + if newComment.Rkey != "" { // update the record on pds - ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoIssueCommentNSID, user.Did, rkey) + ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoIssueCommentNSID, user.Did, comment.Rkey) if err != nil { - // failed to get record - log.Println(err, rkey) + log.Println("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey) rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.") return } - value, _ := ex.Value.MarshalJSON() // we just did get record; it is valid json - record, _ := data.UnmarshalJSON(value) - - repoAt := record["repo"].(string) - issueAt := record["issue"].(string) - createdAt := record["createdAt"].(string) _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ Collection: tangled.RepoIssueCommentNSID, Repo: user.Did, - Rkey: rkey, + Rkey: newComment.Rkey, SwapRecord: ex.Cid, Record: &lexutil.LexiconTypeDecoder{ - Val: &tangled.RepoIssueComment{ - Repo: &repoAt, - Issue: issueAt, - Owner: &comment.OwnerDid, - Body: newBody, - CreatedAt: createdAt, - }, + Val: &record, }, }) if err != nil { - log.Println(err) + l.Error("failed to update record on PDS", "err", err) } } - // optimistic update for htmx - comment.Body = newBody - comment.Edited = &edited - // return new comment body with htmx - rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ + rp.pages.IssueCommentBodyFragment(w, pages.IssueCommentBodyParams{ LoggedInUser: user, RepoInfo: f.RepoInfo(user), Issue: issue, - Comment: comment, + Comment: &newComment, }) - return - } +} func (rp *Issues) ReplyIssueCommentPlaceholder(w http.ResponseWriter, r *http.Request) { l := rp.logger.With("handler", "ReplyIssueCommentPlaceholder") @@ -540,39 +522,36 @@ func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { user := rp.oauth.GetUser(r) f, err := rp.repoResolver.Resolve(r) if err != nil { + l.Error("failed to get repo and knot", "err", err) return } - issueId := chi.URLParam(r, "issue") - issueIdInt, err := strconv.Atoi(issueId) - if err != nil { - http.Error(w, "bad issue id", http.StatusBadRequest) - log.Println("failed to parse issue id", err) - return - } - - issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) - if err != nil { - log.Println("failed to get issue", err) - rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") + issue, ok := r.Context().Value("issue").(*db.Issue) + if !ok { + l.Error("failed to get issue") + rp.pages.Error404(w) return } - commentId := chi.URLParam(r, "comment_id") - commentIdInt, err := strconv.Atoi(commentId) + commentId := chi.URLParam(r, "commentId") + comments, err := db.GetIssueComments( + rp.db, + db.FilterEq("id", commentId), + ) if err != nil { - http.Error(w, "bad comment id", http.StatusBadRequest) - log.Println("failed to parse issue id", err) + l.Error("failed to fetch comment", "id", commentId) + http.Error(w, "failed to fetch comment id", http.StatusBadRequest) return } - - comment, err := db.GetComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt) - if err != nil { - http.Error(w, "bad comment id", http.StatusBadRequest) + if len(comments) != 1 { + l.Error("incorrect number of comments returned", "id", commentId, "len(comments)", len(comments)) + http.Error(w, "invalid comment id", http.StatusBadRequest) return } + comment := comments[0] - if comment.OwnerDid != user.Did { + if comment.Did != user.Did { + l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Did) http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) return } @@ -584,9 +563,9 @@ func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { // optimistic deletion deleted := time.Now() - err = db.DeleteComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt) + err = db.DeleteIssueComments(rp.db, db.FilterEq("id", comment.Id)) if err != nil { - log.Println("failed to delete comment") + l.Error("failed to delete comment", "err", err) rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") return } @@ -614,11 +593,11 @@ func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { comment.Deleted = &deleted // htmx fragment of comment after deletion - rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ + rp.pages.IssueCommentBodyFragment(w, pages.IssueCommentBodyParams{ LoggedInUser: user, RepoInfo: f.RepoInfo(user), Issue: issue, - Comment: comment, + Comment: &comment, }) } @@ -648,7 +627,16 @@ func (rp *Issues) RepoIssues(w http.ResponseWriter, r *http.Request) { return } - issues, err := db.GetIssuesPaginated(rp.db, f.RepoAt(), isOpen, page) + openVal := 0 + if isOpen { + openVal = 1 + } + issues, err := db.GetIssuesPaginated( + rp.db, + page, + db.FilterEq("repo_at", f.RepoAt()), + db.FilterEq("open", openVal), + ) if err != nil { log.Println("failed to get issues", err) rp.pages.Notice(w, "issues", "Failed to load issues. Try again later.") @@ -665,11 +653,12 @@ func (rp *Issues) RepoIssues(w http.ResponseWriter, r *http.Request) { } func (rp *Issues) NewIssue(w http.ResponseWriter, r *http.Request) { + l := rp.logger.With("handler", "NewIssue") user := rp.oauth.GetUser(r) f, err := rp.repoResolver.Resolve(r) if err != nil { - log.Println("failed to get repo and knot", err) + l.Error("failed to get repo and knot", "err", err) return } @@ -698,54 +687,96 @@ func (rp *Issues) NewIssue(w http.ResponseWriter, r *http.Request) { return } - tx, err := rp.db.BeginTx(r.Context(), nil) - if err != nil { - rp.pages.Notice(w, "issues", "Failed to create issue, try again later") - return - } - issue := &db.Issue{ - RepoAt: f.RepoAt(), - Rkey: tid.TID(), - Title: title, - Body: body, - OwnerDid: user.Did, - } - err = db.NewIssue(tx, issue) - if err != nil { - log.Println("failed to create issue", err) - rp.pages.Notice(w, "issues", "Failed to create issue.") - return + RepoAt: f.RepoAt(), + Rkey: tid.TID(), + Title: title, + Body: body, + Did: user.Did, + Created: time.Now(), } + record := issue.AsRecord() + // create an atproto record client, err := rp.oauth.AuthorizedClient(r) if err != nil { - log.Println("failed to get authorized client", err) + l.Error("failed to get authorized client", "err", err) rp.pages.Notice(w, "issues", "Failed to create issue.") return } - atUri := f.RepoAt().String() - _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ + resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ Collection: tangled.RepoIssueNSID, Repo: user.Did, Rkey: issue.Rkey, Record: &lexutil.LexiconTypeDecoder{ - Val: &tangled.RepoIssue{ - Repo: atUri, - Title: title, - Body: &body, - }, + Val: &record, }, }) + if err != nil { + l.Error("failed to create issue", "err", err) + rp.pages.Notice(w, "issues", "Failed to create issue.") + return + } + atUri := resp.Uri + + tx, err := rp.db.BeginTx(r.Context(), nil) + if err != nil { + rp.pages.Notice(w, "issues", "Failed to create issue, try again later") + return + } + rollback := func() { + err1 := tx.Rollback() + err2 := rollbackRecord(context.Background(), atUri, client) + + if errors.Is(err1, sql.ErrTxDone) { + err1 = nil + } + + if err := errors.Join(err1, err2); err != nil { + l.Error("failed to rollback txn", "err", err) + } + } + defer rollback() + + err = db.NewIssue(tx, issue) if err != nil { log.Println("failed to create issue", err) rp.pages.Notice(w, "issues", "Failed to create issue.") return } - rp.notifier.NewIssue(r.Context(), issue) + if err = tx.Commit(); err != nil { + log.Println("failed to create issue", err) + rp.pages.Notice(w, "issues", "Failed to create issue.") + return + } + // everything is successful, do not rollback the atproto record + atUri = "" + rp.notifier.NewIssue(r.Context(), issue) rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issue.IssueId)) return } } + +// this is used to rollback changes made to the PDS +// +// it is a no-op if the provided ATURI is empty +func rollbackRecord(ctx context.Context, aturi string, xrpcc *xrpcclient.Client) error { + if aturi == "" { + return nil + } + + parsed := syntax.ATURI(aturi) + + collection := parsed.Collection().String() + repo := parsed.Authority().String() + rkey := parsed.RecordKey().String() + + _, err := xrpcc.RepoDeleteRecord(ctx, &comatproto.RepoDeleteRecord_Input{ + Collection: collection, + Repo: repo, + Rkey: rkey, + }) + return err +} diff --git a/appview/pages/pages.go b/appview/pages/pages.go index 187cace0..60d55b89 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -909,7 +909,7 @@ type RepoSingleIssueParams struct { RepoInfo repoinfo.RepoInfo Active string Issue *db.Issue - Comments []db.Comment + CommentList []db.CommentListItem IssueOwnerHandle string OrderedReactionKinds []db.ReactionKind @@ -955,22 +955,44 @@ type EditIssueCommentParams struct { LoggedInUser *oauth.User RepoInfo repoinfo.RepoInfo Issue *db.Issue - Comment *db.Comment + Comment *db.IssueComment } func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { return p.executePlain("repo/issues/fragments/editIssueComment", w, params) } -type SingleIssueCommentParams struct { +type ReplyIssueCommentPlaceholderParams struct { LoggedInUser *oauth.User RepoInfo repoinfo.RepoInfo Issue *db.Issue - Comment *db.Comment + Comment *db.IssueComment } -func (p *Pages) SingleIssueCommentFragment(w io.Writer, params SingleIssueCommentParams) error { - return p.executePlain("repo/issues/fragments/issueComment", w, params) +func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { + return p.executePlain("repo/issues/fragments/replyIssueCommentPlaceholder", w, params) +} + +type ReplyIssueCommentParams struct { + LoggedInUser *oauth.User + RepoInfo repoinfo.RepoInfo + Issue *db.Issue + Comment *db.IssueComment +} + +func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { + return p.executePlain("repo/issues/fragments/replyComment", w, params) +} + +type IssueCommentBodyParams struct { + LoggedInUser *oauth.User + RepoInfo repoinfo.RepoInfo + Issue *db.Issue + Comment *db.IssueComment +} + +func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error { + return p.executePlain("repo/issues/fragments/issueCommentBody", w, params) } type RepoNewPullParams struct { diff --git a/appview/pages/templates/repo/issues/fragments/issueCommentActions.html b/appview/pages/templates/repo/issues/fragments/issueCommentActions.html new file mode 100644 index 00000000..6b2a577c --- /dev/null +++ b/appview/pages/templates/repo/issues/fragments/issueCommentActions.html @@ -0,0 +1,34 @@ +{{ define "repo/issues/fragments/issueCommentActions" }} + {{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did) }} + {{ if and $isCommentOwner (not .Comment.Deleted) }} +
+ {{ template "edit" . }} + {{ template "delete" . }} +
+ {{ end }} +{{ end }} + +{{ define "edit" }} + + {{ i "pencil" "size-3" }} + edit + +{{ end }} + +{{ define "delete" }} + + {{ i "trash-2" "size-3" }} + delete + {{ i "loader-circle" "size-3 animate-spin hidden group-[.htmx-request]:inline" }} + +{{ end }} diff --git a/appview/pages/templates/repo/issues/fragments/replyComment.html b/appview/pages/templates/repo/issues/fragments/replyComment.html new file mode 100644 index 00000000..7d0e8b2b --- /dev/null +++ b/appview/pages/templates/repo/issues/fragments/replyComment.html @@ -0,0 +1,57 @@ +{{ define "repo/issues/fragments/replyComment" }} +
+ {{ template "user/fragments/picHandleLink" .LoggedInUser.Did }} + + + + {{ template "replyActions" . }} +
+{{ end }} + +{{ define "replyActions" }} +
+ {{ template "cancel" . }} + {{ template "reply" . }} +
+{{ end }} + +{{ define "cancel" }} + +{{ end }} + +{{ define "reply" }} + +{{ end }} diff --git a/appview/pages/templates/repo/issues/issue.html b/appview/pages/templates/repo/issues/issue.html index ed714233..8ef2545d 100644 --- a/appview/pages/templates/repo/issues/issue.html +++ b/appview/pages/templates/repo/issues/issue.html @@ -32,8 +32,7 @@ opened by - {{ $owner := didOrHandle .Issue.OwnerDid .IssueOwnerHandle }} - {{ template "user/fragments/picHandleLink" $owner }} + {{ template "user/fragments/picHandleLink" .Issue.Did }} {{ template "repo/fragments/time" .Issue.Created }} @@ -62,164 +61,17 @@ {{ end }} {{ define "repoAfter" }} -
- {{ range $index, $comment := .Comments }} -
- {{ if gt $index 0 }} -
- {{ end }} - {{ template "repo/issues/fragments/issueComment" (dict "RepoInfo" $.RepoInfo "LoggedInUser" $.LoggedInUser "Issue" $.Issue "Comment" .)}} -
- {{ end }} -
- - {{ block "newComment" . }} {{ end }} - +
+ {{ + template "repo/issues/fragments/commentList" + (dict + "RepoInfo" $.RepoInfo + "LoggedInUser" $.LoggedInUser + "Issue" $.Issue + "CommentList" $.Issue.CommentList) + }} + + {{ template "repo/issues/fragments/newComment" . }} +
{{ end }} -{{ define "newComment" }} - {{ if .LoggedInUser }} -
-
-
- {{ template "user/fragments/picHandleLink" (didOrHandle .LoggedInUser.Did .LoggedInUser.Handle) }} -
- -
-
-
- -
- - - {{ $isIssueAuthor := and .LoggedInUser (eq .LoggedInUser.Did .Issue.OwnerDid) }} - {{ $isRepoCollaborator := .RepoInfo.Roles.IsCollaborator }} - {{ $isRepoOwner := .RepoInfo.Roles.IsOwner }} - {{ if and (or $isIssueAuthor $isRepoCollaborator $isRepoOwner) (eq .State "open") }} - -
-
-
-
- - {{ else if and (or $isIssueAuthor $isRepoCollaborator $isRepoOwner) (eq .State "closed") }} - - {{ end }} - - -
-
- {{ else }} -
- login to join the discussion -
- {{ end }} -{{ end }} diff --git a/appview/pages/templates/repo/issues/issues.html b/appview/pages/templates/repo/issues/issues.html index b4532b97..d76a64fd 100644 --- a/appview/pages/templates/repo/issues/issues.html +++ b/appview/pages/templates/repo/issues/issues.html @@ -37,55 +37,53 @@ {{ end }} {{ define "repoAfter" }} -
- {{ range .Issues }} -
- -

- {{ $bgColor := "bg-gray-800 dark:bg-gray-700" }} - {{ $icon := "ban" }} - {{ $state := "closed" }} - {{ if .Open }} - {{ $bgColor = "bg-green-600 dark:bg-green-700" }} - {{ $icon = "circle-dot" }} - {{ $state = "open" }} - {{ end }} +

+ {{ range .Issues }} +
+ +

+ {{ $bgColor := "bg-gray-800 dark:bg-gray-700" }} + {{ $icon := "ban" }} + {{ $state := "closed" }} + {{ if .Open }} + {{ $bgColor = "bg-green-600 dark:bg-green-700" }} + {{ $icon = "circle-dot" }} + {{ $state = "open" }} + {{ end }} - - {{ i $icon "w-3 h-3 mr-1.5 text-white dark:text-white" }} - {{ $state }} - + + {{ i $icon "w-3 h-3 mr-1.5 text-white dark:text-white" }} + {{ $state }} + - - {{ template "user/fragments/picHandleLink" .OwnerDid }} - + + {{ template "user/fragments/picHandleLink" .Did }} + - - {{ template "repo/fragments/time" .Created }} - + + {{ template "repo/fragments/time" .Created }} + - - {{ $s := "s" }} - {{ if eq .Metadata.CommentCount 1 }} - {{ $s = "" }} - {{ end }} - {{ .Metadata.CommentCount }} comment{{$s}} - -

+ + {{ $s := "s" }} + {{ if eq (len .Comments) 1 }} + {{ $s = "" }} + {{ end }} + {{ len .Comments }} comment{{$s}} + +

+
+ {{ end }}
- {{ end }} -
- -{{ block "pagination" . }} {{ end }} - + {{ block "pagination" . }} {{ end }} {{ end }} {{ define "pagination" }} diff --git a/appview/pages/templates/repo/issues/new.html b/appview/pages/templates/repo/issues/new.html index b3474856..72020a8e 100644 --- a/appview/pages/templates/repo/issues/new.html +++ b/appview/pages/templates/repo/issues/new.html @@ -3,7 +3,7 @@ {{ define "repoContent" }}
@@ -26,7 +26,7 @@