Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions internal/http/handlers/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,8 +514,22 @@ func (h *ProfileHandler) SetGuildChannel(w http.ResponseWriter, r *http.Request)

redirectPath := "/dashboard/servers/" + guildID
if r.Header.Get("HX-Request") == "true" {
w.Header().Set("HX-Redirect", redirectPath)
w.WriteHeader(http.StatusOK)
var registeredCount int64
if count, err := h.discordReg.CountRegisteredUsers(r.Context(), guildID); err != nil {
slog.WarnContext(r.Context(), "set guild channel: count registered users", "guild_id", guildID, "error", err)
} else {
registeredCount = count
}
data := map[string]any{
"GuildID": guildID,
"IsAdmin": true,
"ChannelConfigured": true,
"ChannelName": channelName,
"CurrentChannelID": channelID,
"TextChannels": channels,
"RegisteredCount": registeredCount,
}
h.renderer.Partial(w, http.StatusOK, "channel-region", data)
return
}
http.Redirect(w, r, redirectPath, http.StatusSeeOther)
Expand Down
37 changes: 36 additions & 1 deletion internal/http/handlers/profile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ func newProfileRealTemplateRenderer(t *testing.T) *handlers.TemplateRenderer {
}

func serveProfileRequest(t *testing.T, user db.User, handler http.HandlerFunc, method, target string, form url.Values) *httptest.ResponseRecorder {
t.Helper()
return serveProfileRequestWithHeaders(t, user, handler, method, target, form, nil)
}

