Skip to content

Commit b368c97

Browse files
author
“Cengiz
committed
feat: add back button
1 parent ee19270 commit b368c97

File tree

10 files changed

+409
-23
lines changed

10 files changed

+409
-23
lines changed

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ GEM
8585
bindex (0.8.1)
8686
bootsnap (1.19.0)
8787
msgpack (~> 1.2)
88-
brakeman (7.1.1)
88+
brakeman (7.1.2)
8989
racc
9090
builder (3.3.0)
9191
bundler-audit (0.9.3)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<div data-controller="dialog" data-dialog-modal-value="true">
2+
<%# Trigger Button %>
3+
<button type="button"
4+
data-action="click->dialog#open"
5+
class="h-10 sm:h-12 px-4 sm:px-8 font-bold uppercase tracking-widest text-xs sm:text-sm border-[2px] rounded-[1px] transition-all duration-200 shadow-[3px_3px_0px_0px_rgba(0,0,0,1)] sm:shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] hover:shadow-none hover:-translate-y-0.5 active:translate-y-0 bg-white border-zinc-900 text-zinc-900 hover:bg-zinc-100">
6+
Go Back
7+
</button>
8+
9+
<%# Modal Dialog %>
10+
<dialog data-dialog-target="dialog"
11+
class="fixed m-auto inset-0 z-50 w-[90vw] sm:w-[480px] max-w-[480px] p-0 bg-[#F8F5EC] text-zinc-900 shadow-[8px_8px_0px_0px_rgba(24,24,27,1)] border-[3px] border-zinc-900 outline-none backdrop:bg-zinc-900/50 backdrop:backdrop-blur-sm">
12+
13+
<%# Header %>
14+
<div class="flex justify-between items-center px-4 py-3 bg-zinc-900 text-white border-b-[3px] border-zinc-900">
15+
<span class="font-black text-sm uppercase tracking-widest">"CONFIRM"</span>
16+
<button type="button" data-action="click->dialog#close" class="text-white hover:text-zinc-300 transition-colors">
17+
<%= icon_tag "close", class: "text-xl" %>
18+
</button>
19+
</div>
20+
21+
<%# Content %>
22+
<div class="p-6">
23+
<div class="text-center mb-6">
24+
<p class="font-black text-lg uppercase tracking-tight text-zinc-900 mb-2">
25+
"GO BACK?"
26+
</p>
27+
<p class="text-sm text-zinc-600">
28+
You are about to go back to <span class="font-bold text-zinc-900">"<%= previous_phase_name %>"</span>
29+
</p>
30+
</div>
31+
32+
<%# Warning about going back %>
33+
<div class="mb-6 p-4 bg-amber-100 border-2 border-amber-500">
34+
<div class="flex items-start gap-3">
35+
<span class="w-8 h-8 flex items-center justify-center bg-amber-500 text-white font-black text-lg flex-shrink-0 border-2 border-amber-700">!</span>
36+
<div>
37+
<p class="font-bold text-sm uppercase tracking-wide text-amber-800 mb-1">
38+
Participants will be redirected
39+
</p>
40+
<p class="text-sm text-amber-700">
41+
All participants will be taken back to the previous phase.
42+
</p>
43+
</div>
44+
</div>
45+
</div>
46+
47+
<%# Action Buttons %>
48+
<div class="flex gap-3 justify-end">
49+
<button type="button"
50+
data-action="click->dialog#close"
51+
class="h-12 px-6 font-bold uppercase tracking-widest text-sm border-[2px] rounded-[1px] transition-all duration-200 shadow-[3px_3px_0px_0px_rgba(0,0,0,1)] hover:shadow-none bg-white border-zinc-900 text-zinc-900 hover:bg-zinc-100">
52+
Cancel
53+
</button>
54+
55+
<%= button_to retro_phase_back_path(@retro),
56+
method: :post,
57+
class: "h-12 px-6 font-bold uppercase tracking-widest text-sm border-[2px] rounded-[1px] transition-all duration-200 shadow-[3px_3px_0px_0px_rgba(0,0,0,1)] hover:shadow-none bg-zinc-900 border-zinc-900 text-white hover:bg-zinc-700" do %>
58+
Yes, Go Back
59+
<% end %>
60+
</div>
61+
</div>
62+
63+
<%# Footer Decoration %>
64+
<div class="h-2 w-full bg-[repeating-linear-gradient(-45deg,#000,#000_2px,transparent_2px,transparent_4px)] opacity-10"></div>
65+
</dialog>
66+
</div>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
class ConfirmBackPhaseModalComponent < ApplicationComponent
2+
def initialize(retro:)
3+
@retro = retro
4+
end
5+
6+
private
7+
8+
def previous_phase_name
9+
@retro.previous_phase&.to_s&.tr("_", " ")&.upcase || "PREVIOUS PHASE"
10+
end
11+
end
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
class Retros::PhaseBacksController < ApplicationController
2+
include RetroAuthorization
3+
4+
before_action :set_retro
5+
before_action :ensure_retro_admin
6+
7+
def create
8+
@retro.back_phase!
9+
broadcast_phase_redirect
10+
redirect_to phase_path_for(@retro)
11+
end
12+
13+
private
14+
15+
def set_retro
16+
@retro = Current.account.retros.find(params[:retro_id])
17+
end
18+
19+
def phase_path_for(retro)
20+
case retro.phase.to_sym
21+
when :waiting_room then retro_waiting_room_path(retro)
22+
when :action_review then retro_action_review_path(retro)
23+
when :brainstorming then retro_brainstorming_path(retro)
24+
when :grouping then retro_grouping_path(retro)
25+
when :voting then retro_voting_path(retro)
26+
when :discussion then retro_discussion_path(retro)
27+
when :complete then retro_complete_path(retro)
28+
else retro_path(retro)
29+
end
30+
end
31+
32+
def broadcast_phase_redirect
33+
redirect_url = phase_path_for(@retro)
34+
Turbo::StreamsChannel.broadcast_stream_to(
35+
@retro,
36+
content: %(<turbo-stream action="redirect" url="#{redirect_url}"></turbo-stream>)
37+
)
38+
end
39+
end

