Skip to content

Commit 76d3e74

Browse files
mateusdeaparieljJuanVqz
authored
[ROAD 534] Add state to stories (#309)
* Add nokogiri for apple silicon * Add `approved` field to the stories table * Add `pending` method with specs * Add `update_story` controller action * Add frontend * Use path helper instead of url * Change button color with approval status * Use enums for story status * Use remote: true for status button and refactor routes * Add appropriate specs and modify existing ones to the new changes * Move story related css to appropriate file * Change back to headless firefox * Refactor routes * Apply suggestions from code review Co-authored-by: Juan Vásquez <[email protected]> * Add status grid-area in scss * Refactor label update logic * Move status field to last position * Remove Rejected as an option on story creation * Add specs to test status selection on story creation * Replace `row` for query selector --------- Co-authored-by: Ariel Juodziukynas <[email protected]> Co-authored-by: Juan Vásquez <[email protected]>
1 parent eaecfb4 commit 76d3e74

File tree

20 files changed

+261
-27
lines changed

20 files changed

+261
-27
lines changed

app/assets/javascripts/stories.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,20 @@ document.addEventListener("DOMContentLoaded", () => {
2626
});
2727
});
2828
});
29+
30+
function updateStatusButton(color, status) {
31+
const button = document.querySelector(".story-title .dropdown-wrapper > button");
32+
button.className = `button ${color}`;
33+
34+
const span = button.querySelector("span");
35+
span.textContent = status;
36+
37+
document.querySelector(":focus").blur();
38+
}
39+
40+
function updateStatusLabel(status, storyId) {
41+
let row = document.getElementById(`story_${storyId}`)
42+
status_label = row.querySelector(".status > .story-status-badge")
43+
status_label.textContent = status
44+
status_label.classList.value = `story-status-badge ${status}`
45+
}

app/assets/stylesheets/3-atoms/_badges.scss

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,29 @@
2323
background-color: #57ce81;
2424
}
2525
}
26+
27+
.story-status-badge {
28+
display: inline-block;
29+
padding: 7px 15px;
30+
font-weight: 600;
31+
line-height: 1;
32+
text-align: center;
33+
white-space: nowrap;
34+
vertical-align: baseline;
35+
border-radius: 20px;
36+
font-size: 11px;
37+
background-color: $orange;
38+
39+
&.approved {
40+
background-color: $green;
41+
border-color: #1d4ed8;
42+
color: $white;
43+
}
44+
45+
&.rejected {
46+
background-color: $magenta;
47+
border-color: #d77e72;
48+
color: $white;
49+
}
50+
51+
}

