.forge/prs/0234b26d-75dc-4bd6-8ed6-38030290b89f/patch.diff 10.3 KiB 217 lines Raw
1 From 8791f8574b3631fdf79830dbde85ad517bef6fee Mon Sep 17 00:00:00 20012 From: gl0w <gl0w@world>3 Date: Thu, 10 Sep 2026 06:51:07 +01004 Subject: [PATCH] m6/submit-buttons: + links on issues/pulls + form intros (1185  tests)6 7 ---8  src/forge.rs     | 64 ++++++++++++++++++++++++++++++++++++++++++++++--9  src/inbox.rs     | 48 +++++++++++++++++++++++++++++++++---10  static/style.css |  8 ++++++11  3 files changed, 115 insertions(+), 5 deletions(-)12 13 diff --git a/src/forge.rs b/src/forge.rs14 index 61a34a7..951136b 10064415 --- a/src/forge.rs16 +++ b/src/forge.rs17 @@ -948,8 +948,9 @@ fn render_issues_index(18      description: Option<&str>,19  ) -> Result<String, String> {20      let mut body = String::from("<h1>Issues</h1>\n");21 +    body.push_str("<p><a class=\"btn-new\" href=\"/new-issue.html\">+ New issue</a></p>\n");22      if data.issues.is_empty() {23 -        body.push_str("<p class=\"muted\">No issues yet.</p>\n");24 +        body.push_str("<p class=\"muted\">No issues yet. <a href=\"/new-issue.html\">Suggest one</a> — no account needed.</p>\n");25      } else {26          body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>comments</th><th>created</th></tr></thead>\n<tbody>\n");27          for issue in &data.issues {28 @@ -994,8 +995,9 @@ fn render_issues_index(29  30  fn render_pulls_index(prs: &[Pr], site_name: &str, nav: &str, description: Option<&str>) -> String {31      let mut body = String::from("<h1>Pulls</h1>\n");32 +    body.push_str("<p><a class=\"btn-new\" href=\"/new-pr.html\">+ New PR prompt</a></p>\n");33      if prs.is_empty() {34 -        body.push_str("<p class=\"muted\">No pull requests yet.</p>\n");35 +        body.push_str("<p class=\"muted\">No pull requests yet. <a href=\"/new-pr.html\">Suggest one</a> — no account needed.</p>\n");36      } else {37          body.push_str("<div class=\"table-scroll\"><table>\n<thead><tr><th>#</th><th>title</th><th>state</th><th>head→base</th><th>created</th></tr></thead>\n<tbody>\n");38          for pr in prs {39 @@ -1391,6 +1393,64 @@ mod tests {40          }41      }42  43 +    /// f0rge #25: index pages carry prominent `+` submit buttons (correct44 +    /// hrefs, icon + words) and the empty states link to the forms.45 +    #[test]46 +    fn submit_buttons_on_issues_pulls_indexes() {47 +        // Empty forge: buttons + linked empty states.48 +        let repo = mktemp("submit-empty");49 +        let out = mktemp("submit-empty-out");50 +        render_loaded(&load(&repo), &out, "test", "<nav>", None).expect("render");51 +        let issues = std::fs::read_to_string(out.join("issues/index.html")).expect("issues");52 +        let pulls = std::fs::read_to_string(out.join("pulls/index.html")).expect("pulls");53 +        assert!(54 +            issues.contains("<a class=\"btn-new\" href=\"/new-issue.html\">+ New issue</a>"),55 +            "issues + button"56 +        );57 +        assert!(58 +            pulls.contains("<a class=\"btn-new\" href=\"/new-pr.html\">+ New PR prompt</a>"),59 +            "pulls + button"60 +        );61 +        assert!(62 +            issues.contains("No issues yet.")63 +                && issues.contains("href=\"/new-issue.html\">Suggest one"),64 +            "issues empty state links to form"65 +        );66 +        assert!(67 +            pulls.contains("No pull requests yet.")68 +                && pulls.contains("href=\"/new-pr.html\">Suggest one"),69 +            "pulls empty state links to form"70 +        );71 +        // Non-empty forge: buttons stay above the tables.72 +        write_forge(73 +            &repo,74 +            "issues/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/issue.md",75 +            "---\nid: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa\ntitle: T\nstate: open\ncreated: 2026-09-10T03:00:00Z\n---\nbody\n",76 +        );77 +        write_forge(78 +            &repo,79 +            "prs/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/pr.md",80 +            "---\nid: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb\ntitle: P\nstate: open\nbase: main\nhead: fix\ncreated: 2026-09-10T03:00:00Z\n---\ndesc\n",81 +        );82 +        let out2 = mktemp("submit-full-out");83 +        render_loaded(&load(&repo), &out2, "test", "<nav>", None).expect("render");84 +        let issues2 = std::fs::read_to_string(out2.join("issues/index.html")).expect("issues");85 +        let pulls2 = std::fs::read_to_string(out2.join("pulls/index.html")).expect("pulls");86 +        assert!(87 +            issues2.contains("href=\"/new-issue.html\">+ New issue"),88 +            "issues + button with rows"89 +        );90 +        assert!(91 +            pulls2.contains("href=\"/new-pr.html\">+ New PR prompt"),92 +            "pulls + button with rows"93 +        );94 +        assert!(95 +            issues2.contains("table-scroll"),96 +            "issues table still renders"97 +        );98 +        assert!(pulls2.contains("table-scroll"), "pulls table still renders");99 +    }100 +101      #[test]102      fn verdict_badge_shapes() {103          assert!(verdict_badge("pass").contains("badge pass"));104 diff --git a/src/inbox.rs b/src/inbox.rs105 index 4998fb0..9806475 100644106 --- a/src/inbox.rs107 +++ b/src/inbox.rs108 @@ -593,7 +593,7 @@ pub fn store_submission(109  // ---- forms (built into dist, zero JS) ----------------------------------110  111  fn license_block() -> String {112 -    let mut out = String::from("<blockquote><p>");113 +    let mut out = String::from("<blockquote id=\"license-full\"><p>");114      let lines: Vec<&str> = LICENSE_TEXT.split('\n').collect();115      for (i, line) in lines.iter().enumerate() {116          if i > 0 {117 @@ -618,6 +618,9 @@ fn form_page(118  ) -> String {119      let body = format!(120          "<h1>{heading}</h1>\n<p>{intro}</p>\n\121 +         <p>What happens: your text goes to a private inbox only the maintainer reads — \122 +         never published on this site. No account needed. The server answers a one-time \123 +         ack id and shows nothing back.</p>\n\124           <form method=\"post\" action=\"{action}\">\n\125           <p><label for=\"f-title\">Title (required, up to 300 chars)</label><br>\n\126           <input type=\"text\" id=\"f-title\" name=\"title\" maxlength=\"300\" required size=\"60\"></p>\n\127 @@ -631,6 +634,9 @@ fn form_page(128           <p><label>Leave this empty: <input type=\"text\" name=\"website\" value=\"\" autocomplete=\"off\" tabindex=\"-1\"></label></p>\n\129           </div>\n\130           {license}\131 +         <p>In plain words: you hand your text over completely — the project can keep, share, \132 +         or change it for any purpose, and you keep no rights and are owed nothing. \133 +         See the <a href=\"#license-full\">full grant text</a> above.</p>\n\134           <p><label><input type=\"checkbox\" name=\"license\" value=\"agree\" required> \135           I agree to the grant above (required — unchecked submissions are rejected).</label></p>\n\136           <p><button type=\"submit\">Submit</button></p>\n\137 @@ -663,7 +669,7 @@ pub fn render_forms(138          site_name,139          nav,140          "Suggest an issue",141 -        "Detailed actionable text from your user. Free textbox on purpose — say what a maintainer can act on.",142 +        "Spotted something worth fixing? Write it up here in plain words — say what a maintainer can act on.",143          InboxType::Issue.route(),144          "body",145          "Body",146 @@ -673,7 +679,7 @@ pub fn render_forms(147          site_name,148          nav,149          "Suggest a change",150 -        "ONE prompt, plain text. No files, no diff, no branch. Code words inside stay text — never applied, never executed.",151 +        "Have an idea for a fix? Send ONE prompt as plain text — no files, no diff, no branch. Code words inside stay text: never applied, never executed.",152          InboxType::PrPrompt.route(),153          "prompt",154          "Prompt",155 @@ -1050,4 +1056,40 @@ mod tests {156          }157          std::fs::remove_dir_all(&dir).ok();158      }159 +160 +    /// f0rge #25: submission pages explain the deal in human words161 +    /// (private inbox, ack id, no account) plus a plain license summary162 +    /// linking to the full grant text — with the full checkbox kept.163 +    #[test]164 +    fn forms_explain_deal_and_summarize_license() {165 +        let dir = std::env::temp_dir().join(format!(166 +            "hu13-inbox-forms-deal-{}-{}",167 +            std::process::id(),168 +            std::time::SystemTime::now()169 +                .duration_since(std::time::UNIX_EPOCH)170 +                .map(|d| d.as_nanos())171 +                .unwrap_or(0)172 +        ));173 +        std::fs::create_dir_all(&dir).unwrap();174 +        render_forms(&dir, "test", "", None).unwrap();175 +        let issue = std::fs::read_to_string(dir.join("new-issue.html")).unwrap();176 +        let pr = std::fs::read_to_string(dir.join("new-pr.html")).unwrap();177 +        for html in [&issue, &pr] {178 +            assert!(html.contains("private inbox"), "private suggestion");179 +            assert!(html.contains("ack id"), "ack id explained");180 +            assert!(html.contains("No account needed"), "no account needed");181 +            assert!(html.contains("In plain words:"), "license summary");182 +            assert!(183 +                html.contains("href=\"#license-full\""),184 +                "summary links to full text"185 +            );186 +            assert!(html.contains("id=\"license-full\""), "full text anchor");187 +            assert!(html.contains("name=\"license\""), "checkbox kept");188 +            assert!(189 +                html.contains("irrevocably give it to this project"),190 +                "full grant text kept"191 +            );192 +        }193 +        std::fs::remove_dir_all(&dir).ok();194 +    }195  }196 diff --git a/static/style.css b/static/style.css197 index 32f05a8..11a2346 100644198 --- a/static/style.css199 +++ b/static/style.css200 @@ -59,6 +59,14 @@ button,input[type=submit]{display:inline-flex;align-items:center;justify-content201    min-height:40px;min-width:40px;background:#7aa2f7;color:#16161e;border:1px solid #7aa2f7;202    padding:.5rem 1rem;font:inherit;font-weight:700;cursor:pointer}203  button:hover,input[type=submit]:hover{background:#7dcfff}204 +/* Submit buttons (m6/submit-buttons, f0rge #25): link styled as a205 +   button — Tokyo Night accent fill, >=40px touch target, icon + words. */206 +.btn-new{display:inline-flex;align-items:center;justify-content:center;207 +  min-height:40px;min-width:40px;background:#7aa2f7;color:#16161e;208 +  border:1px solid #7aa2f7;padding:.5rem 1rem;font:inherit;font-weight:700;209 +  text-decoration:none;cursor:pointer}210 +.btn-new:visited{color:#16161e}211 +.btn-new:hover,.btn-new:focus{background:#7dcfff;text-decoration:none}212  .muted{color:#565f89}213  .badge{display:inline-block;border:1px solid #292e42;padding:.1em .5em;font-size:.85em}214  .badge.pass{color:#9ece6a}.badge.fail{color:#f7768e}215 -- 216 2.55.0217