func serveProfileRequestWithHeaders(t *testing.T, user db.User, handler http.HandlerFunc, method, target string, form url.Values, headers map[string]string) *httptest.ResponseRecorder {
t.Helper()
body := strings.NewReader("")
if form != nil {
Expand All @@ -135,6 +140,9 @@ func serveProfileRequest(t *testing.T, user db.User, handler http.HandlerFunc, m
if form != nil {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
}
for k, v := range headers {
req.Header.Set(k, v)
}
tp, err := auth.IssueAccessToken(user.ID, user.Email, testJWTSecret, 15*time.Minute)
if err != nil {
t.Fatalf("IssueAccessToken: %v", err)
Expand Down Expand Up @@ -173,7 +181,7 @@ func TestGuildPageRendersChannelSelectForManager(t *testing.T) {
body := string(bodyBytes)
for _, want := range []string{
`hx-post="/dashboard/servers/guild-1/channel"`,
`<select name="channel_id"`,
`<select id="channel-select" name="channel_id"`,
`<option value="chan-1"`,
`#general`,
`<option value="chan-2" selected`,
Expand Down Expand Up @@ -247,6 +255,33 @@ func TestSetGuildChannelAuthorizationAndValidation(t *testing.T) {
}
})

t.Run("manager with valid channel htmx request returns partial without redirect", func(t *testing.T) {
discordReg := &fakeDiscordRegistration{channels: []discord.Channel{{ID: "chan-1", Name: "general"}, {ID: "chan-2", Name: "training"}}, registeredCount: 7}
oauthSvc := &fakeProfileOAuth{
memberships: []app.GuildMembership{{GuildID: guildID, GuildName: "Test Guild", IsAdmin: true}},
identity: db.OauthIdentity{ProviderUserID: "discord-user-1"},
}
h := handlers.NewProfileHandler(&fakeProfileUsers{user: user}, oauthSvc, discordReg, "", newProfileRealTemplateRenderer(t), false)

rr := serveProfileRequestWithHeaders(t, user, h.SetGuildChannel, http.MethodPost, "/dashboard/servers/"+guildID+"/channel", url.Values{"channel_id": {"chan-2"}}, map[string]string{"HX-Request": "true"})
if rr.Code != http.StatusOK {
t.Fatalf("status = %d, want 200", rr.Code)
}
if got := rr.Header().Get("HX-Redirect"); got != "" {
t.Fatalf("HX-Redirect = %q, want empty", got)
}
bodyBytes, _ := io.ReadAll(rr.Result().Body)
body := string(bodyBytes)
for _, want := range []string{`id="channel-region"`, `#training`, `hx-post="/dashboard/servers/guild-1/channel"`} {
if !strings.Contains(body, want) {
t.Fatalf("partial missing %q in:\n%s", want, body)
}
}
if len(discordReg.setChannelCalls) != 1 {
t.Fatalf("SetChannel calls = %d, want 1", len(discordReg.setChannelCalls))
}
})

t.Run("empty channel is rejected", func(t *testing.T) {
discordReg := &fakeDiscordRegistration{channels: []discord.Channel{{ID: "chan-1", Name: "general"}}}
oauthSvc := &fakeProfileOAuth{
Expand Down
31 changes: 1 addition & 30 deletions web/templates/dashboard-server.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,36 +34,7 @@ <h2 class="card-title text-xl">{{.GuildName}}</h2>
{{if .IsAdmin}}<span class="badge badge-outline">manager</span>{{end}}
</div>

{{if .ChannelConfigured}}
{{if .ChannelName}}
<div class="text-sm text-base-content/70">Posting results to <span class="font-medium">#{{.ChannelName}}</span></div>
{{else}}
<div class="text-sm text-base-content/70">A results channel is configured.</div>
{{end}}
{{else}}
<div class="text-sm text-base-content/70">
No channel configured yet. Go to the channel you want results in and run
<code class="rounded bg-base-300 px-1.5 py-0.5 text-xs">/setchannel</code>.
</div>
{{end}}

{{if .IsAdmin}}
<div class="text-sm text-base-content/70">Users registered: <span class="font-medium">{{.RegisteredCount}}</span></div>
{{if .TextChannels}}
<form hx-post="/dashboard/servers/{{.GuildID}}/channel" hx-disable="find button" class="flex flex-col gap-2 sm:flex-row sm:items-end">
<label class="form-control w-full max-w-xs">
<span class="label-text">Reporting channel</span>
<select name="channel_id" class="select select-bordered w-full">
{{$currentChannelID := .CurrentChannelID}}
{{range .TextChannels}}
<option value="{{.ID}}" {{if eq .ID $currentChannelID}}selected{{end}}>#{{.Name}}</option>
{{end}}
</select>
</label>
<button type="submit" class="btn btn-sm btn-outline w-fit">Save reporting channel</button>
</form>
{{end}}
{{end}}
{{template "channel-region" .}}

{{if .IsRegistered}}
<div class="text-base-content/70">You're registered to have results sent to this server.</div>
Expand Down
72 changes: 72 additions & 0 deletions web/templates/partials/channel-region.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{{define "channel-region"}}
<div id="channel-region" class="flex flex-col gap-2">
{{if .ChannelConfigured}}
{{if .ChannelName}}
<div class="text-sm text-base-content/70">Posting results to <span class="font-medium">#{{.ChannelName}}</span></div>
{{else}}
<div class="text-sm text-base-content/70">A results channel is configured.</div>
{{end}}
{{else}}
<div class="text-sm text-base-content/70">
No channel configured yet. Go to the channel you want results in and run
<code class="rounded bg-base-300 px-1.5 py-0.5 text-xs">/setchannel</code>.
</div>
{{end}}

{{if .IsAdmin}}
<div class="text-sm text-base-content/70">Users registered: <span class="font-medium">{{.RegisteredCount}}</span></div>
{{if .TextChannels}}
<form id="channel-form" class="flex flex-col gap-2 sm:flex-row sm:items-end">
<label class="form-control w-full max-w-xs">
<span class="label-text">Reporting channel</span>
<select id="channel-select" name="channel_id" class="select select-bordered w-full">
{{$currentChannelID := .CurrentChannelID}}
{{range .TextChannels}}
<option value="{{.ID}}" {{if eq .ID $currentChannelID}}selected{{end}}>#{{.Name}}</option>
{{end}}
</select>
</label>
<button type="button" class="btn btn-sm btn-outline w-fit" onclick="var select=document.getElementById('channel-select'); var target=document.getElementById('channel-confirm-new-channel'); if (select && target) { target.textContent = select.options[select.selectedIndex] ? select.options[select.selectedIndex].text : 'the selected channel'; } document.getElementById('channel-confirm-modal').showModal();">Save reporting channel</button>
</form>

<dialog id="channel-confirm-modal" class="modal">
<div class="modal-box">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" aria-label="Close">✕</button>
</form>
<h3 class="text-lg font-bold">Save reporting channel?</h3>
<div class="mt-4 space-y-2 text-sm text-base-content/70">
<p>
Current channel:
<span class="font-medium">{{if .ChannelName}}#{{.ChannelName}}{{else}}none configured{{end}}</span>
</p>
<p>
New channel:
<span id="channel-confirm-new-channel" class="font-medium">the selected channel</span>
</p>
<p class="alert alert-warning text-sm">
RowBot must be granted permission to post in this channel.
</p>
</div>
<div class="modal-action">
<form method="dialog">
<button class="btn btn-sm btn-ghost">Cancel</button>
</form>
<button type="submit"
form="channel-form"
hx-post="/dashboard/servers/{{.GuildID}}/channel"
hx-include="#channel-select"
hx-target="#channel-region"
hx-swap="outerHTML"
hx-disable="this"
class="btn btn-sm btn-primary">Confirm</button>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
{{end}}
{{end}}
</div>
{{end}}