forge
ExploreLog in

Tahinli/forge

public
0 watching
★ 0
3.8 KiB
1//! Pinning an issue: `POST /{owner}/{repo}/issues/{n}/pin` with `csrf` and `pinned` (`1` pins,
2//! `0` unpins). Triage holders only, through the same guard as the other issue writes. Pinning a
3//! fourth issue re-renders the reason in an error box. A real change adds a `pinned` or `unpinned`
4//! event to the timeline. Also holds the sidebar box that posts here.
5
6use serde::Deserialize;
7use serde_json::Value as Json;
8use topcoat::Result;
9use topcoat::context::{Cx, app_context};
10use topcoat::router::content::Form;
11use topcoat::router::error::{bad_request, see_other};
12use topcoat::router::{page, path_param};
13use topcoat::view::{View, component, view};
14
15use crate::i18n::issues::{self as g, K};
16use crate::i18n::{self, Lang};
17use crate::pages::issue_meta::{
18 action_error_at, item_from_path, item_href, refusal_message, triage_guard,
19};
20use crate::pages::issue_parts::ItemKind;
21use crate::pages::repo_ctx::load_repo;
22use crate::state::AppState;
23use crate::views::shell;
24
25path_param!(owner);
26path_param!(repo);
27path_param!(number);
28
29#[derive(Deserialize)]
30struct PinForm {
31 #[serde(default)]
32 csrf: String,
33 #[serde(default)]
34 pinned: String,
35}
36
37#[page(POST "/{owner}/{repo}/issues/{number}/pin")]
38async fn issue_pin_save(cx: &Cx, Form(form): Form<PinForm>) -> Result<impl View> {
39 let ctx = load_repo(cx, path_param::<Owner>(cx), path_param::<Repo>(cx)).await?;
40 let issue = item_from_path(cx, &ctx, ItemKind::Issue, path_param::<Number>(cx)).await?;
41 let user = triage_guard(cx, &ctx, &form.csrf).await?;
42 let lang = i18n::lang(cx, ctx.viewer.as_ref().map(|(u, _)| u));
43 let pin = match form.pinned.as_str() {
44 "1" => true,
45 "0" => false,
46 _ => return Err(bad_request("pinned must be 1 or 0").into()),
47 };
48 let back = item_href(&ctx, ItemKind::Issue, issue.number);
49 let state = app_context::<AppState>(cx);
50 let result = if pin {
51 state.store.pin_issue(&ctx.repo.id, &issue.id).await
52 } else {
53 state.store.unpin_issue(&ctx.repo.id, &issue.id).await
54 };
55 match result {
56 Ok(changed) => {
57 if changed {
58 let kind = if pin { "pinned" } else { "unpinned" };
59 state
60 .store
61 .add_event(&issue.id, Some(&user.id), kind, Json::Null)
62 .await?;
63 }
64 }
65 Err(e) => {
66 let message = refusal_message(e)?;
67 return Ok(view! { cx =>
68 shell(title: g::t(lang, K::CouldNotSave), viewer: ctx.viewer.as_ref(),
69 action_error_at(lang: lang, message: message, back: back))
70 });
71 }
72 }
73 Err(see_other(back).into())
74}
75
76/// The sidebar box for pinning. Triage holders get the button, which posts the opposite of the
77/// current state; others see whether the issue is pinned.
78#[component]
79pub(crate) async fn pin_box(
80 lang: Lang,
81 write: bool,
82 pinned: bool,
83 action: String,
84 csrf: String,
85) -> Result<impl View> {
86 let next = if pinned { "0" } else { "1" };
87 let label = if pinned {
88 g::t(lang, K::UnpinIssue)
89 } else {
90 g::t(lang, K::PinIssue)
91 };
92 Ok(view! {
93 <section class="issue-side-section">
94 <h2>(g::t(lang, K::Pinned))</h2>
95 if write {
96 <form method="post" action=(action.as_str()) class="stack">
97 <input type="hidden" name="csrf" value=(csrf.as_str())>
98 <input type="hidden" name="pinned" value=(next)>
99 <div><button type="submit">(label)</button></div>
100 </form>
101 } else {
102 if pinned {
103 <p>(g::t(lang, K::PinnedTopNote))</p>
104 } else {
105 <p class="muted small">(g::t(lang, K::NotPinned))</p>
106 }
107 }
108 </section>
109 })
110}