diff --git a/app/assets/stylesheets/3-atoms/_badges.scss b/app/assets/stylesheets/3-atoms/_badges.scss index 9f5957a8..4379a9bb 100644 --- a/app/assets/stylesheets/3-atoms/_badges.scss +++ b/app/assets/stylesheets/3-atoms/_badges.scss @@ -7,6 +7,7 @@ white-space: nowrap; vertical-align: baseline; border-radius: 20px; + font-size: 15px; &.archived { background-color: rgba(29,78,216,0.2); diff --git a/app/assets/stylesheets/3-atoms/_buttons.scss b/app/assets/stylesheets/3-atoms/_buttons.scss index bc8464e8..feb1b52b 100644 --- a/app/assets/stylesheets/3-atoms/_buttons.scss +++ b/app/assets/stylesheets/3-atoms/_buttons.scss @@ -34,6 +34,9 @@ background-color: lighten(red, 10%); } } + &.disabled { + opacity: 0.5; + } &#bulk_delete[aria-disabled=true] { opacity: 0.5; } diff --git a/app/assets/stylesheets/project.scss b/app/assets/stylesheets/project.scss index 86981249..c4d15e94 100644 --- a/app/assets/stylesheets/project.scss +++ b/app/assets/stylesheets/project.scss @@ -109,6 +109,10 @@ background: white; box-shadow: 1px 1px 4px 2px rgba(0, 0, 0, 0.4); + a span{ + padding-left: 5px; + } + a, button, input { diff --git a/app/controllers/action_plans_controller.rb b/app/controllers/action_plans_controller.rb index 9d742f94..fa30395f 100644 --- a/app/controllers/action_plans_controller.rb +++ b/app/controllers/action_plans_controller.rb @@ -3,6 +3,9 @@ class ActionPlansController < ApplicationController def show @project = Project.find(params[:project_id]) + + ensure_unarchived! + @project_stories = @project.stories.by_position @children = Project.sub_projects_with_ordered_stories(@project.id) end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 6164ee06..61d58555 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -10,10 +10,15 @@ def after_sign_in_path_for(resource) projects_path end - private - # we can enable this for everybody, it's hidden by default def enable_rack_mini_profiler Rack::MiniProfiler.authorize_request end + + def ensure_unarchived! + if @project.archived? || @project.parent&.archived? + flash[:error] = "You can't perform this action in an archived project." + redirect_to project_path(@project.id) + end + end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 77bd6c8b..a8b6d39e 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,6 +1,7 @@ class ProjectsController < ApplicationController before_action :authenticate_user! - before_action :find_project, only: [:show, :edit, :update, :sort, :sort_stories, :destroy] + before_action :find_project, except: [:index, :new, :create] + before_action :ensure_unarchived!, only: [:edit, :new_sub_project, :action_plan, :update] def index status = params[:archived] == "true" ? "archived" : nil @@ -29,7 +30,6 @@ def sort_stories end def toggle_archive - @project = Project.find(params[:id]) @project.toggle_archived! end @@ -91,20 +91,19 @@ def update end def new_sub_project - @project = Project.find(params[:project_id]) @sub = Project.new(parent_id: @project) end private - def find_project - @project = Project.find(params[:id]) - end - def projects_params params.require(:project).permit(:title, :status, :parent_id) end + def find_project + @project = Project.find(params[:id] || params[:project_id]) + end + def clone_params params.require(:project).permit(:title, :parent_id) end diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb index 8e3082a4..269960de 100644 --- a/app/controllers/stories_controller.rb +++ b/app/controllers/stories_controller.rb @@ -4,6 +4,8 @@ class StoriesController < ApplicationController before_action :find_project, except: [:bulk_destroy, :render_markdown, :edit, :update, :destroy, :show, :move] before_action :find_story, only: [:edit, :update, :destroy, :show, :move] before_action :validate_url_product_id, only: [:edit, :update, :destroy, :show, :move] + before_action :ensure_unarchived!, except: [:show, :bulk_destroy, :render_markdown, :move] + include ApplicationHelper CSV_HEADERS = %w[id title description position] diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index b7dac7bc..8f6be44f 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -3,4 +3,23 @@ def estimated(story) @estimate_id = story.estimate_for(current_user) @estimate_id.present? end + + def link_unless_archived(project, text, url, classes: nil, method: :get, remote: false, icon: nil, data_attr: nil) + link_text = text_content(icon, text) + if project.archived? || project.parent&.archived? + button_tag(link_text, disabled: true, class: "button #{classes} disabled", aria: {disabled: true}) + else + link_to(link_text, url, class: "button #{classes}", title: text, method: method, remote: remote, data: data_attr) + end + end + + private + + def text_content(icon, text) + if icon + icon_tag = content_tag(:i, "", class: "fa fa-#{icon}").concat(content_tag(:span, text)) + end + + icon_tag || text + end end diff --git a/app/views/layouts/_flash.html.erb b/app/views/layouts/_flash.html.erb index e63f3f8a..9f2be66e 100644 --- a/app/views/layouts/_flash.html.erb +++ b/app/views/layouts/_flash.html.erb @@ -1,6 +1,8 @@ <% bootstrap_alert_names = {"success" => "success", "error" => "danger"}%> -<% if flash[:success] || flash[:error] %> - <% flash.each do |key, value| %> -
<%= value %>
+
+ <% if flash[:success] || flash[:error] %> + <% flash.each do |key, value| %> +
<%= value %>
+ <% end %> <% end %> -<% end %> +
diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb index 2f404901..1b0c44f5 100644 --- a/app/views/projects/show.html.erb +++ b/app/views/projects/show.html.erb @@ -17,7 +17,7 @@ Best
Estimate Worst
Estimate - <%= link_to "Add a Story", new_project_story_path(@project.id), class: "button magenta" %> + <%= link_unless_archived(@project, "Add a Story", new_project_story_path(@project), classes: "magenta") %> @@ -35,23 +35,18 @@ <%= story.estimate_for(current_user)&.worst_case_points %> <% if estimated(story) %> - <%= link_to "Edit Estimate", edit_project_story_estimate_path(@project.id, story, @estimate_id), class: "button edit-estimate", remote: true %> + <%= link_unless_archived(@project, "Edit Estimate", edit_project_story_estimate_path(@project.id, story, @estimate_id), classes: "edit-estimate", remote: true) %> <% else %> - <%= link_to "Add Estimate", new_project_story_estimate_path(@project.id, story), class: "button add-estimate", remote: true %> - <% end %> + <%= link_unless_archived(@project, "Add Estimate", new_project_story_estimate_path(@project.id, story), classes: "add-estimate", remote: true) %> + <% end %> + @@ -96,25 +96,17 @@ <% end %> - - <%= render partial: "projects/import_export" %> -
- <%= button_to 'Delete Project', project_path(@project.id), method: "delete", data: { confirm: 'Are you sure?' }, class: "button magenta", id: "delete" %> - - <% unless @project.parent_id %> - <%= link_to 'Add Sub-Project', project_new_sub_project_path(@project.id), id:"subproject", class: "button magenta" %> - <% end %> +
+ <%= link_unless_archived(@project, "Edit or Delete Project", edit_project_path(@project), classes: "magenta") %> + <%= link_unless_archived(@project, "Add Sub-Project", project_new_sub_project_path(@project), classes: "magenta") unless @project.parent_id %> <%= link_to 'Clone Project', new_clone_project_path(@project.id), class: "button magenta" %> <% unless @project.parent_id %> <%= link_to "#{@project.archived? ? 'Unarchive' : 'Archive'} Project", toggle_archive_project_path(@project.id), method: "patch", id: "toggle_archive", class: "button magenta", remote: true %> <% end %> <%= link_to "Generate Action Plan", project_action_plan_path(@project.id), class: "button" %> - <% if current_user.admin? %> - <%= link_to "View Report", project_report_path(@project.id), class: "button" %> - <% end %> + <%= link_to "View Report", project_report_path(@project.id), class: "button" if current_user.admin? %> + <%= link_to 'Return to Projects Page', projects_path, id:"back", class: "button" %>
- - <%= link_to 'Return to Projects Page', projects_path, id:"back", class: "button" %>
diff --git a/app/views/projects/toggle_archive.js.erb b/app/views/projects/toggle_archive.js.erb index 6154b598..85531403 100644 --- a/app/views/projects/toggle_archive.js.erb +++ b/app/views/projects/toggle_archive.js.erb @@ -1,29 +1,14 @@ (function() { toggleButtonText(); - toggleProjectStatus(); + window.location.reload(); })() function toggleButtonText() { let button_text = document.querySelector('#toggle_archive'); if (button_text.textContent == "Archive Project") { - button_text.textContent = "Unarchive Project" + button_text.textContent = "Unarchive Project"; } else { - button_text.textContent = "Archive Project" - } -} - -function toggleProjectStatus() { - let statusBadge = document.querySelector(".status-badge"); - let projectStatus = "<%= @project.status %>"; - - if (projectStatus === ""){ - statusBadge.classList.add("hide") - statusBadge.textContent = "" - } - else{ - statusBadge.classList.remove("hide") - statusBadge.classList.add(projectStatus) - statusBadge.textContent = projectStatus + button_text.textContent = "Archive Project"; } } diff --git a/app/views/stories/show.html.erb b/app/views/stories/show.html.erb index a3f80328..416b566a 100644 --- a/app/views/stories/show.html.erb +++ b/app/views/stories/show.html.erb @@ -4,7 +4,7 @@ <%= render "shared/story", story: @story %>
- <%= link_to 'Edit', edit_project_story_path(@project), id:"back", class: "button" %> + <%= link_unless_archived(@project, "Edit", edit_project_story_path(@project), classes: "magenta") %> <%= link_to 'Back', project_path(@project), id:"back", class: "button" %> <%= link_to "Delete", project_story_path(@project.id, @story), method: :delete, data: { confirm: "Are you sure?", story_id: @story.id }, class: "button red", remote: true , title: "Delete" %>
diff --git a/config/initializers/rack_mini_profiler.rb b/config/initializers/rack_mini_profiler.rb index 726867ed..7513a43c 100644 --- a/config/initializers/rack_mini_profiler.rb +++ b/config/initializers/rack_mini_profiler.rb @@ -1,2 +1,2 @@ Rack::MiniProfiler.config.start_hidden = true -Rack::MiniProfiler.config.toggle_shortcut = "Alt+Ctrl+P" \ No newline at end of file +Rack::MiniProfiler.config.toggle_shortcut = "Alt+Ctrl+P" diff --git a/spec/controllers/stories_controller_spec.rb b/spec/controllers/stories_controller_spec.rb index b9b9a28c..5b5499c9 100644 --- a/spec/controllers/stories_controller_spec.rb +++ b/spec/controllers/stories_controller_spec.rb @@ -1,130 +1,219 @@ require "rails_helper" -RSpec.describe StoriesController, type: :controller do - render_views - - let!(:project) { FactoryBot.create(:project) } +RSpec.describe "managing stories", js: true do + let(:user) { FactoryBot.create(:user) } + let(:project) { FactoryBot.create(:project) } let!(:story) { FactoryBot.create(:story, project: project) } before do - @request.env["devise.mapping"] = Devise.mappings[:user] - user = FactoryBot.create(:user) - sign_in user + login_as(user, scope: :user) end - describe "#new" do - it "redirects to the new page" do - get :new, params: {id: story.id, project_id: project.id} - expect(response).to render_template :new - end + it "allows me to add a story" do + visit project_path(id: project.id) + click_link "Add a Story" + fill_in "story[title]", with: "As a user, I want to add stories" + fill_in "story[description]", with: "This story allows users to add stories." + click_button "Create" + expect(Story.count).to eq 2 end - describe "#edit" do - before do - get :edit, params: {id: story.id, project_id: project.id} + it "allows me to clone a story" do + visit project_path(id: project.id) + within_story_row(story) do + click_button "More actions" + click_link "Clone" end + expect(page.find("#story_title").value).to eq story.title + expect(page.find("#story_description").value).to eq story.description + click_button "Create" + expect(Story.count).to eq 2 + end - it "redirects to the edit page" do - expect(response).to render_template :edit - end + it "allows me to edit a story" do + visit project_path(id: project.id) + click_button "More actions" + click_link "Edit" + fill_in "story[title]", with: "As a user, I want to edit stories" + click_button "Save Changes" + expect(page).to have_content "Story updated!" + end + + it "allows me to delete a story" do + visit project_path(id: project.id) - it "shows the fields for the story" do - expect(assigns(:story)).to eq story + expect(page).to have_text story.title + + accept_confirm do + click_button "More actions" + click_link "Delete" end + + expect(page).not_to have_text story.title + expect(Story.count).to eq 0 end - describe "#create" do - context "with valid attributes" do - let(:valid_params) { FactoryBot.attributes_for(:story) } + it "does not allow me to bulk delete stories when there are none selected" do + visit project_path(id: project.id) + expect(page).to have_selector("#bulk_delete[aria-disabled='true']") + expect(page).to have_selector("#bulk_delete[disabled]") + end - it "creates a new story" do - expect { - post :create, params: {project_id: project.id, story: valid_params} - }.to change(Story, :count).by(1) - end + it "allows me to bulk delete stories when one or more stories are selected" do + visit project_path(id: project.id) - it "redirects to the project path" do - post :create, params: {project_id: project.id, story: valid_params} + within_story_row(story) { check(option: story.id.to_s) } + expect(page).to have_no_selector("#bulk_delete[aria-disabled='true']") + expect(page).to have_no_selector("#bulk_delete[disabled]") + expect(page).to have_button("Bulk Delete (1 Story)") + click_button("Bulk Delete (1 Story)") + expect(Story.count).to eq 0 + end - expect(response).to redirect_to project_path(project.id) - end + it "shows a preview of the description while typing" do + visit project_path(id: project.id) + click_link "Add a Story" + fill_in "story[title]", with: "As a user, I want to add stories" + + desc = <<~DESC + This story allows users to add stories. + + some + code + + DESC + + fill_in "story[description]", with: desc + + within(".story_preview .content") do + expect(page).to have_selector("p", text: "This story allows users to add stories.") + expect(page).to have_selector("pre", text: "some\ncode") end - context "with invalid attributes" do - let(:invalid_attributes) { {title: ""} } + click_button "Create" - before do - post :create, params: {project_id: project.id, story: invalid_attributes} - end + expect(page).to have_text(project.title) - it "stays on the new template page" do - expect(response).to render_template :new - end + story = Story.last + within_story_row(story) do + click_button "More actions" + click_link "Edit" end - end - describe "#destroy" do - it "deletes the story" do - expect { - delete :destroy, params: {id: story.id, project_id: project.id} - }.to change(Story, :count).by(-1) + expect(page).to have_text("Edit Story") + + within(".story_preview .content") do + expect(page).to have_selector("p", text: "This story allows users to add stories.") + expect(page).to have_selector("pre", text: "some\ncode") end end - describe "#show" do - before do - get :show, params: {id: story.id, project_id: project.id} - end + it "can move stories between siblings" do + project2 = FactoryBot.create(:project, parent: project) + project3 = FactoryBot.create(:project, parent: project) + story = FactoryBot.create(:story, project: project2) - it "redirects to the show page" do - expect(response).to render_template :show - end + visit project_path(id: project2.id) + + within_story_row(story) do + # move to options are hidden + expect(page).not_to have_text project3.title - it "shows the attributes for the right story" do - expect(assigns(:story)).to eq story + click_button "More actions" + click_button "Move to" + + # only one option is to move to since there's only one sibling + expect(page).to have_selector ".move-story .dropdown > form", count: 1 + + # confirm moving the story + accept_confirm do + click_button project3.title + end end + + expect(page).to have_text "Story moved" + expect(page).not_to have_text story.title + expect(story.reload.project).to eq project3 end - describe "#show failure" do - it "Throws 404 if the project_id doesn't match the story's project" do - expect { - get :show, params: {id: story.id, project_id: project.id + 1} - }.to raise_error(ActionController::RoutingError) + # see issue #9 on github + it "preserves order of stories when editing" do + empty_project = FactoryBot.create(:project) + visit project_path(id: empty_project.id) + click_link "Add a Story" + fill_in "Title", with: "Story 1" + fill_in "Description (Markdown)", with: "desc" + click_button "Create" + + # check that it adds a position for new stories + story1 = Story.last + expect(story1.position).to be 1 + + click_link "Add a Story" + fill_in "Title", with: "Story 2" + fill_in "Description (Markdown)", with: "desc" + click_button "Create" + + story2 = Story.last + expect(story2.position).to be 2 + + # check that the order is not broken after edit for stories with no position + story1.update_attribute(:position, nil) + story2.update_attribute(:position, nil) + + within("#story_#{story1.id}") do + click_button "More actions" + click_link "Edit" end - end - describe "#update" do - it "updates the story" do - put :update, params: {id: story.id, - project_id: project.id, - story: {title: "New Story"}} + expect(page).to have_text("Edit Story") + + fill_in "Description (Markdown)", with: "desc2" + + click_button "Save Changes" + + expect(page).to have_text("Story updated!") - expect(story.reload.title).to eq "New Story" + within("#stories") do + expect(find("tr:nth-child(1)")).to have_text story1.title + expect(find("tr:nth-child(2)")).to have_text story2.title end - end - describe "#bulk_destroy" do - it "deletes multiple stories" do - ids = [99, 100] - ids.each do |id| - FactoryBot.create(:story, id: id, project: project) - end + # check that stories with nil position are listed first + + click_link "Add a Story" + fill_in "Title", with: "Story 3" + fill_in "Description (Markdown)", with: "desc" + click_button "Create" - expect { - post :bulk_destroy, params: {ids: ids}, format: :json - }.to change(Story, :count).by(-2) + story3 = Story.last + expect(story3.position).to be 1 + + within("#stories") do + expect(find("tr:nth-child(1)")).to have_text story1.title + expect(find("tr:nth-child(2)")).to have_text story2.title + expect(find("tr:nth-child(3)")).to have_text story3.title end end - describe "#move" do - it "does not allow moving stories to non-sibling projects" do - project2 = FactoryBot.create(:project, parent: project) - project3 = FactoryBot.create(:project) - story = FactoryBot.create(:story, project: project2) + it "allows sorting stories", js: true do + FactoryBot.create(:story, project: project) + story3 = FactoryBot.create(:story, project: project) - put :move, params: {project_id: project2.id, story_id: story.id, to_project: project3.id} - expect(flash[:error]).to eq "Selected project does not exist or is not a sibling." - expect(response).to redirect_to project2 - end + visit project_path(project) + + first = find(".project-table__cell", text: story.title) + last = find(".project-table__cell", text: story3.title) + + # The use expect_any_instance_of is discouraged by the RSpec team, but the drag_to + # method is not always consistent on the distance it drags elements and the order + # is not always the expected. So I'm using expect_any_instance_of on purpose here. + expect_any_instance_of(ProjectsController).to receive(:sort_stories) + + last.drag_to(first, delay: 0, html5: false) + + sleep(1) + + expect(page).not_to have_selector(".project-table.sorting") end end diff --git a/spec/features/projects_manage_spec.rb b/spec/features/projects_manage_spec.rb index 7aa34e0d..2ac0b7e5 100644 --- a/spec/features/projects_manage_spec.rb +++ b/spec/features/projects_manage_spec.rb @@ -21,195 +21,185 @@ expect(Project.count).to eq 1 end - it "allows me to delete a project", js: false do - visit project_path(id: project.id) - click_button "Delete Project" - expect(Project.count).to eq 0 - end - - it "allows me to delete a project" do - visit project_path(id: project.id) - accept_confirm do - click_button "Delete Project" + context "when the project is archived" do + before do + project.toggle_archived! + visit project_path(id: project.id) end - expect(page).not_to have_content "Delete Project" - expect(Project.count).to eq 0 - end - - it "allows editing the project's title inline" do - visit project_path(id: project.id) - - within ".dashboard-title" do - click_button project.title - expect(page).to have_selector("form") - - click_button "Cancel" - expect(page).to have_selector("form", count: 0) - - click_button project.title - expect(page).to have_selector(" form") - - fill_in "project_title", with: "New Title" - click_button "Save" - expect(page).to have_selector("form", count: 0) + it "doesn't allow me to change the project" do + expect(page).to have_selector(:link_or_button, "Edit or Delete Project", disabled: true) + expect(page).to have_selector(:link_or_button, "Add Sub-Project", disabled: true) + expect(page).to have_selector(:link_or_button, "Generate Action Plan", disabled: true) + end - project.reload - expect(project.title).to eq "New Title" + it "allow me to clone the project" do + expect(page).to have_selector(:link_or_button, "Clone Project", disabled: false) end end - context "Sub-Projects" do - it "allows me to add sub projects" do + context "when the project is unarchived" do + before do visit project_path(id: project.id) - click_link "Add Sub-Project" - fill_in "project[title]", with: "Super Sub Project" - click_button "Create" - expect(page).to have_content "Project created!" - expect(current_path).to eq projects_path end - it "lists available sub projects with a link" do - sub_project_one = FactoryBot.create(:project, title: "Sub-project 1", parent_id: project.id) - sub_project_two = FactoryBot.create(:project, title: "Sub-project 2", parent_id: project.id) - project_two = FactoryBot.create(:project) - visit projects_path - - within(".project-card", text: project.title) do - expect(page).to have_link(project.title) - expect(page).to_not have_link(project_two.title) + it "allows me to edit a project" do + click_link "Edit or Delete Project" + fill_in "project[title]", with: "New Project" + click_button "Save Changes" + expect(page).to have_content "Project updated!" + end - expect(page).to have_link(sub_project_one.title) - expect(page).to have_link(sub_project_two.title) + it "allows me to delete a project" do + click_link "Edit or Delete Project" + accept_confirm do + click_link "Delete Project" end + expect(page).not_to have_content "Edit or Delete Project" + expect(Project.count).to eq 0 end - it "allow me to visit sub-projects with a link" do - sub_project_one = FactoryBot.create(:project, title: "Sub-project 1", parent_id: project.id) - visit projects_path + it "allows me to delete a project when javascript is not enabled", js: false do + click_link "Edit or Delete Project" + click_link "Delete Project" + expect(Project.count).to eq 0 + end + + context "when the project has subprojects" do + it "allows me to add sub projects" do + click_link "Add Sub-Project" + fill_in "project[title]", with: "Super Sub Project" + click_button "Create" + expect(page).to have_content "Project created!" + expect(current_path).to eq projects_path + end - within(".project-card", text: project.title) do - expect(page).to have_link(project.title) - expect(page).to have_link(sub_project_one.title) + it "lists available sub projects with a link" do + sub_project_one = FactoryBot.create(:project, title: "Sub-project 1", parent_id: project.id) + sub_project_two = FactoryBot.create(:project, title: "Sub-project 2", parent_id: project.id) + project_two = FactoryBot.create(:project) + visit projects_path - click_link sub_project_one.title + within(".project-card", text: project.title) do + expect(page).to have_link(project.title) + expect(page).to_not have_link(project_two.title) + + expect(page).to have_link(sub_project_one.title) + expect(page).to have_link(sub_project_two.title) + end end - expect(current_path).to eq project_path(sub_project_one) - end - end - context "import & Export" do - before do - project.stories.create(title: "php upgrade", description: "quick php upgrade") - end + it "allow me to visit sub-projects with a link" do + sub_project_one = FactoryBot.create(:project, title: "Sub-project 1", parent_id: project.id) + visit projects_path - it "allows me to export a CSV", js: false do - visit project_path(id: project.id) - find("#import-export").click + within(".project-card", text: project.title) do + expect(page).to have_link(project.title) + expect(page).to have_link(sub_project_one.title) - click_on "Export" - expect(page.response_headers["Content-Type"]).to eql "text/csv" - expect(page.source).to include("php upgrade") + click_link sub_project_one.title + end + expect(current_path).to eq project_path(sub_project_one) + end end - it "allows me to export a CSV" do - visit project_path(id: project.id) - find("#import-export").click + context "when importing and exporting" do + before do + project.stories.create(title: "php upgrade", description: "quick php upgrade") + end - click_on "Export" - expect(page.source).to include("php upgrade") - end + it "allows me to export a CSV", js: false do + find("#import-export").click - it "allows me to import a CSV" do - visit project_path(id: project.id) - find("#import-export").click - page.attach_file("file", (Rails.root + "spec/fixtures/test.csv").to_s) - click_on "Import" - expect(project.stories.count).to be > 1 - expect(project.stories.map(&:title).join).to include("php upgrade") - expect(page.text).to include("success") - expect(page.current_path).to eql project_path(project.id) - end + click_on "Export" + expect(page.response_headers["Content-Type"]).to eql "text/csv" + expect(page.source).to include("php upgrade") + end - it "allows me to update existing stories on import" do - csv_path = (Rails.root + "tmp/stories.csv").to_s - story = project.stories.first - csv_content = "id,title,description,position\n#{story.id},#{story.title},blank!,#{story.position}" - File.write(csv_path, csv_content) + it "allows me to export a CSV" do + visit project_path(id: project.id) + find("#import-export").click - story_count = project.stories.count - visit project_path(id: project.id) - find("#import-export").click - page.attach_file("file", csv_path) - click_on "Import" - expect(project.stories.count).to be story_count - expect(project.stories.map(&:description).join).to_not include("quick") - end - end + click_on "Export" + expect(page.source).to include("php upgrade") + end - context "when archiving", js: true do - it "allows me to archive a project" do - visit project_path(id: project.id) - click_link "Archive Project" - expect(page).to have_content "Unarchive Project" - expect(page).to have_content "archived" - expect(project.reload).to be_archived - end + it "allows me to import a CSV" do + find("#import-export").click + page.attach_file("file", (Rails.root + "spec/fixtures/test.csv").to_s) + click_on "Import" + expect(project.stories.count).to be > 1 + expect(project.stories.map(&:title).join).to include("php upgrade") + expect(page.text).to include("success") + expect(page.current_path).to eql project_path(project.id) + end - it "archives sub projects" do - sub_project = FactoryBot.create(:project, parent: project) - visit project_path(id: project.id) - click_link "Archive Project" - expect(sub_project.reload).to be_archived - end - end + it "allows me to update existing stories on import" do + csv_path = (Rails.root + "tmp/stories.csv").to_s + story = project.stories.first + csv_content = "id,title,description,position\n#{story.id},#{story.title},blank!,#{story.position}" + File.write(csv_path, csv_content) + + story_count = project.stories.count + visit project_path(id: project.id) + find("#import-export").click + page.attach_file("file", csv_path) + click_on "Import" + expect(project.stories.count).to be story_count + expect(project.stories.map(&:description).join).to_not include("quick") + end - context "when unarchiving", js: true do - it "allows me to unarchive a project" do - project.toggle_archived! - visit project_path(id: project.id) - click_link "Unarchive Project" - expect(page).to have_content "Archive Project" - expect(project.reload).not_to be_archived - end - end + context "when archiving", js: true do + it "allows me to archive a project" do + click_link "Archive Project" + expect(page).to have_content "Unarchive Project" + expect(project.reload).to be_archived + end - context "cloning", js: true do - it "allows cloning a project" do - visit project_path(id: project.id) + it "archives sub projects" do + sub_project = FactoryBot.create(:project, parent: project) + visit project_path(id: project.id) + click_link "Archive Project" + expect(sub_project.reload).to be_archived + end + end - click_link "Clone Project" + context "cloning", js: true do + it "allows cloning a project" do + click_link "Clone Project" - expect(page).to have_text("Clone project #{project.title}") + expect(page).to have_text("Clone project #{project.title}") - fill_in :project_title, with: "Cloned Project" + fill_in :project_title, with: "Cloned Project" - expect { - click_button "Clone" - }.to change(Project, :count).by(1) + expect { + click_button "Clone" + }.to change(Project, :count).by(1) - expect(page).to have_text("Project cloned") + expect(page).to have_text("Project cloned") - last_project = Project.last - expect(last_project.id).not_to eq(project.id) - expect(last_project.stories.count).to eq project.stories.count + last_project = Project.last + expect(last_project.id).not_to eq(project.id) + expect(last_project.stories.count).to eq project.stories.count - expect(page).to have_text(last_project.title) - end + expect(page).to have_text(last_project.title) + end - it "defaults to same parent" do - sub_project = FactoryBot.create(:project, parent: project) + it "defaults to same parent" do + sub_project = FactoryBot.create(:project, parent: project) - # None if the project is a parent - visit new_clone_project_path(project) - expect(page).to have_select(:project_parent_id, selected: "None") + # None if the project is a parent + visit new_clone_project_path(project) + expect(page).to have_select(:project_parent_id, selected: "None") - # The parent if the project is a sub project - visit new_clone_project_path(sub_project) - expect(page).to have_select(:project_parent_id, selected: project.title) + # The parent if the project is a sub project + visit new_clone_project_path(sub_project) + expect(page).to have_select(:project_parent_id, selected: project.title) + end + end end - context "picking sub-projects" do + context "when picking sub-projects" do let!(:sub_project1) { FactoryBot.create(:project, parent: project) } let!(:sub_project2) { FactoryBot.create(:project, parent: project) } let!(:sub_project3) { FactoryBot.create(:project, parent: project) } @@ -273,43 +263,57 @@ expect(page).to have_text "Can't clone sub-projects if it's not a parent project" end end - end - context "hierarchy sidebar" do - context "with sub projects" do - let!(:sub_project1) { FactoryBot.create(:project, parent: project) } - let!(:sub_project2) { FactoryBot.create(:project, parent: project) } - - it "renders a sidebar" do - visit project_path(project) - within "aside.hierarchy" do - expect(page).to have_selector("a", text: project.title) - expect(page).to have_selector("a", text: sub_project1.title) - expect(page).to have_selector("a", text: sub_project2.title) + context "hierarchy sidebar" do + context "with sub projects" do + let!(:sub_project1) { FactoryBot.create(:project, parent: project) } + let!(:sub_project2) { FactoryBot.create(:project, parent: project) } + + it "renders a sidebar" do + visit project_path(project) + within "aside.hierarchy" do + expect(page).to have_selector("a", text: project.title) + expect(page).to have_selector("a", text: sub_project1.title) + expect(page).to have_selector("a", text: sub_project2.title) + end + + visit project_path(sub_project1) + within "aside.hierarchy" do + expect(page).to have_selector("a", text: project.title) + expect(page).to have_selector("a", text: sub_project1.title) + expect(page).to have_selector("a", text: sub_project2.title) + end + + visit project_path(sub_project2) + within "aside.hierarchy" do + expect(page).to have_selector("a", text: project.title) + expect(page).to have_selector("a", text: sub_project1.title) + expect(page).to have_selector("a", text: sub_project2.title) + end end + end - visit project_path(sub_project1) - within "aside.hierarchy" do - expect(page).to have_selector("a", text: project.title) - expect(page).to have_selector("a", text: sub_project1.title) - expect(page).to have_selector("a", text: sub_project2.title) - end + context "with no sub projects" do + it "renders no sidebar" do + visit project_path(project) - visit project_path(sub_project2) - within "aside.hierarchy" do - expect(page).to have_selector("a", text: project.title) - expect(page).to have_selector("a", text: sub_project1.title) - expect(page).to have_selector("a", text: sub_project2.title) + expect(page).not_to have_selector("aside.hierarchy") end end end + end - context "with no sub projects" do - it "renders no sidebar" do - visit project_path(project) + def download_csv_file(path) + visit project_path(id: project.id) + find("#import-export").click + click_on "Export" + csv = adjust_csv_descriptions(page.source) + File.write(path, csv) + end - expect(page).not_to have_selector("aside.hierarchy") - end - end + def adjust_csv_descriptions(csv) + CSV.parse(csv, headers: true).each do |row| + row["description"] = "blank!" + end.to_csv end end