diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index 0fe3105abe..0e72c753dc 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -365,6 +365,7 @@ en: message_successful_bulk_delete: Successfully deleted work packages. create: header: 'New Work Package' + header_with_parent: 'New work package (Child of %{type} #%{id})' no_results: title: No work packages to display description_html: | diff --git a/frontend/app/components/work-packages/controllers/wp-new.controller.js b/frontend/app/components/work-packages/controllers/wp-new.controller.js index 6a4eab3021..56363dc487 100644 --- a/frontend/app/components/work-packages/controllers/wp-new.controller.js +++ b/frontend/app/components/work-packages/controllers/wp-new.controller.js @@ -63,6 +63,17 @@ function WorkPackageNewController($scope, vm.notifyCreation = function() { NotificationsService.addSuccess(I18n.t('js.notice_successful_create')); }; + vm.getHeading = function() { + if (vm.parentWorkPackage !== undefined) { + return I18n.t('js.work_packages.create.header_with_parent', + { type: vm.parentWorkPackage.embedded.type.props.name, + id: vm.parentWorkPackage.props.id }); + } + else { + return I18n.t('js.work_packages.create.header'); + } + }; + vm.goBack = function() { var args = ['^'], prevState = $rootScope.previousState; @@ -112,7 +123,14 @@ function WorkPackageNewController($scope, EditableFieldsState.forcedEditState = true; EditableFieldsState.editAll.state = true; - if ($stateParams.copiedFromWorkPackageId) { + if ($stateParams.parent_id) { + vm.loaderPromise = WorkPackageService.getWorkPackage($stateParams.parent_id) + .then(function(workPackage) { + vm.parentWorkPackage = workPackage; + return WorkPackageService.initializeWorkPackageWithParent(workPackage); + }); + } + else if ($stateParams.copiedFromWorkPackageId) { vm.loaderPromise = WorkPackageService.getWorkPackage($stateParams.copiedFromWorkPackageId) .then(function(workPackage) { return WorkPackageService.initializeWorkPackageFromCopy(workPackage); diff --git a/frontend/app/components/work-packages/directives/wp-create-form/wp-full-create-form.directive.html b/frontend/app/components/work-packages/directives/wp-create-form/wp-full-create-form.directive.html index 7702e5a4d7..4bbe51c312 100644 --- a/frontend/app/components/work-packages/directives/wp-create-form/wp-full-create-form.directive.html +++ b/frontend/app/components/work-packages/directives/wp-create-form/wp-full-create-form.directive.html @@ -30,7 +30,7 @@
-

{{ ::I18n.t('js.work_packages.create.header') }}

+

{{ vm.getHeading() }}

diff --git a/frontend/app/components/work-packages/services/work-package.service.js b/frontend/app/components/work-packages/services/work-package.service.js index 29715c6d82..925fcc2b1e 100644 --- a/frontend/app/components/work-packages/services/work-package.service.js +++ b/frontend/app/components/work-packages/services/work-package.service.js @@ -122,6 +122,17 @@ function WorkPackageService($http, PathHelper, WorkPackagesHelper, HALAPIResourc return WorkPackageService.initializeWorkPackage(projectIdentifier, initialData); }, + initializeWorkPackageWithParent: function(parentWorkPackage) { + var projectIdentifier = parentWorkPackage.embedded.project.props.identifier; + + var initialData = { + parentId: String(parentWorkPackage.props.id) + }; + + return WorkPackageService.initializeWorkPackage(projectIdentifier, initialData); + }, + + getWorkPackage: function(id) { var path = PathHelper.apiV3WorkPackagePath(id), resource = HALAPIResource.setup(path); diff --git a/frontend/app/routing.js b/frontend/app/routing.js index aa908112d5..6cfbc06951 100644 --- a/frontend/app/routing.js +++ b/frontend/app/routing.js @@ -83,7 +83,7 @@ angular.module('openproject') }) .state('work-packages.new', { - url: '/{projects}/{projectPath}/work_packages/new?type', + url: '/{projects}/{projectPath}/work_packages/new?type&parent_id', templateUrl: '/components/routes/partials/work-packages.new.html', controllerAs: 'vm', reloadOnSearch: false diff --git a/spec/factories/issue_priority_factory.rb b/spec/factories/issue_priority_factory.rb index 9165c676d7..dd0fe09daa 100644 --- a/spec/factories/issue_priority_factory.rb +++ b/spec/factories/issue_priority_factory.rb @@ -30,4 +30,8 @@ FactoryGirl.define do factory :issue_priority do sequence(:name) { |n| "IssuePriority #{n}" } end + + factory :default_priority, parent: :issue_priority do + is_default true + end end diff --git a/spec/features/work_packages/create_child_spec.rb b/spec/features/work_packages/create_child_spec.rb new file mode 100644 index 0000000000..45ff6de5f6 --- /dev/null +++ b/spec/features/work_packages/create_child_spec.rb @@ -0,0 +1,129 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2015 the OpenProject Foundation (OPF) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See doc/COPYRIGHT.rdoc for more details. +#++ + +require 'spec_helper' + +RSpec.feature 'Work package create children', js: true, selenium: true do + let(:user) do + FactoryGirl.create(:user, + member_in_project: project, + member_through_role: create_role) + end + let(:work_flow) do + FactoryGirl.create(:workflow, + role: create_role, + type_id: original_work_package.type_id, + old_status: original_work_package.status, + new_status: FactoryGirl.create(:status)) + end + + let(:create_role) do + FactoryGirl.create(:role, + permissions: [:view_work_packages, + :add_work_packages, + :edit_work_packages, + :manage_subtasks]) + end + let(:project) { FactoryGirl.create(:project) } + let(:original_work_package) do + FactoryGirl.build(:work_package, + project: project, + assigned_to: assignee, + responsible: responsible, + fixed_version: version, + priority: default_priority, + author: author, + status: default_status) + end + let(:default_priority) do + FactoryGirl.build(:default_priority) + end + let(:default_status) do + FactoryGirl.build(:default_status) + end + let(:role) { FactoryGirl.build(:role, permissions: [:view_work_packages]) } + let(:assignee) do + FactoryGirl.build(:user, + firstname: 'An', + lastname: 'assignee', + member_in_project: project, + member_through_role: role) + end + let(:responsible) do + FactoryGirl.build(:user, + firstname: 'The', + lastname: 'responsible', + member_in_project: project, + member_through_role: role) + end + let(:author) do + FactoryGirl.build(:user, + firstname: 'The', + lastname: 'author', + member_in_project: project, + member_through_role: role) + end + let(:version) do + FactoryGirl.build(:version, + project: project) + end + + before do + login_as(user) + allow(user.pref).to receive(:warn_on_leaving_unsaved?).and_return(false) + original_work_package.save! + work_flow.save! + end + + scenario 'on fullscreen page' do + original_work_package_page = Pages::FullWorkPackage.new(original_work_package) + + child_work_package_page = original_work_package_page.add_child + + child_work_package_page.expect_current_path + child_work_package_page.expect_heading + + child_work_package_page.update_attributes Subject: 'Child work package' + + child_work_package_page.save! + + expect(page).to have_selector('.notification-box--content', + text: I18n.t('js.notice_successful_create')) + + child_work_package = WorkPackage.order(created_at: 'desc').first + + expect(child_work_package).to_not eql original_work_package + + child_work_package_page = Pages::FullWorkPackage.new(child_work_package) + + child_work_package_page.expect_subject + child_work_package_page.expect_current_path + + child_work_package_page.expect_parent(original_work_package) + end +end diff --git a/spec/support/pages/full_work_package.rb b/spec/support/pages/full_work_package.rb index fe11dd195c..fc92eaeec9 100644 --- a/spec/support/pages/full_work_package.rb +++ b/spec/support/pages/full_work_package.rb @@ -72,8 +72,29 @@ module Pages expect(page).to have_selector(container + ' .user', text: user.name) end + def expect_parent(parent = nil) + parent ||= work_package.parent + + expect(parent).to_not be_nil + + visit_tab!('relations') + + expect(page).to have_selector(".relation[title=#{I18n.t('js.relation_labels.parent')}] a", + text: "##{parent.id} #{parent.subject}") + end + + def add_child + visit_tab!('relations') + + page.find('.relation a', text: I18n.t('js.relation_labels.children')).click + + click_button I18n.t('js.relation_buttons.add_child') + + Pages::FullWorkPackageCreate.new(parent_work_package: work_package) + end + def visit_copy! - page = FullWorkPackageCreate.new(work_package) + page = FullWorkPackageCreate.new(original_work_package: work_package) page.visit! page @@ -85,7 +106,7 @@ module Pages find('.work-packages--show-view') end - def path(tab='activity') + def path(tab = 'activity') work_package_path(work_package.id, tab) end end diff --git a/spec/support/pages/full_work_package_create.rb b/spec/support/pages/full_work_package_create.rb index f62e641acb..a468b14a77 100644 --- a/spec/support/pages/full_work_package_create.rb +++ b/spec/support/pages/full_work_package_create.rb @@ -30,17 +30,29 @@ require 'support/pages/page' module Pages class FullWorkPackageCreate < Page - attr_reader :work_package + attr_reader :original_work_package, + :parent_work_package - def initialize(work_package = nil) + def initialize(original_work_package: nil, parent_work_package: nil) # in case of copy, the original work package can be provided - @work_package = work_package + @original_work_package = original_work_package + @parent_work_package = parent_work_package end def expect_fully_loaded expect(page).to have_field(I18n.t('js.work_packages.properties.subject')) end + def expect_heading + if parent_work_package + expect(page).to have_selector('h2', text: I18n.t('js.work_packages.create.header_with_parent', + type: parent_work_package.type, + id: parent_work_package.id)) + else + expect(page).to have_selector('h2', text: I18n.t('js.work_packages.create.header')) + end + end + def update_attributes(attribute_map) # Only designed for text fields for now attribute_map.each do |label, value| @@ -59,7 +71,11 @@ module Pages end def path - work_package_path(work_package) + '/copy' if work_package + if original_work_package + work_package_path(work_package) + '/copy' + elsif parent_work_package + new_project_work_packages_path(parent_work_package.project.identifier) + end end end end