Monorepo for Tangled tangled.org

appview/pulls: add functionality to edit pull title and description #1131

open opened by murex.tngl.sh targeting master from murex.tngl.sh/tangled: feat/pull-edit
  • add a pencil icon in UI
  • add corresponding route to edit PR title and description

closes: #175

Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:owyua2lvxbs55wyhs22dqu2s/sh.tangled.repo.pull/3mgoby3czel22
+227 -2
Diff #0
+19
appview/db/pulls.go
··· 644 644 return err 645 645 } 646 646 647 + func UpdatePullTitleBody(e Execer, repoAt syntax.ATURI, pullId int, title, body string) error { 648 + _, err := e.Exec( 649 + `update pulls set title = ?, body = ? where repo_at = ? and pull_id = ?`, 650 + title, body, repoAt, pullId, 651 + ) 652 + return err 653 + } 654 + 655 + func UpdatePullTitleBodyAndRefs(tx *sql.Tx, pull *models.Pull) error { 656 + _, err := tx.Exec( 657 + `update pulls set title = ?, body = ? where repo_at = ? and pull_id = ?`, 658 + pull.Title, pull.Body, pull.RepoAt, pull.PullId, 659 + ) 660 + if err != nil { 661 + return err 662 + } 663 + return putReferences(tx, pull.AtUri(), pull.References) 664 + } 665 + 647 666 func ResubmitPull(e Execer, pullAt syntax.ATURI, newRoundNumber int, newPatch string, combinedPatch string, newSourceRev string) error { 648 667 _, err := e.Exec(` 649 668 insert into pull_submissions (pull_at, round_number, patch, combined, source_rev)
+10
appview/pages/pages.go
··· 1202 1202 return p.executeRepo("repo/pulls/pull", w, params) 1203 1203 } 1204 1204 1205 + type EditPullParams struct { 1206 + LoggedInUser *oauth.MultiAccountUser 1207 + RepoInfo repoinfo.RepoInfo 1208 + Pull *models.Pull 1209 + } 1210 + 1211 + func (p *Pages) EditPullFragment(w io.Writer, params EditPullParams) error { 1212 + return p.executePlain("repo/pulls/fragments/putPull", w, params) 1213 + } 1214 + 1205 1215 type RepoPullPatchParams struct { 1206 1216 LoggedInUser *oauth.MultiAccountUser 1207 1217 RepoInfo repoinfo.RepoInfo
+12 -2
appview/pages/templates/repo/pulls/fragments/pullHeader.html
··· 1 1 {{ define "repo/pulls/fragments/pullHeader" }} 2 + <div id="pull-{{ .Pull.PullId }}"> 2 3 <header class="pb-2"> 3 4 <h1 class="text-2xl dark:text-white"> 4 5 {{ .Pull.Title | description }} ··· 55 56 </span> 56 57 {{ end }} 57 58 </span> 59 + {{ if and .LoggedInUser (eq .LoggedInUser.Did .Pull.OwnerDid) }} 60 + <a 61 + class="text-gray-500 dark:text-gray-400 flex gap-1 items-center group cursor-pointer" 62 + hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/edit" 63 + hx-swap="innerHTML" 64 + hx-target="#pull-{{ .Pull.PullId }}" 65 + title="Edit pull request"> 66 + {{ i "pencil" "size-3" }} 67 + </a> 68 + {{ end }} 58 69 </div> 59 70 60 71 {{ if .Pull.Body }} ··· 70 81 "ThreadAt" .Pull.AtUri) }} 71 82 </div> 72 83 </section> 73 - 74 - 84 + </div> 75 85 {{ end }}
+44
appview/pages/templates/repo/pulls/fragments/putPull.html
··· 1 + {{ define "repo/pulls/fragments/putPull" }} 2 + <form 3 + hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/edit" 4 + hx-trigger="submit, keydown[(ctrlKey || metaKey) && key=='Enter'] from:(#title,#body)" 5 + hx-swap="none" 6 + hx-indicator="#pull-edit-spinner"> 7 + <div class="flex flex-col gap-2"> 8 + <div> 9 + <label for="title">title</label> 10 + <input type="text" name="title" id="title" class="w-full" value="{{ .Pull.Title }}" /> 11 + </div> 12 + <div> 13 + <label for="body">body</label> 14 + <textarea 15 + name="body" 16 + id="body" 17 + rows="15" 18 + class="w-full resize-y" 19 + placeholder="Describe your pull request. Markdown is supported." 20 + >{{ .Pull.Body }}</textarea> 21 + </div> 22 + <div class="flex justify-between"> 23 + <div id="pull-edit-error" class="error"></div> 24 + <div class="flex gap-2 items-center"> 25 + <a 26 + class="btn flex items-center gap-2 no-underline hover:no-underline" 27 + type="button" 28 + href="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}" 29 + > 30 + {{ i "x" "w-4 h-4" }} 31 + cancel 32 + </a> 33 + <button type="submit" class="btn-create flex items-center gap-2"> 34 + {{ i "pencil" "w-4 h-4" }} 35 + edit pull request 36 + <span id="pull-edit-spinner" class="group"> 37 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 38 + </span> 39 + </button> 40 + </div> 41 + </div> 42 + </div> 43 + </form> 44 + {{ end }}
+124
appview/pulls/pulls.go
··· 2343 2343 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId)) 2344 2344 } 2345 2345 2346 + func (s *Pulls) EditPull(w http.ResponseWriter, r *http.Request) { 2347 + l := s.logger.With("handler", "EditPull") 2348 + user := s.oauth.GetMultiAccountUser(r) 2349 + noticeId := "pull-edit-error" 2350 + 2351 + _, err := s.repoResolver.Resolve(r) 2352 + if err != nil { 2353 + l.Error("failed to resolve repo", "err", err) 2354 + s.pages.Notice(w, noticeId, "Failed to edit pull.") 2355 + return 2356 + } 2357 + 2358 + pull, ok := r.Context().Value("pull").(*models.Pull) 2359 + if !ok { 2360 + l.Error("failed to get pull") 2361 + s.pages.Notice(w, noticeId, "Failed to edit pull.") 2362 + return 2363 + } 2364 + 2365 + if user.Active.Did != pull.OwnerDid { 2366 + s.pages.Notice(w, noticeId, "Only the pull request author can edit it.") 2367 + return 2368 + } 2369 + 2370 + switch r.Method { 2371 + case http.MethodGet: 2372 + s.pages.EditPullFragment(w, pages.EditPullParams{ 2373 + LoggedInUser: user, 2374 + RepoInfo: s.repoResolver.GetRepoInfo(r, user), 2375 + Pull: pull, 2376 + }) 2377 + case http.MethodPost: 2378 + updatedPull := *pull 2379 + updatedPull.Title = r.FormValue("title") 2380 + updatedPull.Body = r.FormValue("body") 2381 + updatedPull.Mentions, updatedPull.References = s.mentionsResolver.Resolve(r.Context(), updatedPull.Body) 2382 + 2383 + if err := s.validator.ValidatePull(&updatedPull); err != nil { 2384 + l.Error("validation error", "err", err) 2385 + s.pages.Notice(w, noticeId, fmt.Sprintf("Failed to edit pull: %s", err)) 2386 + return 2387 + } 2388 + 2389 + client, err := s.oauth.AuthorizedClient(r) 2390 + if err != nil { 2391 + l.Error("failed to get authorized client", "err", err) 2392 + s.pages.Notice(w, noticeId, "Failed to edit pull.") 2393 + return 2394 + } 2395 + 2396 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, pull.OwnerDid, pull.Rkey) 2397 + if err != nil { 2398 + l.Error("failed to get record", "err", err) 2399 + s.pages.Notice(w, noticeId, "Failed to edit pull, no record found on PDS.") 2400 + return 2401 + } 2402 + 2403 + var existingRecord tangled.RepoPull 2404 + bytes, err := ex.Value.MarshalJSON() 2405 + if err != nil { 2406 + l.Error("failed to marshal existing record", "err", err) 2407 + s.pages.Notice(w, noticeId, "Failed to edit pull.") 2408 + return 2409 + } 2410 + if err := json.Unmarshal(bytes, &existingRecord); err != nil { 2411 + l.Error("failed to decode existing record", "err", err) 2412 + s.pages.Notice(w, noticeId, "Failed to edit pull.") 2413 + return 2414 + } 2415 + 2416 + existingRecord.Title = updatedPull.Title 2417 + if updatedPull.Body != "" { 2418 + existingRecord.Body = &updatedPull.Body 2419 + } else { 2420 + existingRecord.Body = nil 2421 + } 2422 + existingRecord.Mentions = make([]string, len(updatedPull.Mentions)) 2423 + for i, did := range updatedPull.Mentions { 2424 + existingRecord.Mentions[i] = string(did) 2425 + } 2426 + existingRecord.References = make([]string, len(updatedPull.References)) 2427 + for i, uri := range updatedPull.References { 2428 + existingRecord.References[i] = string(uri) 2429 + } 2430 + 2431 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 2432 + Collection: tangled.RepoPullNSID, 2433 + Repo: pull.OwnerDid, 2434 + Rkey: pull.Rkey, 2435 + SwapRecord: ex.Cid, 2436 + Record: &lexutil.LexiconTypeDecoder{ 2437 + Val: &existingRecord, 2438 + }, 2439 + }) 2440 + if err != nil { 2441 + l.Error("failed to edit record on PDS", "err", err) 2442 + s.pages.Notice(w, noticeId, "Failed to edit pull on PDS.") 2443 + return 2444 + } 2445 + 2446 + tx, err := s.db.BeginTx(r.Context(), nil) 2447 + if err != nil { 2448 + l.Error("failed to begin transaction", "err", err) 2449 + s.pages.Notice(w, noticeId, "Failed to edit pull.") 2450 + return 2451 + } 2452 + defer tx.Rollback() 2453 + 2454 + if err := db.UpdatePullTitleBodyAndRefs(tx, &updatedPull); err != nil { 2455 + l.Error("failed to update pull in DB", "err", err) 2456 + s.pages.Notice(w, noticeId, "Failed to edit pull.") 2457 + return 2458 + } 2459 + 2460 + if err = tx.Commit(); err != nil { 2461 + l.Error("failed to commit", "err", err) 2462 + s.pages.Notice(w, noticeId, "Failed to edit pull.") 2463 + return 2464 + } 2465 + 2466 + s.pages.HxRefresh(w) 2467 + } 2468 + } 2469 + 2346 2470 func (s *Pulls) ClosePull(w http.ResponseWriter, r *http.Request) { 2347 2471 user := s.oauth.GetMultiAccountUser(r) 2348 2472
+2
appview/pulls/router.go
··· 41 41 42 42 r.Group(func(r chi.Router) { 43 43 r.Use(middleware.AuthMiddleware(s.oauth)) 44 + r.Get("/edit", s.EditPull) 45 + r.Post("/edit", s.EditPull) 44 46 r.Route("/resubmit", func(r chi.Router) { 45 47 r.Get("/", s.ResubmitPull) 46 48 r.Post("/", s.ResubmitPull)
+16
appview/validator/pull.go
··· 1 + package validator 2 + 3 + import ( 4 + "fmt" 5 + "strings" 6 + 7 + "tangled.org/core/appview/models" 8 + ) 9 + 10 + func (v *Validator) ValidatePull(pull *models.Pull) error { 11 + if pull.Title == "" || strings.TrimSpace(v.sanitizer.SanitizeDescription(pull.Title)) == "" { 12 + return fmt.Errorf("pull title is empty") 13 + } 14 + 15 + return nil 16 + }

History

1 round 0 comments
sign up or login to add to the discussion
murex.tngl.sh submitted #0
1 commit
expand
appview/pulls: add functionality to edit pull title and description
no conflicts, ready to merge
expand 0 comments