diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index e8eae375..f72f4f6d 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -1,29 +1,23 @@ document.addEventListener("turbolinks:load", function () { $("input[name='stories[]']").click(() => { - const selected = $("input[name='stories[]']:checked"); - const is_unlocked = $("#stories").data("unlocked"); - if (!is_unlocked) { - return; - } - - if (selected.length > 0) { - const ending = selected.length == 1 ? "y" : "ies"; - $("#bulk_delete") - .text(`Bulk Delete (${selected.length} Stor${ending})`) - .attr("aria-disabled", "false") - .prop("disabled", false); - } else { - $("#bulk_delete") - .text("Bulk Delete") - .attr("aria-disabled", "true") - .prop("disabled", true); - } + updateBulkDeleteStatus(); + updateSelectAllStatus(); }); $(".import-export-header").click(function () { $(this).children(".rotate").toggleClass("left"); }); + $("#select_all").click((event) => { + let checked = event.target.checked; + + $("input[name='stories[]']").each((_, checkbox) => { + checkbox.checked = checked; + }) + + updateBulkDeleteStatus(); + }) + $("#bulk_delete").click((event) => { let stories_ids = []; $("input[name='stories[]']:checked").each((_, checkbox) => { @@ -103,7 +97,7 @@ const filterStories = () => { document.querySelectorAll("#stories tr").forEach(function (element) { const cl = element.classList; const storyTitle = element - .querySelector("td:first-child") + .querySelector("td:nth-child(2)") .innerText.toLowerCase(); if (storyTitle.includes(searchTerm) || element.id.replace(/\D/g, '').includes(searchTerm)) { cl.remove("hidden"); @@ -118,3 +112,47 @@ function toggleCloneSubProjects(value) { .querySelectorAll("#sub-projects-to-clone input[type='checkbox']") .forEach((el) => (el.checked = value)); } + +function updateBulkDeleteStatus() { + const selected = $("input[name='stories[]']:checked"); + const is_unlocked = $("#stories").data("unlocked"); + if (!is_unlocked) { + return; + } + + if (selected.length > 0) { + const ending = selected.length == 1 ? "y" : "ies"; + $("#bulk_delete") + .text(`Bulk Delete (${selected.length} Stor${ending})`) + .attr("aria-disabled", "false") + .prop("disabled", false); + } else { + $("#bulk_delete") + .text("Bulk Delete") + .attr("aria-disabled", "true") + .prop("disabled", true); + } +} + +function updateSelectAllStatus() { + const selectAll = $("#select_all")[0]; + // Select All is only rendered for unlocked projects, so there is nothing to + // sync when it is absent. + if (!selectAll) { + return; + } + + const selected = $("input[name='stories[]']:checked"); + const checkboxes = $("input[name='stories[]']"); + + if (selected.length == 0) { + selectAll.checked = false; + selectAll.indeterminate = false; + } else if (selected.length == checkboxes.length) { + selectAll.checked = true; + selectAll.indeterminate = false; + } else { + selectAll.checked = false; + selectAll.indeterminate = true; + } +} diff --git a/app/assets/stylesheets/4-molecules/_tables.scss b/app/assets/stylesheets/4-molecules/_tables.scss index 6f0b852e..810bd64b 100644 --- a/app/assets/stylesheets/4-molecules/_tables.scss +++ b/app/assets/stylesheets/4-molecules/_tables.scss @@ -11,9 +11,15 @@ } .project-table__row { display: grid; - grid-template-columns: 1fr 100px 70px 70px 260px; + grid-template-columns: 120px 1fr 100px 70px 70px 260px; align-items: center; padding: 10px 0; + .select-all-cell { + text-align: center; + label { + font-weight: bold; + } + } &.project-table__row--reports { display: flex; flex-direction: row; diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb index da676373..703c9961 100644 --- a/app/views/projects/show.html.erb +++ b/app/views/projects/show.html.erb @@ -13,6 +13,12 @@
| + <% if is_unlocked?(@project) %> + + + <% end %> + | Story Title | Status | Best Estimate |
@@ -27,9 +33,11 @@
<% if @stories.present? %>
<% @stories.each do | story | %>
|---|---|---|---|
| + + | Copied to clipboard - <%= link_to "#{story.id} - #{story.title}", [story.project, story] %> | <%= status_label(story) %> | @@ -82,6 +90,7 @@|
| Total estimates | <%= @project.best_estimate_sum_per_user(current_user) %> | diff --git a/spec/features/stories_manage_spec.rb b/spec/features/stories_manage_spec.rb index cd4fb12f..316d7f41 100644 --- a/spec/features/stories_manage_spec.rb +++ b/spec/features/stories_manage_spec.rb @@ -108,6 +108,40 @@ assert_current_path project_path(id: project.id) end + it "allows me to select all stories" do + visit project_path(id: project.id) + check("Select All") + + expect(page).to have_checked_field(name: "stories[]") + end + + it "allows me to unselect all stories" do + visit project_path(id: project.id) + check("Select All") + uncheck("Select All") + + expect(page).to have_unchecked_field(name: "stories[]") + end + + it "does not show Select All on a locked project" do + locked_project = FactoryBot.create(:project, :locked) + FactoryBot.create(:story, project: locked_project) + + visit project_path(id: locked_project.id) + + expect(page).to have_no_field("Select All") + end + + it "keeps Bulk Delete disabled on a locked project even when a story is selected" do + locked_project = FactoryBot.create(:project, :locked) + FactoryBot.create(:story, project: locked_project) + + visit project_path(id: locked_project.id) + find("input[name='stories[]']", match: :first).check + + expect(page).to have_selector("#bulk_delete[disabled]") + end + it "allows me to delete a story" do visit project_path(id: project.id) @@ -396,14 +430,14 @@ fill_in "title_contains", with: "XYZ" within("#stories") do - expect(find("td:nth-child(1)")).to have_text story4.title + expect(find("td:nth-child(2)")).to have_text story4.title expect(all("#stories > tr").count).to eq(1) end fill_in "title_contains", with: story5.id within("#stories") do - expect(find("td:nth-child(1)")).to have_text story5.title + expect(find("td:nth-child(2)")).to have_text story5.title expect(all("#stories > tr").count).to eq(1) end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 0a289ff2..6f404eb7 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -63,8 +63,8 @@ def expect_closed_modal def expect_story_estimates(story, best, worst) within_story_row(story) do - expect(find("td:nth-child(3)")).to have_text best.to_s - expect(find("td:nth-child(4)")).to have_text worst.to_s + expect(find("td:nth-child(4)")).to have_text best.to_s + expect(find("td:nth-child(5)")).to have_text worst.to_s end end end