app/models/retro.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,29 @@ def last_phase?
4545
phase.to_sym == PHASE_ORDER.last
4646
end
4747

48+
def back_phase!
49+
current_index = PHASE_ORDER.index(phase.to_sym)
50+
return if current_index.nil? || current_index <= PHASE_ORDER.index(:brainstorming)
51+
52+
from_phase = phase
53+
prev_phase = PHASE_ORDER[current_index - 1]
54+
update!(phase: prev_phase, highlighted_user_id: nil)
55+
participants.update_all(finished: false)
56+
57+
record_event("retro.phase_changed", particulars: { from: from_phase, to: phase, direction: "back" })
58+
end
59+
60+
def previous_phase
61+
current_index = PHASE_ORDER.index(phase.to_sym)
62+
return nil if current_index.nil? || current_index <= PHASE_ORDER.index(:brainstorming)
63+
PHASE_ORDER[current_index - 1]
64+
end
65+
66+
def can_go_back?
67+
current_index = PHASE_ORDER.index(phase.to_sym)
68+
current_index.present? && current_index > PHASE_ORDER.index(:brainstorming)
69+
end
70+
4871
def add_participant(user, role: :participant)
4972
participants.find_or_create_by!(user: user) do |p|
5073
p.role = role

app/views/layouts/retro.html.erb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@
106106
<% end %>
107107
<% end %>
108108

109+
<%# Go Back button with confirmation modal - admin only, when can go back %>
110+
<% if @retro.admin?(Current.user) && @retro.can_go_back? %>
111+
<%= render ConfirmBackPhaseModalComponent.new(retro: @retro) %>
112+
<% end %>
113+
109114
<%# Complete Phase button with confirmation modal - admin only, not shown on last phase %>
110115
<% if @retro.admin?(Current.user) && !@retro.last_phase? %>
111116
<%= render ConfirmPhaseModalComponent.new(retro: @retro) %>

