Skip to content

Commit 6254ec1

Browse files
authored
Merge pull request #84 from codeGROOVE-dev/reliable
Improve dashboard styling
2 parents d638b01 + 53584c4 commit 6254ec1

File tree

2 files changed

+150
-97
lines changed

2 files changed

+150
-97
lines changed

pkg/home/ui.go

Lines changed: 138 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func BuildBlocks(dashboard *Dashboard, userTZ string) []slack.Block {
2727
slack.NewButtonBlockElement(
2828
"refresh_dashboard",
2929
"refresh",
30-
slack.NewTextBlockObject("plain_text", "🔄 Refresh Dashboard", true, false),
30+
slack.NewTextBlockObject("plain_text", "🔄 Refresh", true, false),
3131
).WithStyle("primary"),
3232
),
3333
)
@@ -52,15 +52,19 @@ func BuildBlocks(dashboard *Dashboard, userTZ string) []slack.Block {
5252
if len(dashboard.WorkspaceOrgs) > 0 {
5353
blocks = append(blocks, slack.NewDividerBlock())
5454

55+
// Sort orgs alphabetically
56+
sortedOrgs := make([]string, len(dashboard.WorkspaceOrgs))
57+
copy(sortedOrgs, dashboard.WorkspaceOrgs)
58+
sort.Strings(sortedOrgs)
59+
5560
var orgLines []string
56-
for _, org := range dashboard.WorkspaceOrgs {
61+
for _, org := range sortedOrgs {
5762
// URL-escape org name to prevent injection
5863
esc := url.PathEscape(org)
59-
orgLine := fmt.Sprintf("• <%s|%s> [<%s|config>, <%s|dashboard>]",
60-
fmt.Sprintf("https://github.com/%s", esc),
64+
orgLine := fmt.Sprintf("• %s [<%s|dashboard> | <%s|config>]",
6165
org,
62-
fmt.Sprintf("https://github.com/%s/.github/blob/main/.codeGROOVE/slack.yaml", esc),
6366
fmt.Sprintf("https://%s.ready-to-review.dev", esc),
67+
fmt.Sprintf("https://github.com/%s/.codeGROOVE/blob/main/slack.yaml", esc),
6468
)
6569
orgLines = append(orgLines, orgLine)
6670
}
@@ -81,6 +85,49 @@ func BuildBlocks(dashboard *Dashboard, userTZ string) []slack.Block {
8185
return blocks
8286
}
8387

88+
// formatPRLine formats a single PR line with indicator, repo, number, title, and optional action.
89+
func formatPRLine(pr PR, isIncoming bool) string {
90+
// Extract repo name
91+
repo := pr.Repository
92+
if idx := strings.LastIndex(repo, "/"); idx >= 0 {
93+
repo = repo[idx+1:]
94+
}
95+
96+
// Determine indicator
97+
var indicator string
98+
if isIncoming {
99+
switch {
100+
case pr.NeedsReview, pr.IsBlocked:
101+
indicator = ":red_circle:"
102+
case pr.ActionKind != "":
103+
indicator = ":speech_balloon:"
104+
default:
105+
indicator = ":white_small_square:"
106+
}
107+
} else {
108+
switch {
109+
case pr.NeedsReview, pr.IsBlocked:
110+
indicator = ":large_green_circle:"
111+
case pr.ActionKind != "":
112+
indicator = ":speech_balloon:"
113+
default:
114+
indicator = ":white_small_square:"
115+
}
116+
}
117+
118+
// Build line
119+
line := fmt.Sprintf("%s <%s|%s#%d> • %s", indicator, pr.URL, repo, pr.Number, pr.Title)
120+
if pr.ActionKind != "" {
121+
action := strings.ReplaceAll(pr.ActionKind, "_", " ")
122+
// Bold the action if this PR is blocked on the user
123+
if pr.IsBlocked || (isIncoming && pr.NeedsReview) {
124+
action = "*" + action + "*"
125+
}
126+
line = fmt.Sprintf("%s — %s", line, action)
127+
}
128+
return line
129+
}
130+
84131
// BuildPRSections creates Block Kit blocks for PR sections (incoming/outgoing).
85132
// Format inspired by goose - simple, minimal, action-focused.
86133
// This is the core formatting used by both daily reports and the dashboard.
@@ -108,32 +155,7 @@ func BuildPRSections(incoming, outgoing []PR) []slack.Block {
108155
if prs[i].IsBlocked || prs[i].NeedsReview {
109156
n++
110157
}
111-
112-
// Format PR line - extract repo name
113-
repo := prs[i].Repository
114-
if idx := strings.LastIndex(repo, "/"); idx >= 0 {
115-
repo = repo[idx+1:]
116-
}
117-
118-
// Determine indicator
119-
var indicator string
120-
switch {
121-
case prs[i].NeedsReview:
122-
indicator = ":red_circle:"
123-
case prs[i].IsBlocked:
124-
indicator = ":red_circle:"
125-
case prs[i].ActionKind != "":
126-
indicator = ":speech_balloon:"
127-
default:
128-
indicator = ":white_small_square:"
129-
}
130-
131-
// Build line
132-
line := fmt.Sprintf("%s <%s|%s#%d> • %s", indicator, prs[i].URL, repo, prs[i].Number, prs[i].Title)
133-
if prs[i].ActionKind != "" {
134-
line = fmt.Sprintf("%s — %s", line, strings.ReplaceAll(prs[i].ActionKind, "_", " "))
135-
}
136-
lines = append(lines, line)
158+
lines = append(lines, formatPRLine(prs[i], true))
137159
}
138160

139161
// Build header
@@ -154,6 +176,15 @@ func BuildPRSections(incoming, outgoing []PR) []slack.Block {
154176
)
155177
}
156178

179+
// Spacer between Incoming and Outgoing sections
180+
if len(incoming) > 0 && len(outgoing) > 0 {
181+
blocks = append(blocks,
182+
slack.NewContextBlock("",
183+
slack.NewTextBlockObject("plain_text", " ", false, false),
184+
),
185+
)
186+
}
187+
157188
// Outgoing PRs section
158189
if len(outgoing) > 0 {
159190
// Sort: blocked first, then by most recent within each group
@@ -175,32 +206,7 @@ func BuildPRSections(incoming, outgoing []PR) []slack.Block {
175206
if prs[i].IsBlocked {
176207
n++
177208
}
178-
179-
// Format PR line - extract repo name
180-
repo := prs[i].Repository
181-
if idx := strings.LastIndex(repo, "/"); idx >= 0 {
182-
repo = repo[idx+1:]
183-
}
184-
185-
// Determine indicator
186-
var indicator string
187-
switch {
188-
case prs[i].NeedsReview:
189-
indicator = ":large_green_circle:"
190-
case prs[i].IsBlocked:
191-
indicator = ":large_green_circle:"
192-
case prs[i].ActionKind != "":
193-
indicator = ":speech_balloon:"
194-
default:
195-
indicator = ":white_small_square:"
196-
}
197-
198-
// Build line
199-
line := fmt.Sprintf("%s <%s|%s#%d> • %s", indicator, prs[i].URL, repo, prs[i].Number, prs[i].Title)
200-
if prs[i].ActionKind != "" {
201-
line = fmt.Sprintf("%s — %s", line, strings.ReplaceAll(prs[i].ActionKind, "_", " "))
202-
}
203-
lines = append(lines, line)
209+
lines = append(lines, formatPRLine(prs[i], false))
204210
}
205211

206212
// Build header
@@ -224,46 +230,80 @@ func BuildPRSections(incoming, outgoing []PR) []slack.Block {
224230
return blocks
225231
}
226232

227-
// BuildBlocksWithDebug creates Slack Block Kit UI with debug information about user mapping.
233+
// BuildBlocksWithDebug creates Slack Block Kit UI with GitHub username integrated into the header.
228234
func BuildBlocksWithDebug(dashboard *Dashboard, userTZ string, mapping *usermapping.ReverseMapping) []slack.Block {
229-
// Build standard blocks first
230-
blocks := BuildBlocks(dashboard, userTZ)
235+
var blocks []slack.Block
236+
237+
// Header with GitHub username if available
238+
headerText := "🚀 Ready to Review"
239+
if mapping != nil {
240+
headerText = fmt.Sprintf("🚀 Ready to Review — @%s", mapping.GitHubUsername)
241+
}
242+
243+
blocks = append(blocks,
244+
slack.NewHeaderBlock(
245+
slack.NewTextBlockObject("plain_text", headerText, true, false),
246+
),
247+
// Refresh button
248+
slack.NewActionBlock(
249+
"refresh_actions",
250+
slack.NewButtonBlockElement(
251+
"refresh_dashboard",
252+
"refresh",
253+
slack.NewTextBlockObject("plain_text", "🔄 Refresh", true, false),
254+
).WithStyle("primary"),
255+
),
256+
slack.NewDividerBlock(),
257+
)
258+
259+
// PR sections
260+
blocks = append(blocks, BuildPRSections(dashboard.IncomingPRs, dashboard.OutgoingPRs)...)
261+
262+
// Add spacing after PR sections if we have content
263+
if len(dashboard.IncomingPRs) > 0 || len(dashboard.OutgoingPRs) > 0 {
264+
blocks = append(blocks,
265+
slack.NewContextBlock("",
266+
slack.NewTextBlockObject("plain_text", " ", false, false),
267+
),
268+
)
269+
}
270+
271+
// Organizations section - only show if there are orgs configured
272+
if len(dashboard.WorkspaceOrgs) > 0 {
273+
blocks = append(blocks, slack.NewDividerBlock())
274+
275+
// Sort orgs alphabetically
276+
sortedOrgs := make([]string, len(dashboard.WorkspaceOrgs))
277+
copy(sortedOrgs, dashboard.WorkspaceOrgs)
278+
sort.Strings(sortedOrgs)
279+
280+
var orgLines []string
281+
for _, org := range sortedOrgs {
282+
// URL-escape org name to prevent injection
283+
esc := url.PathEscape(org)
284+
orgLine := fmt.Sprintf("• %s [<%s|dashboard> | <%s|config>]",
285+
org,
286+
fmt.Sprintf("https://%s.ready-to-review.dev", esc),
287+
fmt.Sprintf("https://github.com/%s/.codeGROOVE/blob/main/slack.yaml", esc),
288+
)
289+
orgLines = append(orgLines, orgLine)
290+
}
231291

232-
// Add debug section based on mapping status
233-
switch {
234-
case mapping != nil:
235292
blocks = append(blocks,
236-
slack.NewDividerBlock(),
237293
slack.NewSectionBlock(
238294
slack.NewTextBlockObject("mrkdwn",
239-
fmt.Sprintf("🔍 *Debug Info*\n"+
240-
"GitHub: `@%s` • Mapped via: `%s` • Confidence: `%d%%`",
241-
mapping.GitHubUsername,
242-
mapping.MatchMethod,
243-
mapping.Confidence),
295+
"*Organizations*\n"+strings.Join(orgLines, "\n"),
244296
false,
245297
false,
246298
),
247299
nil,
248300
nil,
249301
),
250302
)
303+
}
251304

252-
// Add mapping guidance if confidence is low
253-
if mapping.Confidence < 80 {
254-
blocks = append(blocks,
255-
slack.NewContextBlock("",
256-
slack.NewTextBlockObject("mrkdwn",
257-
fmt.Sprintf("⚠️ Low confidence mapping. Add manual override to `slack.yaml`:\n```yaml\nusers:\n %s: %s\n```",
258-
mapping.GitHubUsername,
259-
mapping.SlackEmail),
260-
false,
261-
false,
262-
),
263-
),
264-
)
265-
}
266-
case len(dashboard.WorkspaceOrgs) == 0:
305+
// Add helpful messages for error cases
306+
if mapping == nil && len(dashboard.WorkspaceOrgs) == 0 {
267307
// No organizations configured for this workspace (likely startup/race condition)
268308
blocks = append(blocks,
269309
slack.NewDividerBlock(),
@@ -282,7 +322,7 @@ func BuildBlocksWithDebug(dashboard *Dashboard, userTZ string, mapping *usermapp
282322
nil,
283323
),
284324
)
285-
default:
325+
} else if mapping == nil {
286326
// User mapping failed
287327
blocks = append(blocks,
288328
slack.NewDividerBlock(),
@@ -299,6 +339,19 @@ func BuildBlocksWithDebug(dashboard *Dashboard, userTZ string, mapping *usermapp
299339
)
300340
}
301341

342+
// Updated timestamp at the bottom
343+
now := formatTimestamp(time.Now(), userTZ)
344+
blocks = append(blocks,
345+
slack.NewDividerBlock(),
346+
slack.NewContextBlock("",
347+
slack.NewTextBlockObject("mrkdwn",
348+
fmt.Sprintf("Updated %s", now),
349+
false,
350+
false,
351+
),
352+
),
353+
)
354+
302355
return blocks
303356
}
304357

pkg/home/ui_test.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -325,9 +325,9 @@ func TestBuildPRSections_SortOrder(t *testing.T) {
325325

326326
blocks := BuildPRSections(incoming, outgoing)
327327

328-
// Should have 2 blocks (incoming and outgoing sections)
329-
if len(blocks) != 2 {
330-
t.Fatalf("expected 2 blocks, got %d", len(blocks))
328+
// Should have 3 blocks (incoming section, spacer, outgoing section)
329+
if len(blocks) != 3 {
330+
t.Fatalf("expected 3 blocks (incoming, spacer, outgoing), got %d", len(blocks))
331331
}
332332

333333
// Check incoming section order
@@ -366,10 +366,10 @@ func TestBuildPRSections_SortOrder(t *testing.T) {
366366
t.Error("newer non-blocked PR#3 should appear before older non-blocked PR#1")
367367
}
368368

369-
// Check outgoing section order
370-
outgoingBlock, ok := blocks[1].(*slack.SectionBlock)
369+
// Check outgoing section order (index 2, after spacer at index 1)
370+
outgoingBlock, ok := blocks[2].(*slack.SectionBlock)
371371
if !ok {
372-
t.Fatal("expected second block to be SectionBlock")
372+
t.Fatal("expected third block to be SectionBlock")
373373
}
374374
outgoingText := outgoingBlock.Text.Text
375375

@@ -457,9 +457,9 @@ func TestBuildBlocksWithDebug_NoOrgsConfigured(t *testing.T) {
457457
MatchMethod: "email_match",
458458
Confidence: 95,
459459
},
460-
expectText: "Debug Info",
461-
notExpect: "Low confidence mapping",
462-
description: "should show debug info without low confidence warning",
460+
expectText: "Updated",
461+
notExpect: "Could not map Slack user to GitHub",
462+
description: "should show timestamp without error messages",
463463
},
464464
{
465465
name: "successful mapping with low confidence",
@@ -474,9 +474,9 @@ func TestBuildBlocksWithDebug_NoOrgsConfigured(t *testing.T) {
474474
MatchMethod: "name_similarity",
475475
Confidence: 65,
476476
},
477-
expectText: "Low confidence mapping",
478-
notExpect: "",
479-
description: "should show low confidence warning with suggestion",
477+
expectText: "Updated",
478+
notExpect: "Could not map Slack user to GitHub",
479+
description: "should show timestamp without error messages regardless of confidence",
480480
},
481481
}
482482

0 commit comments

Comments
 (0)