forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

appview/issues: rework issues to be better (tm)

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li de63011a 4923b1da

verified
Changed files
+286 -292
appview
+111 -80
appview/issues/issues.go
··· 402 402 } 403 403 404 404 // rkey is optional, it was introduced later 405 - if comment.Rkey != "" { 405 + if newComment.Rkey != "" { 406 406 // update the record on pds 407 - ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoIssueCommentNSID, user.Did, rkey) 407 + ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoIssueCommentNSID, user.Did, comment.Rkey) 408 408 if err != nil { 409 - // failed to get record 410 - log.Println(err, rkey) 409 + log.Println("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey) 411 410 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.") 412 411 return 413 412 } 414 - value, _ := ex.Value.MarshalJSON() // we just did get record; it is valid json 415 - record, _ := data.UnmarshalJSON(value) 416 - 417 - repoAt := record["repo"].(string) 418 - issueAt := record["issue"].(string) 419 - createdAt := record["createdAt"].(string) 420 413 421 414 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 422 415 Collection: tangled.RepoIssueCommentNSID, 423 416 Repo: user.Did, 424 - Rkey: rkey, 417 + Rkey: newComment.Rkey, 425 418 SwapRecord: ex.Cid, 426 419 Record: &lexutil.LexiconTypeDecoder{ 427 - Val: &tangled.RepoIssueComment{ 428 - Repo: &repoAt, 429 - Issue: issueAt, 430 - Owner: &comment.OwnerDid, 431 - Body: newBody, 432 - CreatedAt: createdAt, 433 - }, 420 + Val: &record, 434 421 }, 435 422 }) 436 423 if err != nil { 437 - log.Println(err) 424 + l.Error("failed to update record on PDS", "err", err) 438 425 } 439 426 } 440 427 441 - // optimistic update for htmx 442 - comment.Body = newBody 443 - comment.Edited = &edited 444 - 445 428 // return new comment body with htmx 446 - rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 429 + rp.pages.IssueCommentBodyFragment(w, pages.IssueCommentBodyParams{ 447 430 LoggedInUser: user, 448 431 RepoInfo: f.RepoInfo(user), 449 432 Issue: issue, 450 - Comment: comment, 433 + Comment: &newComment, 451 434 }) 452 - return 453 - 454 435 } 436 + } 455 437 456 438 func (rp *Issues) ReplyIssueCommentPlaceholder(w http.ResponseWriter, r *http.Request) { 457 439 l := rp.logger.With("handler", "ReplyIssueCommentPlaceholder") ··· 540 522 user := rp.oauth.GetUser(r) 541 523 f, err := rp.repoResolver.Resolve(r) 542 524 if err != nil { 543 - return 544 - } 545 - 546 - issueId := chi.URLParam(r, "issue") 547 - issueIdInt, err := strconv.Atoi(issueId) 548 - if err != nil { 549 - http.Error(w, "bad issue id", http.StatusBadRequest) 550 - log.Println("failed to parse issue id", err) 525 + l.Error("failed to get repo and knot", "err", err) 551 526 return 552 527 } 553 528 554 - issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt) 555 - if err != nil { 556 - log.Println("failed to get issue", err) 557 - rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 529 + issue, ok := r.Context().Value("issue").(*db.Issue) 530 + if !ok { 531 + l.Error("failed to get issue") 532 + rp.pages.Error404(w) 558 533 return 559 534 } 560 535 561 - commentId := chi.URLParam(r, "comment_id") 562 - commentIdInt, err := strconv.Atoi(commentId) 536 + commentId := chi.URLParam(r, "commentId") 537 + comments, err := db.GetIssueComments( 538 + rp.db, 539 + db.FilterEq("id", commentId), 540 + ) 563 541 if err != nil { 564 - http.Error(w, "bad comment id", http.StatusBadRequest) 565 - log.Println("failed to parse issue id", err) 542 + l.Error("failed to fetch comment", "id", commentId) 543 + http.Error(w, "failed to fetch comment id", http.StatusBadRequest) 566 544 return 567 545 } 568 - 569 - comment, err := db.GetComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt) 570 - if err != nil { 571 - http.Error(w, "bad comment id", http.StatusBadRequest) 546 + if len(comments) != 1 { 547 + l.Error("incorrect number of comments returned", "id", commentId, "len(comments)", len(comments)) 548 + http.Error(w, "invalid comment id", http.StatusBadRequest) 572 549 return 573 550 } 551 + comment := comments[0] 574 552 575 - if comment.OwnerDid != user.Did { 553 + if comment.Did != user.Did { 554 + l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Did) 576 555 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 577 556 return 578 557 } ··· 584 563 585 564 // optimistic deletion 586 565 deleted := time.Now() 587 - err = db.DeleteComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt) 566 + err = db.DeleteIssueComments(rp.db, db.FilterEq("id", comment.Id)) 588 567 if err != nil { 589 - log.Println("failed to delete comment") 568 + l.Error("failed to delete comment", "err", err) 590 569 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") 591 570 return 592 571 } ··· 614 593 comment.Deleted = &deleted 615 594 616 595 // htmx fragment of comment after deletion 617 - rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 596 + rp.pages.IssueCommentBodyFragment(w, pages.IssueCommentBodyParams{ 618 597 LoggedInUser: user, 619 598 RepoInfo: f.RepoInfo(user), 620 599 Issue: issue, 621 - Comment: comment, 600 + Comment: &comment, 622 601 }) 623 602 } 624 603 ··· 648 627 return 649 628 } 650 629 651 - issues, err := db.GetIssuesPaginated(rp.db, f.RepoAt(), isOpen, page) 630 + openVal := 0 631 + if isOpen { 632 + openVal = 1 633 + } 634 + issues, err := db.GetIssuesPaginated( 635 + rp.db, 636 + page, 637 + db.FilterEq("repo_at", f.RepoAt()), 638 + db.FilterEq("open", openVal), 639 + ) 652 640 if err != nil { 653 641 log.Println("failed to get issues", err) 654 642 rp.pages.Notice(w, "issues", "Failed to load issues. Try again later.") ··· 665 653 } 666 654 667 655 func (rp *Issues) NewIssue(w http.ResponseWriter, r *http.Request) { 656 + l := rp.logger.With("handler", "NewIssue") 668 657 user := rp.oauth.GetUser(r) 669 658 670 659 f, err := rp.repoResolver.Resolve(r) 671 660 if err != nil { 672 - log.Println("failed to get repo and knot", err) 661 + l.Error("failed to get repo and knot", "err", err) 673 662 return 674 663 } 675 664 ··· 698 687 return 699 688 } 700 689 701 - tx, err := rp.db.BeginTx(r.Context(), nil) 702 - if err != nil { 703 - rp.pages.Notice(w, "issues", "Failed to create issue, try again later") 704 - return 705 - } 706 - 707 690 issue := &db.Issue{ 708 - RepoAt: f.RepoAt(), 709 - Rkey: tid.TID(), 710 - Title: title, 711 - Body: body, 712 - OwnerDid: user.Did, 713 - } 714 - err = db.NewIssue(tx, issue) 715 - if err != nil { 716 - log.Println("failed to create issue", err) 717 - rp.pages.Notice(w, "issues", "Failed to create issue.") 718 - return 691 + RepoAt: f.RepoAt(), 692 + Rkey: tid.TID(), 693 + Title: title, 694 + Body: body, 695 + Did: user.Did, 696 + Created: time.Now(), 719 697 } 698 + record := issue.AsRecord() 720 699 700 + // create an atproto record 721 701 client, err := rp.oauth.AuthorizedClient(r) 722 702 if err != nil { 723 - log.Println("failed to get authorized client", err) 703 + l.Error("failed to get authorized client", "err", err) 724 704 rp.pages.Notice(w, "issues", "Failed to create issue.") 725 705 return 726 706 } 727 - atUri := f.RepoAt().String() 728 - _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 707 + resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 729 708 Collection: tangled.RepoIssueNSID, 730 709 Repo: user.Did, 731 710 Rkey: issue.Rkey, 732 711 Record: &lexutil.LexiconTypeDecoder{ 733 - Val: &tangled.RepoIssue{ 734 - Repo: atUri, 735 - Title: title, 736 - Body: &body, 737 - }, 712 + Val: &record, 738 713 }, 739 714 }) 740 715 if err != nil { 716 + l.Error("failed to create issue", "err", err) 717 + rp.pages.Notice(w, "issues", "Failed to create issue.") 718 + return 719 + } 720 + atUri := resp.Uri 721 + 722 + tx, err := rp.db.BeginTx(r.Context(), nil) 723 + if err != nil { 724 + rp.pages.Notice(w, "issues", "Failed to create issue, try again later") 725 + return 726 + } 727 + rollback := func() { 728 + err1 := tx.Rollback() 729 + err2 := rollbackRecord(context.Background(), atUri, client) 730 + 731 + if errors.Is(err1, sql.ErrTxDone) { 732 + err1 = nil 733 + } 734 + 735 + if err := errors.Join(err1, err2); err != nil { 736 + l.Error("failed to rollback txn", "err", err) 737 + } 738 + } 739 + defer rollback() 740 + 741 + err = db.NewIssue(tx, issue) 742 + if err != nil { 741 743 log.Println("failed to create issue", err) 742 744 rp.pages.Notice(w, "issues", "Failed to create issue.") 743 745 return 744 746 } 745 747 748 + if err = tx.Commit(); err != nil { 749 + log.Println("failed to create issue", err) 750 + rp.pages.Notice(w, "issues", "Failed to create issue.") 751 + return 752 + } 753 + 754 + // everything is successful, do not rollback the atproto record 755 + atUri = "" 746 756 rp.notifier.NewIssue(r.Context(), issue) 747 - 748 757 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issue.IssueId)) 749 758 return 750 759 } 751 760 } 761 + 762 + // this is used to rollback changes made to the PDS 763 + // 764 + // it is a no-op if the provided ATURI is empty 765 + func rollbackRecord(ctx context.Context, aturi string, xrpcc *xrpcclient.Client) error { 766 + if aturi == "" { 767 + return nil 768 + } 769 + 770 + parsed := syntax.ATURI(aturi) 771 + 772 + collection := parsed.Collection().String() 773 + repo := parsed.Authority().String() 774 + rkey := parsed.RecordKey().String() 775 + 776 + _, err := xrpcc.RepoDeleteRecord(ctx, &comatproto.RepoDeleteRecord_Input{ 777 + Collection: collection, 778 + Repo: repo, 779 + Rkey: rkey, 780 + }) 781 + return err 782 + }
+28 -6
appview/pages/pages.go
··· 909 909 RepoInfo repoinfo.RepoInfo 910 910 Active string 911 911 Issue *db.Issue 912 - Comments []db.Comment 912 + CommentList []db.CommentListItem 913 913 IssueOwnerHandle string 914 914 915 915 OrderedReactionKinds []db.ReactionKind ··· 955 955 LoggedInUser *oauth.User 956 956 RepoInfo repoinfo.RepoInfo 957 957 Issue *db.Issue 958 - Comment *db.Comment 958 + Comment *db.IssueComment 959 959 } 960 960 961 961 func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 962 962 return p.executePlain("repo/issues/fragments/editIssueComment", w, params) 963 963 } 964 964 965 - type SingleIssueCommentParams struct { 965 + type ReplyIssueCommentPlaceholderParams struct { 966 966 LoggedInUser *oauth.User 967 967 RepoInfo repoinfo.RepoInfo 968 968 Issue *db.Issue 969 - Comment *db.Comment 969 + Comment *db.IssueComment 970 970 } 971 971 972 - func (p *Pages) SingleIssueCommentFragment(w io.Writer, params SingleIssueCommentParams) error { 973 - return p.executePlain("repo/issues/fragments/issueComment", w, params) 972 + func (p *Pages) ReplyIssueCommentPlaceholderFragment(w io.Writer, params ReplyIssueCommentPlaceholderParams) error { 973 + return p.executePlain("repo/issues/fragments/replyIssueCommentPlaceholder", w, params) 974 + } 975 + 976 + type ReplyIssueCommentParams struct { 977 + LoggedInUser *oauth.User 978 + RepoInfo repoinfo.RepoInfo 979 + Issue *db.Issue 980 + Comment *db.IssueComment 981 + } 982 + 983 + func (p *Pages) ReplyIssueCommentFragment(w io.Writer, params ReplyIssueCommentParams) error { 984 + return p.executePlain("repo/issues/fragments/replyComment", w, params) 985 + } 986 + 987 + type IssueCommentBodyParams struct { 988 + LoggedInUser *oauth.User 989 + RepoInfo repoinfo.RepoInfo 990 + Issue *db.Issue 991 + Comment *db.IssueComment 992 + } 993 + 994 + func (p *Pages) IssueCommentBodyFragment(w io.Writer, params IssueCommentBodyParams) error { 995 + return p.executePlain("repo/issues/fragments/issueCommentBody", w, params) 974 996 } 975 997 976 998 type RepoNewPullParams struct {
+34
appview/pages/templates/repo/issues/fragments/issueCommentActions.html
··· 1 + {{ define "repo/issues/fragments/issueCommentActions" }} 2 + {{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did) }} 3 + {{ if and $isCommentOwner (not .Comment.Deleted) }} 4 + <div class="flex flex-wrap items-center gap-4 text-gray-500 dark:text-gray-400 text-sm pt-2"> 5 + {{ template "edit" . }} 6 + {{ template "delete" . }} 7 + </div> 8 + {{ end }} 9 + {{ end }} 10 + 11 + {{ define "edit" }} 12 + <a 13 + class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group" 14 + hx-get="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.Id }}/comment/{{ .Comment.Id }}/edit" 15 + hx-swap="outerHTML" 16 + hx-target="#comment-body-{{.Comment.Id}}"> 17 + {{ i "pencil" "size-3" }} 18 + edit 19 + </a> 20 + {{ end }} 21 + 22 + {{ define "delete" }} 23 + <a 24 + class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group" 25 + hx-delete="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.Id }}/comment/{{ .Comment.Id }}/" 26 + hx-confirm="Are you sure you want to delete your comment?" 27 + hx-swap="outerHTML" 28 + hx-target="#comment-body-{{.Comment.Id}}" 29 + > 30 + {{ i "trash-2" "size-3" }} 31 + delete 32 + {{ i "loader-circle" "size-3 animate-spin hidden group-[.htmx-request]:inline" }} 33 + </a> 34 + {{ end }}
+57
appview/pages/templates/repo/issues/fragments/replyComment.html
··· 1 + {{ define "repo/issues/fragments/replyComment" }} 2 + <form 3 + class="p-2 group w-full border-t border-gray-200 dark:border-gray-700 flex flex-col gap-2" 4 + id="reply-form-{{ .Comment.Id }}" 5 + hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment" 6 + hx-on::after-request="if(event.detail.successful) this.reset()" 7 + > 8 + {{ template "user/fragments/picHandleLink" .LoggedInUser.Did }} 9 + <textarea 10 + id="reply-{{.Comment.Id}}-textarea" 11 + name="body" 12 + class="w-full p-2" 13 + placeholder="Leave a reply..." 14 + autofocus 15 + rows="3"></textarea> 16 + 17 + <input 18 + type="text" 19 + id="reply-to" 20 + name="reply-to" 21 + required 22 + value="{{ .Comment.AtUri }}" 23 + class="hidden" 24 + /> 25 + {{ template "replyActions" . }} 26 + </form> 27 + {{ end }} 28 + 29 + {{ define "replyActions" }} 30 + <div class="flex flex-wrap items-stretch justify-end gap-2 text-gray-500 dark:text-gray-400 text-sm"> 31 + {{ template "cancel" . }} 32 + {{ template "reply" . }} 33 + </div> 34 + {{ end }} 35 + 36 + {{ define "cancel" }} 37 + <button 38 + class="btn text-red-500 dark:text-red-400 flex gap-2 items-center group" 39 + hx-get="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment/{{ .Comment.Id }}/replyPlaceholder" 40 + hx-target="#reply-form-{{ .Comment.Id }}" 41 + hx-swap="outerHTML"> 42 + {{ i "x" "size-4" }} 43 + cancel 44 + </button> 45 + {{ end }} 46 + 47 + {{ define "reply" }} 48 + <button 49 + id="reply-{{ .Comment.Id }}" 50 + type="submit" 51 + hx-disabled-elt="#reply-{{ .Comment.Id }}" 52 + class="btn-create flex items-center gap-2 no-underline hover:no-underline"> 53 + {{ i "reply" "w-4 h-4 inline group-[.htmx-request]:hidden" }} 54 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 55 + reply 56 + </button> 57 + {{ end }}
+12 -160
appview/pages/templates/repo/issues/issue.html
··· 32 32 </div> 33 33 <span class="text-gray-500 dark:text-gray-400 text-sm flex flex-wrap items-center gap-1"> 34 34 opened by 35 - {{ $owner := didOrHandle .Issue.OwnerDid .IssueOwnerHandle }} 36 - {{ template "user/fragments/picHandleLink" $owner }} 35 + {{ template "user/fragments/picHandleLink" .Issue.Did }} 37 36 <span class="select-none before:content-['\00B7']"></span> 38 37 {{ template "repo/fragments/time" .Issue.Created }} 39 38 </span> ··· 62 61 {{ end }} 63 62 64 63 {{ define "repoAfter" }} 65 - <section id="comments" class="my-2 mt-2 space-y-2 relative"> 66 - {{ range $index, $comment := .Comments }} 67 - <div 68 - id="comment-{{ .CommentId }}" 69 - class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-2 px-4 relative w-full md:max-w-3/5 md:w-fit"> 70 - {{ if gt $index 0 }} 71 - <div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div> 72 - {{ end }} 73 - {{ template "repo/issues/fragments/issueComment" (dict "RepoInfo" $.RepoInfo "LoggedInUser" $.LoggedInUser "Issue" $.Issue "Comment" .)}} 74 - </div> 75 - {{ end }} 76 - </section> 64 + <div class="flex flex-col gap-4 mt-4"> 65 + {{ 66 + template "repo/issues/fragments/commentList" 67 + (dict 68 + "RepoInfo" $.RepoInfo 69 + "LoggedInUser" $.LoggedInUser 70 + "Issue" $.Issue 71 + "CommentList" $.Issue.CommentList) 72 + }} 77 73 78 - {{ block "newComment" . }} {{ end }} 79 - 74 + {{ template "repo/issues/fragments/newComment" . }} 75 + <div> 80 76 {{ end }} 81 77 82 - {{ define "newComment" }} 83 - {{ if .LoggedInUser }} 84 - <form 85 - id="comment-form" 86 - hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment" 87 - hx-on::after-request="if(event.detail.successful) this.reset()" 88 - > 89 - <div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-4 px-4 relative w-full md:w-3/5"> 90 - <div class="text-sm pb-2 text-gray-500 dark:text-gray-400"> 91 - {{ template "user/fragments/picHandleLink" (didOrHandle .LoggedInUser.Did .LoggedInUser.Handle) }} 92 - </div> 93 - <textarea 94 - id="comment-textarea" 95 - name="body" 96 - class="w-full p-2 rounded border border-gray-200 dark:border-gray-700" 97 - placeholder="Add to the discussion. Markdown is supported." 98 - onkeyup="updateCommentForm()" 99 - ></textarea> 100 - <div id="issue-comment"></div> 101 - <div id="issue-action" class="error"></div> 102 - </div> 103 - 104 - <div class="flex gap-2 mt-2"> 105 - <button 106 - id="comment-button" 107 - hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment" 108 - type="submit" 109 - hx-disabled-elt="#comment-button" 110 - class="btn p-2 flex items-center gap-2 no-underline hover:no-underline group" 111 - disabled 112 - > 113 - {{ i "message-square-plus" "w-4 h-4" }} 114 - comment 115 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 116 - </button> 117 - 118 - {{ $isIssueAuthor := and .LoggedInUser (eq .LoggedInUser.Did .Issue.OwnerDid) }} 119 - {{ $isRepoCollaborator := .RepoInfo.Roles.IsCollaborator }} 120 - {{ $isRepoOwner := .RepoInfo.Roles.IsOwner }} 121 - {{ if and (or $isIssueAuthor $isRepoCollaborator $isRepoOwner) (eq .State "open") }} 122 - <button 123 - id="close-button" 124 - type="button" 125 - class="btn flex items-center gap-2" 126 - hx-indicator="#close-spinner" 127 - hx-trigger="click" 128 - > 129 - {{ i "ban" "w-4 h-4" }} 130 - close 131 - <span id="close-spinner" class="group"> 132 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 133 - </span> 134 - </button> 135 - <div 136 - id="close-with-comment" 137 - hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment" 138 - hx-trigger="click from:#close-button" 139 - hx-disabled-elt="#close-with-comment" 140 - hx-target="#issue-comment" 141 - hx-indicator="#close-spinner" 142 - hx-vals="js:{body: document.getElementById('comment-textarea').value.trim() !== '' ? document.getElementById('comment-textarea').value : ''}" 143 - hx-swap="none" 144 - > 145 - </div> 146 - <div 147 - id="close-issue" 148 - hx-disabled-elt="#close-issue" 149 - hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/close" 150 - hx-trigger="click from:#close-button" 151 - hx-target="#issue-action" 152 - hx-indicator="#close-spinner" 153 - hx-swap="none" 154 - > 155 - </div> 156 - <script> 157 - document.addEventListener('htmx:configRequest', function(evt) { 158 - if (evt.target.id === 'close-with-comment') { 159 - const commentText = document.getElementById('comment-textarea').value.trim(); 160 - if (commentText === '') { 161 - evt.detail.parameters = {}; 162 - evt.preventDefault(); 163 - } 164 - } 165 - }); 166 - </script> 167 - {{ else if and (or $isIssueAuthor $isRepoCollaborator $isRepoOwner) (eq .State "closed") }} 168 - <button 169 - type="button" 170 - class="btn flex items-center gap-2" 171 - hx-post="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/reopen" 172 - hx-indicator="#reopen-spinner" 173 - hx-swap="none" 174 - > 175 - {{ i "refresh-ccw-dot" "w-4 h-4" }} 176 - reopen 177 - <span id="reopen-spinner" class="group"> 178 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 179 - </span> 180 - </button> 181 - {{ end }} 182 - 183 - <script> 184 - function updateCommentForm() { 185 - const textarea = document.getElementById('comment-textarea'); 186 - const commentButton = document.getElementById('comment-button'); 187 - const closeButton = document.getElementById('close-button'); 188 - 189 - if (textarea.value.trim() !== '') { 190 - commentButton.removeAttribute('disabled'); 191 - } else { 192 - commentButton.setAttribute('disabled', ''); 193 - } 194 - 195 - if (closeButton) { 196 - if (textarea.value.trim() !== '') { 197 - closeButton.innerHTML = ` 198 - {{ i "ban" "w-4 h-4" }} 199 - <span>close with comment</span> 200 - <span id="close-spinner" class="group"> 201 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 202 - </span>`; 203 - } else { 204 - closeButton.innerHTML = ` 205 - {{ i "ban" "w-4 h-4" }} 206 - <span>close</span> 207 - <span id="close-spinner" class="group"> 208 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 209 - </span>`; 210 - } 211 - } 212 - } 213 - 214 - document.addEventListener('DOMContentLoaded', function() { 215 - updateCommentForm(); 216 - }); 217 - </script> 218 - </div> 219 - </form> 220 - {{ else }} 221 - <div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-4 px-4 relative w-fit"> 222 - <a href="/login" class="underline">login</a> to join the discussion 223 - </div> 224 - {{ end }} 225 - {{ end }}
+42 -44
appview/pages/templates/repo/issues/issues.html
··· 37 37 {{ end }} 38 38 39 39 {{ define "repoAfter" }} 40 - <div class="flex flex-col gap-2 mt-2"> 41 - {{ range .Issues }} 42 - <div class="rounded drop-shadow-sm bg-white px-6 py-4 dark:bg-gray-800 dark:border-gray-700"> 43 - <div class="pb-2"> 44 - <a 45 - href="/{{ $.RepoInfo.FullName }}/issues/{{ .IssueId }}" 46 - class="no-underline hover:underline" 47 - > 48 - {{ .Title | description }} 49 - <span class="text-gray-500">#{{ .IssueId }}</span> 50 - </a> 51 - </div> 52 - <p class="text-sm text-gray-500 dark:text-gray-400 flex flex-wrap items-center gap-1"> 53 - {{ $bgColor := "bg-gray-800 dark:bg-gray-700" }} 54 - {{ $icon := "ban" }} 55 - {{ $state := "closed" }} 56 - {{ if .Open }} 57 - {{ $bgColor = "bg-green-600 dark:bg-green-700" }} 58 - {{ $icon = "circle-dot" }} 59 - {{ $state = "open" }} 60 - {{ end }} 40 + <div class="flex flex-col gap-2 mt-2"> 41 + {{ range .Issues }} 42 + <div class="rounded drop-shadow-sm bg-white px-6 py-4 dark:bg-gray-800 dark:border-gray-700"> 43 + <div class="pb-2"> 44 + <a 45 + href="/{{ $.RepoInfo.FullName }}/issues/{{ .IssueId }}" 46 + class="no-underline hover:underline" 47 + > 48 + {{ .Title | description }} 49 + <span class="text-gray-500">#{{ .IssueId }}</span> 50 + </a> 51 + </div> 52 + <p class="text-sm text-gray-500 dark:text-gray-400 flex flex-wrap items-center gap-1"> 53 + {{ $bgColor := "bg-gray-800 dark:bg-gray-700" }} 54 + {{ $icon := "ban" }} 55 + {{ $state := "closed" }} 56 + {{ if .Open }} 57 + {{ $bgColor = "bg-green-600 dark:bg-green-700" }} 58 + {{ $icon = "circle-dot" }} 59 + {{ $state = "open" }} 60 + {{ end }} 61 61 62 - <span class="inline-flex items-center rounded px-2 py-[5px] {{ $bgColor }} text-sm"> 63 - {{ i $icon "w-3 h-3 mr-1.5 text-white dark:text-white" }} 64 - <span class="text-white dark:text-white">{{ $state }}</span> 65 - </span> 62 + <span class="inline-flex items-center rounded px-2 py-[5px] {{ $bgColor }} text-sm"> 63 + {{ i $icon "w-3 h-3 mr-1.5 text-white dark:text-white" }} 64 + <span class="text-white dark:text-white">{{ $state }}</span> 65 + </span> 66 66 67 - <span class="ml-1"> 68 - {{ template "user/fragments/picHandleLink" .OwnerDid }} 69 - </span> 67 + <span class="ml-1"> 68 + {{ template "user/fragments/picHandleLink" .Did }} 69 + </span> 70 70 71 - <span class="before:content-['·']"> 72 - {{ template "repo/fragments/time" .Created }} 73 - </span> 71 + <span class="before:content-['·']"> 72 + {{ template "repo/fragments/time" .Created }} 73 + </span> 74 74 75 - <span class="before:content-['·']"> 76 - {{ $s := "s" }} 77 - {{ if eq .Metadata.CommentCount 1 }} 78 - {{ $s = "" }} 79 - {{ end }} 80 - <a href="/{{ $.RepoInfo.FullName }}/issues/{{ .IssueId }}" class="text-gray-500 dark:text-gray-400">{{ .Metadata.CommentCount }} comment{{$s}}</a> 81 - </span> 82 - </p> 75 + <span class="before:content-['·']"> 76 + {{ $s := "s" }} 77 + {{ if eq (len .Comments) 1 }} 78 + {{ $s = "" }} 79 + {{ end }} 80 + <a href="/{{ $.RepoInfo.FullName }}/issues/{{ .IssueId }}" class="text-gray-500 dark:text-gray-400">{{ len .Comments }} comment{{$s}}</a> 81 + </span> 82 + </p> 83 + </div> 84 + {{ end }} 83 85 </div> 84 - {{ end }} 85 - </div> 86 - 87 - {{ block "pagination" . }} {{ end }} 88 - 86 + {{ block "pagination" . }} {{ end }} 89 87 {{ end }} 90 88 91 89 {{ define "pagination" }}
+2 -2
appview/pages/templates/repo/issues/new.html
··· 3 3 {{ define "repoContent" }} 4 4 <form 5 5 hx-post="/{{ .RepoInfo.FullName }}/issues/new" 6 - class="mt-6 space-y-6" 6 + class="space-y-6" 7 7 hx-swap="none" 8 8 hx-indicator="#spinner" 9 9 > ··· 26 26 <button type="submit" class="btn-create flex items-center gap-2"> 27 27 {{ i "circle-plus" "w-4 h-4" }} 28 28 create issue 29 - <span id="create-pull-spinner" class="group"> 29 + <span id="spinner" class="group"> 30 30 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 31 31 </span> 32 32 </button>