config/brakeman.ignore

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,5 @@
11
{
2-
"ignored_warnings": [
3-
{
4-
"warning_type": "Dynamic Render Path",
5-
"warning_code": 15,
6-
"fingerprint": "7c922d5793fec09548004f709c579bcec4f00b78bd5ff362bedb8d3148d39a9b",
7-
"check_name": "Render",
8-
"message": "Render path contains parameter value",
9-
"file": "app/views/feedbacks/edit.html.erb",
10-
"line": 53,
11-
"link": "https://brakemanscanner.org/docs/warning_types/dynamic_render_path/",
12-
"code": "render(action => ButtonComponent.new(:variant => :primary, :href => retro_path(Current.account.retros.find(params[:retro_id])), :data => ({ :controller => \"hotkey\", :action => \"keydown.ctrl+enter@document->hotkey#click keydown.meta+enter@document->hotkey#click\" })).with_content(\"Done\"), {})",
13-
"render_path": null,
14-
"location": null,
15-
"user_input": "params[:retro_id]",
16-
"confidence": "Weak",
17-
"note": "False positive - this is rendering a ViewComponent with a safe href attribute, not a dynamic template path"
18-
}
19-
],
20-
"updated": "2025-12-13",
2+
"ignored_warnings": [],
3+
"updated": "2026-01-06",
214
"brakeman_version": "7.1.1"
225
}

config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
resource :finished, only: :update
2525
resource :finish_brainstorming, only: :create
2626
resource :phase_transition, only: :create
27+
resource :phase_back, only: :create
2728
resources :feedback_groups, only: %i[create destroy] do
2829
delete :remove_feedback, on: :collection
2930
end
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
require "test_helper"
2+
3+
class Retros::PhaseBacksControllerTest < ActionDispatch::IntegrationTest
4+
setup do
5+
@retro = retros(:one)
6+
@admin_user = users(:one)
7+
@member_user = users(:two)
8+
# Add member as participant (non-admin)
9+
@retro.add_participant(@member_user, role: :participant)
10+
end
11+
12+
test "admin can go back from grouping to brainstorming" do
13+
sign_in_as :one
14+
@retro.update!(phase: :grouping)
15+
16+
post retro_phase_back_path(@retro)
17+
18+
assert_redirected_to retro_brainstorming_path(@retro)
19+
assert_equal "brainstorming", @retro.reload.phase
20+
end
21+
22+
test "admin can go back from voting to grouping" do
23+
sign_in_as :one
24+
@retro.update!(phase: :voting)
25+
26+
post retro_phase_back_path(@retro)
27+
28+
assert_redirected_to retro_grouping_path(@retro)
29+
assert_equal "grouping", @retro.reload.phase
30+
end
31+
32+
test "admin can go back from discussion to voting" do
33+
sign_in_as :one
34+
@retro.update!(phase: :discussion)
35+
36+
post retro_phase_back_path(@retro)
37+
38+
assert_redirected_to retro_voting_path(@retro)
39+
assert_equal "voting", @retro.reload.phase
40+
end
41+
42+
test "admin can go back from complete to discussion" do
43+
sign_in_as :one
44+
@retro.update!(phase: :complete)
45+
46+
post retro_phase_back_path(@retro)
47+
48+
assert_redirected_to retro_discussion_path(@retro)
49+
assert_equal "discussion", @retro.reload.phase
50+
end
51+
52+
test "non-admin participant cannot go back" do
53+
sign_in_as :two
54+
@retro.update!(phase: :grouping)
55+
56+
post retro_phase_back_path(@retro)
57+
58+
assert_redirected_to retros_path
59+
assert_equal "grouping", @retro.reload.phase
60+
end
61+
62+
test "unauthenticated user cannot go back" do
63+
@retro.update!(phase: :grouping)
64+
65+
post retro_phase_back_path(@retro, script_name: nil)
66+
67+
assert_redirected_to session_menu_path(script_name: nil)
68+
assert_equal "grouping", @retro.reload.phase
69+
end
70+
71+
test "going back does not change phase when already at brainstorming" do
72+
sign_in_as :one
73+
@retro.update!(phase: :brainstorming)
74+
75+
post retro_phase_back_path(@retro)
76+
77+
assert_redirected_to retro_brainstorming_path(@retro)
78+
assert_equal "brainstorming", @retro.reload.phase
79+
end
80+
end

0 commit comments

Comments
 (0)