app/assets/stylesheets/4-molecules/_tables.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
}
1212
.project-table__row {
1313
display: grid;
14-
grid-template-columns: 1fr 70px 70px 260px;
14+
grid-template-columns: 1fr 100px 70px 70px 260px;
1515
align-items: center;
1616
padding: 10px 0;
1717
&.project-table__row--reports {

app/assets/stylesheets/stories.scss

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010
word-break: break-all;
1111
}
1212

13-
.story-description, .extra-info, .story_preview {
13+
.story-description,
14+
.extra-info,
15+
.story_preview {
1416
margin-bottom: 25px;
1517
font-size: 15px;
16-
p{
18+
p {
1719
margin-top: 1em;
1820
}
1921
em {
@@ -55,7 +57,6 @@
5557
margin-bottom: 1.5em;
5658
}
5759

58-
5960
.modal p {
6061
padding-bottom: 1.3em;
6162
}
@@ -72,6 +73,7 @@
7273
"title preview"
7374
"description preview"
7475
"extra extra-preview"
76+
"status ."
7577
"submit .";
7678
grid-template-columns: repeat(2, minmax(50%, 1fr));
7779
grid-column-gap: 10px;
@@ -94,6 +96,10 @@
9496
&.story_extra_info {
9597
grid-area: extra;
9698
}
99+
100+
&.story_status {
101+
grid-area: status;
102+
}
97103
}
98104

99105
.story_preview {
@@ -104,7 +110,8 @@
104110
grid-area: extra-preview;
105111
}
106112

107-
.extra_info_preview .content, .story_preview .content {
113+
.extra_info_preview .content,
114+
.story_preview .content {
108115
overflow: auto;
109116
max-height: min(50vh, 700px);
110117
// prevent long links from overflowing
@@ -131,6 +138,12 @@
131138
padding: 5px;
132139
}
133140

141+
.story-title {
142+
display: flex;
143+
align-items: center;
144+
gap: 1rem;
145+
}
146+
134147
.comments-section {
135148
margin: 16px 0;
136149

app/controllers/stories_controller.rb

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
require "csv"
22
class StoriesController < ApplicationController
33
before_action :authenticate_user!
4-
before_action :find_project, except: [:bulk_destroy, :render_markdown, :edit, :update, :destroy, :show, :move]
5-
before_action :find_story, only: [:edit, :update, :destroy, :show, :move]
4+
before_action :find_project, except: [:bulk_destroy, :render_markdown, :edit, :update, :destroy, :show, :move, :approve, :reject, :pending]
5+
before_action :find_story, only: [:edit, :update, :destroy, :show, :move, :approve, :reject, :pending]
66
before_action :validate_url_product_id, only: [:edit, :update, :destroy, :show, :move]
77
before_action :ensure_unarchived!, except: [:show, :bulk_destroy, :render_markdown, :move]
88

@@ -127,6 +127,27 @@ def move
127127
redirect_to @project
128128
end
129129

130+
def approve
131+
@story.approved!
132+
respond_to do |format|
133+
format.js { render "shared/update_status" }
134+
end
135+
end
136+
137+
def reject
138+
@story.rejected!
139+
respond_to do |format|
140+
format.js { render "shared/update_status" }
141+
end
142+
end
143+
144+
def pending
145+
@story.pending!
146+
respond_to do |format|
147+
format.js { render "shared/update_status" }
148+
end
149+
end
150+
130151
private
131152

132153
def find_project
@@ -143,7 +164,7 @@ def validate_url_product_id
143164
end
144165

145166
def stories_params
146-
params.require(:story).permit(:title, :description, :extra_info, :project_id)
167+
params.require(:story).permit(:title, :description, :extra_info, :project_id, :status)
147168
end
148169

149170
def expected_csv_headers?(file)

app/helpers/stories_helper.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,18 @@
11
module StoriesHelper
2+
def status_label(story)
3+
"<span class='story-status-badge #{story.status}'>#{story.status}</span>".html_safe
4+
end
5+
6+
def status_color(story)
7+
return "green" if @story.approved?
8+
return "magenta" if @story.rejected?
9+
10+
"orange"
11+
end
12+
13+
def options_for_status_select(story, action)
14+
return options_for_select({"Pending" => "pending", "Approved" => "approved"}, selected: story.status) if action == "new"
15+
16+
options_for_select({"Pending" => "pending", "Approved" => "approved", "Rejected" => "rejected"}, selected: story.status)
17+
end
218
end

app/models/story.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ class Story < ApplicationRecord
88

99
before_create :add_position
1010

11+
enum :status, [:pending, :approved, :rejected]
12+
1113
scope :by_position, -> { order("stories.position ASC NULLS FIRST, stories.created_at ASC") }
1214

1315
def best_estimate_average
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
let row = document.getElementById("story_<%= estimate.story_id %>")
2-
row.children[1].innerText = "<%= j(estimate.best_case_points.to_s) %>"
3-
row.children[2].innerText = "<%= j(estimate.worst_case_points.to_s) %>"
1+
updateStatusLabel("<%= estimate.story.status %>", "<%= estimate.story_id %>")
42

5-
let totals_row = document.querySelector('.project-table tfoot tr')
6-
totals_row.children[1].innerText = "<%= j @project.best_estimate_sum_per_user(current_user) %>"
7-
totals_row.children[2].innerText = "<%= j @project.worst_estimate_sum_per_user(current_user) %>"
3+
document.getElementById("best_estimate_<%= estimate.story_id %>").innerText = "<%= j(estimate.best_case_points.to_s) %>"
4+
document.getElementById("worst_estimate_<%= estimate.story_id %>").innerText = "<%= j(estimate.worst_case_points.to_s) %>"
5+
6+
document.querySelector('.project-table tfoot tr > .best_estimates_total').innerText = "<%= j @project.best_estimate_sum_per_user(current_user) %>"
7+
document.querySelector('.project-table tfoot tr > .worst_estimates_total').innerText = "<%= j @project.worst_estimate_sum_per_user(current_user) %>"

app/views/estimates/create.js.erb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
(function(){
22
<% if @estimate.persisted? %>
33
<%= render partial: 'update_row', locals: {estimate: @estimate} %>
4-
const addEstimate = row.querySelector('.add-estimate')
4+
const addEstimate = document.getElementById("story_<%= @estimate.story_id %>").querySelector('.add-estimate')
55
addEstimate.insertAdjacentHTML('afterend', "<%= j(link_to 'Edit Estimate', edit_project_story_estimate_path(@project.id, @estimate.story, @estimate.id), class: "button edit-estimate", remote: true) %>")
66
addEstimate.remove()
77
closeModal()
88
<% else %>
99
updateModal("New estimate", "<%= j(render partial: 'modal_body') %>")
1010
<% end %>
11-
})()
11+
})()

app/views/projects/show.html.erb

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<thead class="table-header fixed-header">
1515
<tr class="project-table__row project-table__row--header">
1616
<th class="project-table__cell">Story Title</th>
17+
<th class="project-table__cell">Status</th>
1718
<th class="project-table__cell">Best<br />Estimate</th>
1819
<th class="project-table__cell">Worst<br />Estimate</th>
1920
<th class="project-table__cell story_actions">
@@ -25,14 +26,15 @@
2526
<tbody id="stories" data-url="<%= sort_stories_project_path(@project.id) %>" data-unlocked="<%= is_unlocked?(@project) %>">
2627
<% if @stories.present? %>
2728
<% @stories.each do | story | %>
28-
<tr class="project-table__row project-table__row--story" id="<%= dom_id(story)%>" >
29+
<tr class="project-table__row project-table__row--story" id="<%= dom_id(story)%>" data-status="<%= story.status %>">
2930
<td class="project-table__cell">
3031
<span class="popup">Copied to clipboard</span>
3132
<input type="checkbox" name="stories[]" value="<%= story.id %>">
3233
<%= link_to "#{story.id} - #{story.title}", [story.project, story] %>
3334
</td>
34-
<td class="project-table__cell"><%= story.estimate_for(current_user)&.best_case_points %></td>
35-
<td class="project-table__cell"><%= story.estimate_for(current_user)&.worst_case_points %></td>
35+
<td class="project-table__cell status"><%= status_label(story) %></td>
36+
<td class="project-table__cell" id="best_estimate_<%= story.id %>"><%= story.estimate_for(current_user)&.best_case_points %></td>
37+
<td class="project-table__cell" id="worst_estimate_<%= story.id %>"><%= story.estimate_for(current_user)&.worst_case_points %></td>
3638
<td class="project-table__cell story_actions">
3739
<% if is_unlocked?(@project) %>
3840
<% if estimated(story) %>
@@ -81,8 +83,9 @@
8183
<tfoot>
8284
<tr class="project-table__row project-table__row--footer">
8385
<td class="project-table__cell">Total estimates</td>
84-
<td class="project-table__cell"><%= @project.best_estimate_sum_per_user(current_user) %></td>
85-
<td class="project-table__cell"><%= @project.worst_estimate_sum_per_user(current_user) %></td>
86+
<td class="project-table__cell"></td>
87+
<td class="project-table__cell best_estimates_total"><%= @project.best_estimate_sum_per_user(current_user) %></td>
88+
<td class="project-table__cell worst_estimates_total"><%= @project.worst_estimate_sum_per_user(current_user) %></td>
8689
<td class="project-table__cell"></td>
8790
</tr>
8891
</tfoot>

0 commit comments

Comments
 (0)