Skip to content

Commit 9d1bfb7

Browse files
authored
UX: Show contextual dates on GitHub PR oneboxes (#36637)
Previously, GitHub PR oneboxes always showed "opened on [date]" regardless of the PR's status. This could be misleading for merged or closed PRs where the relevant date is when that action occurred. Now the date label and value match the PR status: - open/draft: "opened/drafted on [created_at]" - approved/changes_requested: "[status] on [review submitted_at]" - merged/closed: "merged/closed on [merged_at/closed_at]" Also removes an unused GITHUB_COMMENT_REGEX constant from github_repo_onebox.rb. **Here are a few examples** <img width="743" height="1035" alt="CleanShot 2025-12-11 at 18 10 42" src="https://github.com/user-attachments/assets/810893a5-2563-4a19-96a1-7bf876e2d27c" />
1 parent cd3d1c3 commit 9d1bfb7

File tree

5 files changed

+104
-45
lines changed

5 files changed

+104
-45
lines changed

config/locales/server.en.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5567,6 +5567,13 @@ en:
55675567
changes_requested: "Changes Requested"
55685568
merged: "Merged Pull Request"
55695569
closed: "Closed Pull Request"
5570+
status_date:
5571+
open: "opened"
5572+
draft: "drafted"
5573+
approved: "approved"
5574+
changes_requested: "changes requested"
5575+
merged: "merged"
5576+
closed: "closed"
55705577
55715578
discourse_push_notifications:
55725579
popup:

lib/onebox/engine/github_pull_request_onebox.rb

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,16 @@ def match
3333
def data
3434
result = raw(github_auth_header(match[:org])).clone
3535
result["link"] = link
36-
result["pr_status"] = fetch_pr_status(result)
36+
37+
status_data = fetch_pr_status(result)
38+
result["pr_status"] = status_data&.dig(:status)
3739
result["pr_status_title"] = pr_status_title(result["pr_status"])
3840

39-
created_at = Time.parse(result["created_at"])
40-
result["created_at"] = created_at.strftime("%I:%M%p - %d %b %y %Z")
41-
result["created_at_date"] = created_at.strftime("%F")
42-
result["created_at_time"] = created_at.strftime("%T")
41+
status_timestamp = status_data&.dig(:timestamp) || result["created_at"]
42+
status_date = Time.parse(status_timestamp)
43+
result["status_date"] = status_date.strftime("%I:%M%p - %d %b %y %Z")
44+
result["status_date_date"] = status_date.strftime("%F")
45+
result["status_date_time"] = status_date.strftime("%T")
4346

4447
ulink = URI(link)
4548
_, org, repo = ulink.path.split("/")
@@ -59,6 +62,7 @@ def data
5962
end
6063

6164
result["i18n"] = i18n
65+
result["i18n"]["status_date_label"] = status_date_label(result["pr_status"])
6266
result["i18n"]["pr_summary"] = I18n.t(
6367
"onebox.github.pr_summary",
6468
{
@@ -85,6 +89,11 @@ def i18n
8589
}
8690
end
8791

92+
def status_date_label(status)
93+
key = status.presence || "opened"
94+
I18n.t("onebox.github.status_date.#{key}", default: I18n.t("onebox.github.opened"))
95+
end
96+
8897
def pr_status_title(status)
8998
key = status.presence || "default"
9099
I18n.t("onebox.github.pr_title.#{key}")
@@ -126,30 +135,37 @@ def load_json(url)
126135
def fetch_pr_status(pr_data)
127136
return unless SiteSetting.github_pr_status_enabled
128137

129-
return "merged" if pr_data["merged"]
130-
return "closed" if pr_data["state"] == "closed"
131-
return "draft" if pr_data["draft"]
138+
return { status: "merged", timestamp: pr_data["merged_at"] } if pr_data["merged"]
139+
return { status: "closed", timestamp: pr_data["closed_at"] } if pr_data["state"] == "closed"
140+
return { status: "draft", timestamp: pr_data["created_at"] } if pr_data["draft"]
132141

133142
reviews_data = load_json(url + "/reviews")
134-
review_states = latest_review_states(reviews_data)
143+
latest_reviews = latest_review_states_with_timestamps(reviews_data)
135144

136-
return "changes_requested" if review_states.include?("CHANGES_REQUESTED")
137-
return "approved" if review_states.include?("APPROVED")
145+
%w[CHANGES_REQUESTED APPROVED].each do |state|
146+
reviews = latest_reviews.select { |r| r[:state] == state }
147+
if reviews.present?
148+
return { status: state.downcase, timestamp: reviews.map { |r| r[:timestamp] }.max }
149+
end
150+
end
138151

139-
"open"
152+
{ status: "open", timestamp: pr_data["created_at"] }
140153
rescue StandardError => e
141154
Rails.logger.warn("GitHub PR status fetch error: #{e.message}")
142155
nil
143156
end
144157

145-
def latest_review_states(reviews)
158+
def latest_review_states_with_timestamps(reviews)
146159
return [] if reviews.blank?
147160

148161
reviews
149-
.reject { |r| r.dig("user", "id").nil? || %w[PENDING COMMENTED].include?(r["state"]) }
162+
.reject do |r|
163+
r.dig("user", "id").nil? || !%w[CHANGES_REQUESTED APPROVED].include?(r["state"])
164+
end
150165
.group_by { |r| r.dig("user", "id") }
151-
.transform_values { |rs| rs.max_by { |r| r["submitted_at"] }["state"] }
166+
.transform_values { |rs| rs.max_by { |r| r["submitted_at"] } }
152167
.values
168+
.map { |r| { state: r["state"], timestamp: r["submitted_at"] } }
153169
end
154170
end
155171
end

lib/onebox/engine/github_repo_onebox.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ class GithubRepoOnebox
1111
include JSON
1212
include Onebox::Mixins::GithubAuthHeader
1313

14-
GITHUB_COMMENT_REGEX = /(<!--.*?-->\r\n)/m
15-
1614
matches_domain("github.com", "www.github.com")
1715
always_https
1816

lib/onebox/templates/githubpullrequest.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
{{#pr}}
7474
<div class="github-info">
7575
<div class="date">
76-
{{i18n.opened}} <span class="discourse-local-date" data-format="ll" data-date="{{created_at_date}}" data-time="{{created_at_time}}" data-timezone="UTC">{{created_at}}</span>
76+
{{i18n.status_date_label}} <span class="discourse-local-date" data-format="ll" data-date="{{status_date_date}}" data-time="{{status_date_time}}" data-timezone="UTC">{{status_date}}</span>
7777
</div>
7878

7979
<div class="user">

plugins/discourse-github/spec/lib/github_pr_onebox_status_spec.rb

Lines changed: 65 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -40,34 +40,43 @@ def onebox_html
4040
stub_request(:get, reviews_api_uri).to_return(status: 200, body: "[]")
4141
end
4242

43-
it "includes open status" do
44-
expect(onebox_html).to include("--gh-status-open")
43+
it "includes open status and shows created_at date" do
44+
html = onebox_html
45+
expect(html).to include("--gh-status-open")
46+
expect(html).to include(I18n.t("onebox.github.status_date.open"))
47+
expect(html).to include('data-date="2013-07-26"')
4548
end
4649
end
4750

4851
context "when PR is merged" do
4952
before do
5053
resp = open_pr_response
5154
resp["merged"] = true
55+
resp["merged_at"] = "2024-02-15T10:30:00Z"
5256
stub_request(:get, api_uri).to_return(status: 200, body: MultiJson.dump(resp))
53-
stub_request(:get, reviews_api_uri).to_return(status: 200, body: "[]")
5457
end
5558

56-
it "includes merged status" do
57-
expect(onebox_html).to include("--gh-status-merged")
59+
it "includes merged status and shows merged_at date" do
60+
html = onebox_html
61+
expect(html).to include("--gh-status-merged")
62+
expect(html).to include(I18n.t("onebox.github.status_date.merged"))
63+
expect(html).to include('data-date="2024-02-15"')
5864
end
5965
end
6066

6167
context "when PR is closed" do
6268
before do
6369
resp = open_pr_response
6470
resp["state"] = "closed"
71+
resp["closed_at"] = "2024-03-20T14:45:00Z"
6572
stub_request(:get, api_uri).to_return(status: 200, body: MultiJson.dump(resp))
66-
stub_request(:get, reviews_api_uri).to_return(status: 200, body: "[]")
6773
end
6874

69-
it "includes closed status" do
70-
expect(onebox_html).to include("--gh-status-closed")
75+
it "includes closed status and shows closed_at date" do
76+
html = onebox_html
77+
expect(html).to include("--gh-status-closed")
78+
expect(html).to include(I18n.t("onebox.github.status_date.closed"))
79+
expect(html).to include('data-date="2024-03-20"')
7180
end
7281
end
7382

@@ -76,25 +85,36 @@ def onebox_html
7685
resp = open_pr_response
7786
resp["draft"] = true
7887
stub_request(:get, api_uri).to_return(status: 200, body: MultiJson.dump(resp))
79-
stub_request(:get, reviews_api_uri).to_return(status: 200, body: "[]")
8088
end
8189

82-
it "includes draft status" do
83-
expect(onebox_html).to include("--gh-status-draft")
90+
it "includes draft status and shows created_at date" do
91+
html = onebox_html
92+
expect(html).to include("--gh-status-draft")
93+
expect(html).to include(I18n.t("onebox.github.status_date.draft"))
94+
expect(html).to include('data-date="2013-07-26"')
8495
end
8596
end
8697

8798
context "when PR is approved" do
8899
before do
89100
stub_request(:get, api_uri).to_return(status: 200, body: MultiJson.dump(open_pr_response))
90101
reviews = [
91-
{ "user" => { "id" => 1 }, "state" => "APPROVED", "submitted_at" => Time.now.iso8601 },
102+
{
103+
"user" => {
104+
"id" => 1,
105+
},
106+
"state" => "APPROVED",
107+
"submitted_at" => "2024-04-10T09:15:00Z",
108+
},
92109
]
93110
stub_request(:get, reviews_api_uri).to_return(status: 200, body: MultiJson.dump(reviews))
94111
end
95112

96-
it "includes approved status" do
97-
expect(onebox_html).to include("--gh-status-approved")
113+
it "includes approved status and shows review submitted_at date" do
114+
html = onebox_html
115+
expect(html).to include("--gh-status-approved")
116+
expect(html).to include(I18n.t("onebox.github.status_date.approved"))
117+
expect(html).to include('data-date="2024-04-10"')
98118
end
99119
end
100120

@@ -107,36 +127,48 @@ def onebox_html
107127
"id" => 1,
108128
},
109129
"state" => "CHANGES_REQUESTED",
110-
"submitted_at" => Time.now.iso8601,
130+
"submitted_at" => "2024-05-05T16:20:00Z",
111131
},
112132
]
113133
stub_request(:get, reviews_api_uri).to_return(status: 200, body: MultiJson.dump(reviews))
114134
end
115135

116-
it "includes changes_requested status" do
117-
expect(onebox_html).to include("--gh-status-changes_requested")
136+
it "includes changes_requested status and shows review submitted_at date" do
137+
html = onebox_html
138+
expect(html).to include("--gh-status-changes_requested")
139+
expect(html).to include(I18n.t("onebox.github.status_date.changes_requested"))
140+
expect(html).to include('data-date="2024-05-05"')
118141
end
119142
end
120143

121144
context "when PR has both approval and changes requested from different reviewers" do
122145
before do
123146
stub_request(:get, api_uri).to_return(status: 200, body: MultiJson.dump(open_pr_response))
124147
reviews = [
125-
{ "user" => { "id" => 1 }, "state" => "APPROVED", "submitted_at" => Time.now.iso8601 },
148+
{
149+
"user" => {
150+
"id" => 1,
151+
},
152+
"state" => "APPROVED",
153+
"submitted_at" => "2024-05-30T12:00:00Z",
154+
},
126155
{
127156
"user" => {
128157
"id" => 2,
129158
},
130159
"state" => "CHANGES_REQUESTED",
131-
"submitted_at" => Time.now.iso8601,
160+
"submitted_at" => "2024-06-01T12:00:00Z",
132161
},
133162
]
134163
stub_request(:get, reviews_api_uri).to_return(status: 200, body: MultiJson.dump(reviews))
135164
end
136165

137-
it "shows changes_requested (takes priority over approved)" do
138-
expect(onebox_html).to include("--gh-status-changes_requested")
139-
expect(onebox_html).not_to include("--gh-status-approved")
166+
it "shows changes_requested status with its date (takes priority over approved)" do
167+
html = onebox_html
168+
expect(html).to include("--gh-status-changes_requested")
169+
expect(html).not_to include("--gh-status-approved")
170+
expect(html).to include(I18n.t("onebox.github.status_date.changes_requested"))
171+
expect(html).to include('data-date="2024-06-01"')
140172
end
141173
end
142174

@@ -162,9 +194,12 @@ def onebox_html
162194
stub_request(:get, reviews_api_uri).to_return(status: 200, body: MultiJson.dump(reviews))
163195
end
164196

165-
it "shows approved (latest review wins)" do
166-
expect(onebox_html).to include("--gh-status-approved")
167-
expect(onebox_html).not_to include("--gh-status-changes_requested")
197+
it "shows approved status with latest review date (latest review wins)" do
198+
html = onebox_html
199+
expect(html).to include("--gh-status-approved")
200+
expect(html).not_to include("--gh-status-changes_requested")
201+
expect(html).to include(I18n.t("onebox.github.status_date.approved"))
202+
expect(html).to include('data-date="2024-01-02"')
168203
end
169204
end
170205

@@ -174,8 +209,11 @@ def onebox_html
174209
stub_request(:get, reviews_api_uri).to_return(status: 500, body: "error")
175210
end
176211

177-
it "falls back gracefully without status" do
178-
expect(onebox_html).not_to include("--gh-status-")
212+
it "falls back gracefully without status class and shows opened with created_at date" do
213+
html = onebox_html
214+
expect(html).not_to include("--gh-status-")
215+
expect(html).to include(I18n.t("onebox.github.status_date.open"))
216+
expect(html).to include('data-date="2013-07-26"')
179217
end
180218
end
181219
end

0 commit comments

Comments
 (0)