Monorepo for Tangled tangled.org

allow editing and deleting issues

Changed files
+476 -46
appview
+9
appview/db/db.go
··· 248 248 return nil 249 249 }) 250 250 251 + runMigration(db, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error { 252 + // add unconstrained column 253 + _, err := tx.Exec(` 254 + alter table comments add column deleted text; -- timestamp 255 + alter table comments add column edited text; -- timestamp 256 + `) 257 + return err 258 + }) 259 + 251 260 return &DB{db}, nil 252 261 } 253 262
+71 -1
appview/db/issues.go
··· 27 27 type Comment struct { 28 28 OwnerDid string 29 29 RepoAt syntax.ATURI 30 - CommentAt string 30 + CommentAt syntax.ATURI 31 31 Issue int 32 32 CommentId int 33 33 Body string 34 34 Created *time.Time 35 + Deleted *time.Time 36 + Edited *time.Time 35 37 } 36 38 37 39 func NewIssue(tx *sql.Tx, issue *Issue) error { ··· 247 249 } 248 250 249 251 return comments, nil 252 + } 253 + 254 + func GetComment(e Execer, repoAt syntax.ATURI, issueId, commentId int) (*Comment, error) { 255 + query := ` 256 + select 257 + owner_did, body, comment_at, created, deleted, edited 258 + from 259 + comments where repo_at = ? and issue_id = ? and comment_id = ? 260 + ` 261 + row := e.QueryRow(query, repoAt, issueId, commentId) 262 + 263 + var comment Comment 264 + var createdAt string 265 + var deletedAt, editedAt sql.NullString 266 + err := row.Scan(&comment.OwnerDid, &comment.Body, &comment.CommentAt, &createdAt, &deletedAt, &editedAt) 267 + if err != nil { 268 + return nil, err 269 + } 270 + 271 + createdTime, err := time.Parse(time.RFC3339, createdAt) 272 + if err != nil { 273 + return nil, err 274 + } 275 + comment.Created = &createdTime 276 + 277 + if deletedAt.Valid { 278 + deletedTime, err := time.Parse(time.RFC3339, deletedAt.String) 279 + if err != nil { 280 + return nil, err 281 + } 282 + comment.Deleted = &deletedTime 283 + } 284 + 285 + if editedAt.Valid { 286 + editedTime, err := time.Parse(time.RFC3339, editedAt.String) 287 + if err != nil { 288 + return nil, err 289 + } 290 + comment.Edited = &editedTime 291 + } 292 + 293 + comment.RepoAt = repoAt 294 + comment.Issue = issueId 295 + comment.CommentId = commentId 296 + 297 + return &comment, nil 298 + } 299 + 300 + func EditComment(e Execer, repoAt syntax.ATURI, issueId, commentId int, newBody string) error { 301 + _, err := e.Exec( 302 + ` 303 + update comments 304 + set body = ?, 305 + edited = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 306 + where repo_at = ? and issue_id = ? and comment_id = ? 307 + `, newBody, repoAt, issueId, commentId) 308 + return err 309 + } 310 + 311 + func DeleteComment(e Execer, repoAt syntax.ATURI, issueId, commentId int) error { 312 + _, err := e.Exec( 313 + ` 314 + update comments 315 + set body = "", 316 + deleted = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 317 + where repo_at = ? and issue_id = ? and comment_id = ? 318 + `, repoAt, issueId, commentId) 319 + return err 250 320 } 251 321 252 322 func CloseIssue(e Execer, repoAt syntax.ATURI, issueId int) error {
+23
appview/pages/pages.go
··· 534 534 return p.executeRepo("repo/issues/new", w, params) 535 535 } 536 536 537 + type EditIssueCommentParams struct { 538 + LoggedInUser *auth.User 539 + RepoInfo RepoInfo 540 + Issue *db.Issue 541 + Comment *db.Comment 542 + } 543 + 544 + func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentParams) error { 545 + return p.executePlain("fragments/editIssueComment", w, params) 546 + } 547 + 548 + type SingleIssueCommentParams struct { 549 + LoggedInUser *auth.User 550 + DidHandleMap map[string]string 551 + RepoInfo RepoInfo 552 + Issue *db.Issue 553 + Comment *db.Comment 554 + } 555 + 556 + func (p *Pages) SingleIssueCommentFragment(w io.Writer, params SingleIssueCommentParams) error { 557 + return p.executePlain("fragments/issueComment", w, params) 558 + } 559 + 537 560 type RepoNewPullParams struct { 538 561 LoggedInUser *auth.User 539 562 RepoInfo RepoInfo
+23 -18
appview/pages/templates/fragments/diff.html
··· 79 79 This is a binary file and will not be displayed. 80 80 </p> 81 81 {{ else }} 82 - <pre class="overflow-auto"> 83 - {{- range .TextFragments -}} 84 - <div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none">{{ .Header }}</div> 85 - {{- range .Lines -}} 86 - {{- if eq .Op.String "+" -}} 87 - <div class="bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400 p-1 w-full min-w-fit"><span class="select-none mx-2">{{ .Op.String }}</span><span>{{ .Line }}</span></div> 88 - {{- end -}} 89 - 90 - {{- if eq .Op.String "-" -}} 91 - <div class="bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 p-1 w-full min-w-fit"><span class="select-none mx-2">{{ .Op.String }}</span><span>{{ .Line }}</span></div> 92 - {{- end -}} 93 - 94 - {{- if eq .Op.String " " -}} 95 - <div class="bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 px"><span class="select-none mx-2">{{ .Op.String }}</span><span>{{ .Line }}</span></div> 96 - {{- end -}} 97 - 98 - {{- end -}} 99 - {{- end -}} 82 + <pre class="overflow-x-auto"> 83 + {{- range .TextFragments -}} 84 + <div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none">{{- .Header -}}</div><div class="overflow-x-auto"><div class="min-w-full inline-block"> 85 + {{- range .Lines -}} 86 + {{- if eq .Op.String "+" -}} 87 + <div class="bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400 flex min-w-full"> 88 + <div class="w-10 flex-shrink-0 select-none p-1 text-center">{{ .Op.String }}</div> 89 + <div class="p-1 whitespace-pre">{{ .Line }}</div> 90 + </div> 91 + {{- end -}} 92 + {{- if eq .Op.String "-" -}} 93 + <div class="bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 flex min-w-full"> 94 + <div class="w-10 flex-shrink-0 select-none p-1 text-center">{{ .Op.String }}</div> 95 + <div class="p-1 whitespace-pre">{{ .Line }}</div> 96 + </div> 97 + {{- end -}} 98 + {{- if eq .Op.String " " -}} 99 + <div class="bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400 flex min-w-full"> 100 + <div class="w-10 flex-shrink-0 select-none p-1 text-center">{{ .Op.String }}</div> 101 + <div class="p-1 whitespace-pre">{{ .Line }}</div> 102 + </div> 103 + {{- end -}} 104 + {{- end -}}</div></div>{{- end -}} 100 105 </pre> 101 106 {{- end -}} 102 107 {{ end }}
+52
appview/pages/templates/fragments/editIssueComment.html
··· 1 + {{ define "fragments/editIssueComment" }} 2 + {{ with .Comment }} 3 + <div id="comment-container-{{.CommentId}}"> 4 + <div class="flex items-center gap-2 mb-2 text-gray-500 text-sm"> 5 + {{ $owner := didOrHandle $.LoggedInUser.Did $.LoggedInUser.Handle }} 6 + <a href="/{{ $owner }}" class="no-underline hover:underline">{{ $owner }}</a> 7 + 8 + <!-- show user "hats" --> 9 + {{ $isIssueAuthor := eq .OwnerDid $.Issue.OwnerDid }} 10 + {{ if $isIssueAuthor }} 11 + <span class="before:content-['·']"></span> 12 + <span class="rounded bg-gray-100 text-black font-mono px-2 mx-1/2 inline-flex items-center"> 13 + author 14 + </span> 15 + {{ end }} 16 + 17 + <span class="before:content-['·']"></span> 18 + <a 19 + href="#{{ .CommentId }}" 20 + class="text-gray-500 hover:text-gray-500 hover:underline no-underline" 21 + id="{{ .CommentId }}"> 22 + {{ .Created | timeFmt }} 23 + </a> 24 + 25 + <button 26 + class="btn px-2 py-1 flex items-center gap-2 text-sm" 27 + hx-post="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/edit" 28 + hx-include="#edit-textarea-{{ .CommentId }}" 29 + hx-target="#comment-container-{{ .CommentId }}" 30 + hx-swap="outerHTML"> 31 + {{ i "check" "w-4 h-4" }} 32 + </button> 33 + <button 34 + class="btn px-2 py-1 flex items-center gap-2 text-sm" 35 + hx-get="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/" 36 + hx-target="#comment-container-{{ .CommentId }}" 37 + hx-swap="outerHTML"> 38 + {{ i "x" "w-4 h-4" }} 39 + </button> 40 + <span id="comment-{{.CommentId}}-status"></span> 41 + </div> 42 + 43 + <div> 44 + <textarea 45 + id="edit-textarea-{{ .CommentId }}" 46 + name="body" 47 + class="w-full p-2 border rounded min-h-[100px]">{{ .Body }}</textarea> 48 + </div> 49 + </div> 50 + {{ end }} 51 + {{ end }} 52 +
+52
appview/pages/templates/fragments/issueComment.html
··· 1 + {{ define "fragments/issueComment" }} 2 + {{ with .Comment }} 3 + <div id="comment-container-{{.CommentId}}"> 4 + <div class="flex items-center gap-2 mb-2 text-gray-500 text-sm"> 5 + {{ $owner := index $.DidHandleMap .OwnerDid }} 6 + <a href="/{{ $owner }}" class="no-underline hover:underline">{{ $owner }}</a> 7 + 8 + <!-- show user "hats" --> 9 + {{ $isIssueAuthor := eq .OwnerDid $.Issue.OwnerDid }} 10 + {{ if $isIssueAuthor }} 11 + <span class="before:content-['·']"></span> 12 + <span class="rounded bg-gray-100 text-black font-mono px-2 mx-1/2 inline-flex items-center"> 13 + author 14 + </span> 15 + {{ end }} 16 + 17 + <span class="before:content-['·']"></span> 18 + <a 19 + href="#{{ .CommentId }}" 20 + class="text-gray-500 hover:text-gray-500 hover:underline no-underline" 21 + id="{{ .CommentId }}"> 22 + {{ .Created | timeFmt }} 23 + </a> 24 + 25 + {{ $isCommentOwner := eq $.LoggedInUser.Did .OwnerDid }} 26 + {{ if and $isCommentOwner (not .Deleted) }} 27 + <button 28 + class="btn px-2 py-1 text-sm" 29 + hx-get="/{{ $.RepoInfo.FullName }}/issues/{{ .Issue }}/comment/{{ .CommentId }}/edit" 30 + hx-swap="outerHTML" 31 + hx-target="#comment-container-{{.CommentId}}" 32 + > 33 + {{ i "pencil" "w-4 h-4" }} 34 + </button> 35 + <button class="btn px-2 py-1 text-sm text-red-500" hx-delete=""> 36 + {{ i "trash-2" "w-4 h-4" }} 37 + </button> 38 + {{ end }} 39 + 40 + {{ if .Deleted }} 41 + <span class="before:content-['·']">deleted {{ .Deleted | timeFmt }}</span> 42 + {{ end }} 43 + 44 + </div> 45 + {{ if not .Deleted }} 46 + <div class="prose"> 47 + {{ .Body | markdown }} 48 + </div> 49 + {{ end }} 50 + </div> 51 + {{ end }} 52 + {{ end }}
+3 -25
appview/pages/templates/repo/issues/issue.html
··· 1 - {{ define "title" }}{{ .Issue.Title }} &middot; issue #{{ .Issue.IssueId }} &middot;{{ .RepoInfo.FullName }}{{ end }} 1 + {{ define "title" }}{{ .Issue.Title }} &middot; issue #{{ .Issue.IssueId }} &middot; {{ .RepoInfo.FullName }}{{ end }} 2 2 3 3 {{ define "repoContent" }} 4 4 <header class="pb-4"> ··· 49 49 {{ range $index, $comment := .Comments }} 50 50 <div 51 51 id="comment-{{ .CommentId }}" 52 - class="rounded bg-white px-6 py-4 relative dark:bg-gray-800" 53 - > 52 + class="rounded bg-white px-6 py-4 relative dark:bg-gray-800"> 54 53 {{ if eq $index 0 }} 55 54 <div class="absolute left-8 -top-8 w-px h-8 bg-gray-300 dark:bg-gray-700" ></div> 56 55 {{ else }} 57 56 <div class="absolute left-8 -top-4 w-px h-4 bg-gray-300 dark:bg-gray-700" ></div> 58 57 {{ end }} 59 - <div class="flex items-center gap-2 mb-2 text-gray-500 dark:text-gray-400"> 60 - {{ $owner := index $.DidHandleMap .OwnerDid }} 61 - <span class="text-sm"> 62 - <a 63 - href="/{{ $owner }}" 64 - class="no-underline hover:underline" 65 - >{{ $owner }}</a 66 - > 67 - </span> 68 58 69 - <span class="before:content-['·']"></span> 70 - <a 71 - href="#{{ .CommentId }}" 72 - class="text-gray-500 text-sm hover:text-gray-500 hover:underline no-underline dark:text-gray-400 dark:hover:text-gray-300 dark:hover:bg-gray-800" 73 - id="{{ .CommentId }}" 74 - title="{{ .Created | longTimeFmt }}" 75 - > 76 - {{ .Created | timeFmt }} 77 - </a> 78 - </div> 79 - <div class="prose dark:prose-invert"> 80 - {{ .Body | markdown }} 81 - </div> 59 + {{ template "fragments/issueComment" (dict "RepoInfo" $.RepoInfo "LoggedInUser" $.LoggedInUser "DidHandleMap" $.DidHandleMap "Issue" $.Issue "Comment" .)}} 82 60 </div> 83 61 {{ end }} 84 62 </section>
+236 -1
appview/state/repo.go
··· 14 14 "strings" 15 15 "time" 16 16 17 + "github.com/bluesky-social/indigo/atproto/data" 17 18 "github.com/bluesky-social/indigo/atproto/identity" 18 19 "github.com/bluesky-social/indigo/atproto/syntax" 19 20 securejoin "github.com/cyphar/filepath-securejoin" ··· 907 908 } 908 909 } 909 910 910 - func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) { 911 + func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) { 911 912 user := s.auth.GetUser(r) 912 913 f, err := fullyResolvedRepo(r) 913 914 if err != nil { ··· 982 983 s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issueIdInt, commentId)) 983 984 return 984 985 } 986 + } 987 + 988 + func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) { 989 + user := s.auth.GetUser(r) 990 + f, err := fullyResolvedRepo(r) 991 + if err != nil { 992 + log.Println("failed to get repo and knot", err) 993 + return 994 + } 995 + 996 + issueId := chi.URLParam(r, "issue") 997 + issueIdInt, err := strconv.Atoi(issueId) 998 + if err != nil { 999 + http.Error(w, "bad issue id", http.StatusBadRequest) 1000 + log.Println("failed to parse issue id", err) 1001 + return 1002 + } 1003 + 1004 + commentId := chi.URLParam(r, "comment_id") 1005 + commentIdInt, err := strconv.Atoi(commentId) 1006 + if err != nil { 1007 + http.Error(w, "bad comment id", http.StatusBadRequest) 1008 + log.Println("failed to parse issue id", err) 1009 + return 1010 + } 1011 + 1012 + issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt) 1013 + if err != nil { 1014 + log.Println("failed to get issue", err) 1015 + s.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 1016 + return 1017 + } 1018 + 1019 + comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt) 1020 + if err != nil { 1021 + http.Error(w, "bad comment id", http.StatusBadRequest) 1022 + return 1023 + } 1024 + 1025 + identity, err := s.resolver.ResolveIdent(r.Context(), comment.OwnerDid) 1026 + if err != nil { 1027 + log.Println("failed to resolve did") 1028 + return 1029 + } 1030 + 1031 + didHandleMap := make(map[string]string) 1032 + if !identity.Handle.IsInvalidHandle() { 1033 + didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 1034 + } else { 1035 + didHandleMap[identity.DID.String()] = identity.DID.String() 1036 + } 1037 + 1038 + s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 1039 + LoggedInUser: user, 1040 + RepoInfo: f.RepoInfo(s, user), 1041 + DidHandleMap: didHandleMap, 1042 + Issue: issue, 1043 + Comment: comment, 1044 + }) 1045 + } 1046 + 1047 + func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) { 1048 + user := s.auth.GetUser(r) 1049 + f, err := fullyResolvedRepo(r) 1050 + if err != nil { 1051 + log.Println("failed to get repo and knot", err) 1052 + return 1053 + } 1054 + 1055 + issueId := chi.URLParam(r, "issue") 1056 + issueIdInt, err := strconv.Atoi(issueId) 1057 + if err != nil { 1058 + http.Error(w, "bad issue id", http.StatusBadRequest) 1059 + log.Println("failed to parse issue id", err) 1060 + return 1061 + } 1062 + 1063 + commentId := chi.URLParam(r, "comment_id") 1064 + commentIdInt, err := strconv.Atoi(commentId) 1065 + if err != nil { 1066 + http.Error(w, "bad comment id", http.StatusBadRequest) 1067 + log.Println("failed to parse issue id", err) 1068 + return 1069 + } 1070 + 1071 + issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt) 1072 + if err != nil { 1073 + log.Println("failed to get issue", err) 1074 + s.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 1075 + return 1076 + } 1077 + 1078 + comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt) 1079 + if err != nil { 1080 + http.Error(w, "bad comment id", http.StatusBadRequest) 1081 + return 1082 + } 1083 + 1084 + if comment.OwnerDid != user.Did { 1085 + http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 1086 + return 1087 + } 1088 + 1089 + switch r.Method { 1090 + case http.MethodGet: 1091 + s.pages.EditIssueCommentFragment(w, pages.EditIssueCommentParams{ 1092 + LoggedInUser: user, 1093 + RepoInfo: f.RepoInfo(s, user), 1094 + Issue: issue, 1095 + Comment: comment, 1096 + }) 1097 + case http.MethodPost: 1098 + // extract form value 1099 + newBody := r.FormValue("body") 1100 + client, _ := s.auth.AuthorizedClient(r) 1101 + log.Println("comment at", comment.CommentAt) 1102 + rkey := comment.CommentAt.RecordKey() 1103 + 1104 + // optimistic update 1105 + err = db.EditComment(s.db, comment.RepoAt, comment.Issue, comment.CommentId, newBody) 1106 + if err != nil { 1107 + log.Println("failed to perferom update-description query", err) 1108 + s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 1109 + return 1110 + } 1111 + 1112 + // update the record on pds 1113 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, rkey.String()) 1114 + if err != nil { 1115 + // failed to get record 1116 + log.Println(err, rkey.String()) 1117 + s.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.") 1118 + return 1119 + } 1120 + value, _ := ex.Value.MarshalJSON() // we just did get record; it is valid json 1121 + record, _ := data.UnmarshalJSON(value) 1122 + 1123 + repoAt := record["repo"].(string) 1124 + issueAt := record["issue"].(string) 1125 + createdAt := record["createdAt"].(string) 1126 + commentIdInt64 := int64(commentIdInt) 1127 + 1128 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1129 + Collection: tangled.RepoNSID, 1130 + Repo: user.Did, 1131 + Rkey: rkey.String(), 1132 + SwapRecord: ex.Cid, 1133 + Record: &lexutil.LexiconTypeDecoder{ 1134 + Val: &tangled.RepoIssueComment{ 1135 + Repo: &repoAt, 1136 + Issue: issueAt, 1137 + CommentId: &commentIdInt64, 1138 + Owner: &comment.OwnerDid, 1139 + Body: &newBody, 1140 + CreatedAt: &createdAt, 1141 + }, 1142 + }, 1143 + }) 1144 + if err != nil { 1145 + log.Println(err) 1146 + } 1147 + 1148 + // optimistic update for htmx 1149 + didHandleMap := map[string]string{ 1150 + user.Did: user.Handle, 1151 + } 1152 + comment.Body = newBody 1153 + 1154 + // return new comment body with htmx 1155 + s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 1156 + LoggedInUser: user, 1157 + RepoInfo: f.RepoInfo(s, user), 1158 + DidHandleMap: didHandleMap, 1159 + Issue: issue, 1160 + Comment: comment, 1161 + }) 1162 + return 1163 + 1164 + } 1165 + 1166 + } 1167 + 1168 + func (s *State) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { 1169 + user := s.auth.GetUser(r) 1170 + f, err := fullyResolvedRepo(r) 1171 + if err != nil { 1172 + log.Println("failed to get repo and knot", err) 1173 + return 1174 + } 1175 + 1176 + issueId := chi.URLParam(r, "issue") 1177 + issueIdInt, err := strconv.Atoi(issueId) 1178 + if err != nil { 1179 + http.Error(w, "bad issue id", http.StatusBadRequest) 1180 + log.Println("failed to parse issue id", err) 1181 + return 1182 + } 1183 + 1184 + commentId := chi.URLParam(r, "comment_id") 1185 + commentIdInt, err := strconv.Atoi(commentId) 1186 + if err != nil { 1187 + http.Error(w, "bad comment id", http.StatusBadRequest) 1188 + log.Println("failed to parse issue id", err) 1189 + return 1190 + } 1191 + 1192 + comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt) 1193 + if err != nil { 1194 + http.Error(w, "bad comment id", http.StatusBadRequest) 1195 + return 1196 + } 1197 + 1198 + if comment.OwnerDid != user.Did { 1199 + http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 1200 + return 1201 + } 1202 + 1203 + if comment.Deleted != nil { 1204 + http.Error(w, "comment already deleted", http.StatusBadRequest) 1205 + return 1206 + } 1207 + 1208 + // optimistic deletion 1209 + err = db.DeleteComment(s.db, f.RepoAt, issueIdInt, commentIdInt) 1210 + if err != nil { 1211 + log.Println("failed to delete comment") 1212 + s.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") 1213 + return 1214 + } 1215 + 1216 + // delete from pds 1217 + 1218 + // htmx fragment of comment after deletion 1219 + return 985 1220 } 986 1221 987 1222 func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) {
+7 -1
appview/state/router.go
··· 73 73 r.Use(AuthMiddleware(s)) 74 74 r.Get("/new", s.NewIssue) 75 75 r.Post("/new", s.NewIssue) 76 - r.Post("/{issue}/comment", s.IssueComment) 76 + r.Post("/{issue}/comment", s.NewIssueComment) 77 + r.Route("/{issue}/comment/{comment_id}/", func(r chi.Router) { 78 + r.Get("/", s.IssueComment) 79 + r.Delete("/", s.DeleteIssueComment) 80 + r.Get("/edit", s.EditIssueComment) 81 + r.Post("/edit", s.EditIssueComment) 82 + }) 77 83 r.Post("/{issue}/close", s.CloseIssue) 78 84 r.Post("/{issue}/reopen", s.ReopenIssue) 79 85 })