[#63593] Primerize Project create form

Updates the form visuals according to new wireframes: the Project create
form now only includes required (built-in and custom) fields, along with
the "Subproject of" field.

In detail:

* Introduces `Projects::NewComponent` and `TemplateSelectComponent`.
* Adds, extends necessary Primer forms with basic specs.
* Adds `ProjectsController#create` action, routes and permissions.
* Adds `highlight-when-value-selected` Stimulus controller.
* Updates project feature specs for new form, including rewriting some
  expectations that were reliant on `data-qa-name` attribute that Primer
  form components do not render by default.
* Removes now obsolete Project Status administation feature spec.
This commit is contained in:
Alexander Brandon Coles
2025-04-23 21:27:00 +01:00
parent 0d097223f5
commit e225422ccd
34 changed files with 1503 additions and 288 deletions
@@ -0,0 +1,69 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) the OpenProject GmbH
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 COPYRIGHT and LICENSE files for more details.
++#%>
<%=
component_wrapper(
tag: "turbo-frame",
refresh: :morph,
autoscroll: true,
data: {
turbo_action: :replace,
autoscroll_block: :start,
autoscroll_behavior: :smooth
}
) do
settings_primer_form_with(model: project) do |f|
flex_layout do |container|
container.with_row(mb: 3) do
render(
Primer::Forms::FormList.new(
Projects::Settings::NameForm.new(f),
Projects::Settings::RelationsForm.new(f),
Projects::Settings::CustomFieldsForm.new(f)
)
)
end
if template
container.with_row(mb: 3) do
render(Primer::Box.new(border: true, border_radius: 2, bg: :inset, p: 3)) do
render(
Projects::TemplateForm.new(f, template:, copy_options:)
)
end
end
end
container.with_row(mb: 3) do
render Projects::SubmitOrCancel.new(f)
end
end
end
end
%>
+39
View File
@@ -0,0 +1,39 @@
# frozen_string_literal: true
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2010-2024 the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
# ++
module Projects
class NewComponent < ApplicationComponent
include ApplicationHelper
include OpPrimer::ComponentHelpers
include OpTurbo::Streamable
options :project, :template, :copy_options
end
end
@@ -0,0 +1,59 @@
<%#-- copyright
OpenProject is an open source project management software.
Copyright (C) the OpenProject GmbH
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 COPYRIGHT and LICENSE files for more details.
++#%>
<%=
component_wrapper do
settings_primer_form_with(
url: new_project_path,
method: :get,
data: {
turbo_frame: "projects-new-component",
controller: "auto-submit",
auto_submit_delay_value: 0
}
) do |f|
render(
Primer::Box.new(
border: true,
border_radius: 2,
bg: :inset,
my: 3,
p: 3,
data: {
controller: "highlight-when-value-selected",
highlight_when_value_selected_highlight_class: resolve_css_classes(bg: :accent),
highlight_when_value_selected_target: "content"
}
)
) do
render Projects::TemplateAutocompleter.new(f, template_id:, parent_id:)
end
end
end
%>
@@ -0,0 +1,48 @@
# frozen_string_literal: true
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2010-2024 the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
# ++
module Projects
class TemplateSelectComponent < ApplicationComponent
include ApplicationHelper
include OpPrimer::ComponentHelpers
include OpTurbo::Streamable
options :template, :parent
def resolve_css_classes(**)
Primer::Classify.(**)[:class]
end
private
def template_id = template&.id
def parent_id = parent&.id
end
end
+83 -3
View File
@@ -32,11 +32,13 @@ class ProjectsController < ApplicationController
menu_item :overview
menu_item :roadmap, only: :roadmap
before_action :find_project, except: %i[index new export_list_modal]
before_action :find_project, except: %i[index new create export_list_modal]
before_action :load_query_or_deny_access, only: %i[index export_list_modal]
before_action :authorize, only: %i[copy deactivate_work_package_attachments]
before_action :authorize_global, only: %i[new]
before_action :authorize_global, only: %i[new create]
before_action :require_admin, only: %i[destroy destroy_info]
before_action :find_optional_template, only: %i[new create]
before_action :find_optional_parent, only: :new
no_authorization_required! :index, :export_list_modal
@@ -89,7 +91,19 @@ class ProjectsController < ApplicationController
end
def new
render layout: "no_menu"
if from_template?
new_from_template
else
new_blank
end
end
def create
if from_template?
create_from_template
else
create_blank
end
end
def copy
@@ -135,6 +149,68 @@ class ProjectsController < ApplicationController
private
def from_template? = @template.present?
def new_blank
@new_project = @parent&.children&.build || Project.new
render layout: "no_menu"
end
def new_from_template
@copy_options = Projects::CopyOptions.new
@new_project = Projects::CopyService
.new(user: current_user, source: @template, contract_options: { validate_model: false })
.call(target_project_params: params.permit(:parent_id).to_h, attributes_only: true)
.result
render layout: "no_menu"
end
def create_blank
service_call = Projects::CreateService
.new(user: current_user)
.call(permitted_params.project)
@new_project = service_call.result
if service_call.success?
redirect_to project_path(@new_project), notice: I18n.t(:notice_successful_create)
else
flash.now[:error] = I18n.t(:notice_unsuccessful_create_with_reason, reason: service_call.message)
render action: :new, status: :unprocessable_entity
end
end
def create_from_template # rubocop:disable Metrics/AbcSize
@copy_options = Projects::CopyOptions.new(copy_options_params)
service_call = Projects::EnqueueCopyService
.new(user: current_user, model: @template)
.call(
target_project_params: permitted_params.project.to_h,
only: @copy_options.dependencies,
send_notifications: @copy_options.send_notifications
)
if service_call.success?
job = service_call.result
redirect_to job_status_path(job.job_id)
else
@new_project = service_call.result
flash.now[:error] = I18n.t(:notice_unsuccessful_create_with_reason, reason: service_call.message)
render action: :new, status: :unprocessable_entity
end
end
def find_optional_template
@template = Project.templated.visible(current_user).find(params[:template_id]) if params[:template_id].present?
end
def find_optional_parent
@parent = Project.visible(current_user).find(params[:parent_id]) if params[:parent_id].present?
end
def has_managed_project_folders?(project)
project.project_storages.any?(&:project_folder_automatic?)
end
@@ -162,5 +238,9 @@ class ProjectsController < ApplicationController
::Exports::Register.list_formats(Project).map(&:to_s)
end
def copy_options_params
params.expect(copy_options: [[dependencies: []], :send_notifications])
end
helper_method :supported_export_formats
end
+45
View File
@@ -0,0 +1,45 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
module Projects
class CopyOptions
include ActiveModel::Model
include ActiveModel::Attributes
def self.all_dependencies
CopyService.copyable_dependencies.to_h { [it[:identifier], it[:name_source].call] }
end
attribute :dependencies, array: true, default: all_dependencies.keys
attribute :send_notifications, :boolean, default: false
validates :dependencies, inclusion: { in: all_dependencies.keys }
end
end
+45
View File
@@ -0,0 +1,45 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
module Projects
class CopyOptionsForm < ApplicationForm
form do |f|
f.check_box_group(name: :dependencies, label: I18n.t("js.project.copy.copy_options")) do |group|
CopyOptions.all_dependencies.each do |value, label|
group.check_box label:, value:
end
end
f.separator
f.check_box name: :send_notifications, label: I18n.t("label_project_copy_notifications")
end
end
end
@@ -0,0 +1,52 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
module Projects
module Settings
class CustomFieldsForm < ApplicationForm
include ::CustomFields::CustomFieldRendering
form do |f|
render_custom_fields(form: f)
end
def additional_custom_field_input_arguments
{ wrapper_id: nil }
end
private
def custom_fields
@custom_fields ||= model
.available_custom_fields
.required
end
end
end
end
@@ -40,7 +40,7 @@ module Projects
model: project_autocompleter_model,
focusDirectly: false,
dropdownPosition: "bottom",
url: ::API::V3::Utilities::PathHelper::ApiV3Path.projects_available_parents + "?of=#{model.id}",
url: project_autocompleter_url,
filters: [],
data: { qa_field_name: "parent" }
}
@@ -55,6 +55,12 @@ module Projects
{ id: parent.id, name: parent.name }
end
def project_autocompleter_url
url_str = ::API::V3::Utilities::PathHelper::ApiV3Path.projects_available_parents
url_str << "?of=#{model.id}" unless model.new_record?
url_str
end
end
end
end
+70
View File
@@ -0,0 +1,70 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
module Projects
class SubmitOrCancel < ApplicationForm
attr_reader :submit_options, :cancel_options
form do |buttons|
buttons.group(layout: :horizontal) do |button_group|
button_group.submit(**submit_options)
button_group.button(**cancel_options)
end
end
def initialize(submit_options: {}, cancel_options: {})
super()
@submit_options = submit_options.with_defaults(default_submit_options)
@cancel_options = cancel_options.with_defaults(default_cancel_options)
end
private
def default_submit_options
{
name: :submit,
scheme: :primary,
label: I18n.t("button_create"),
disabled: false
}
end
def default_cancel_options
{
name: :cancel,
scheme: :default,
tag: :a,
href: OpenProject::StaticRouting::StaticRouter.new.url_helpers.projects_path,
label: I18n.t("button_cancel")
}
end
end
end
@@ -0,0 +1,61 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
module Projects
class TemplateAutocompleter < ApplicationForm
extend Dry::Initializer
option :template_id, optional: true
option :parent_id, optional: true
form do |f|
f.project_autocompleter(
name: :template_id,
label: I18n.t("js.project.use_template"),
autocomplete_options: {
focusDirectly: false,
dropdownPosition: "bottom",
inputValue: template_id,
placeholder: I18n.t("js.project.no_template_selected"),
filters: [
{ name: "user_action", operator: "=", values: ["projects/copy"] },
{ name: "templated", operator: "=", values: ["t"] }
],
data: {
action: "change->highlight-when-value-selected#itemSelected change->auto-submit#submit",
"qa-field-name": "use_template"
}
}
)
f.hidden name: :parent_id, value: parent_id
end
end
end
+46
View File
@@ -0,0 +1,46 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
module Projects
class TemplateForm < ApplicationForm
extend Dry::Initializer
option :template
option :copy_options
form do |f|
f.hidden name: :template_id, value: template.id, scope_name_to_model: false
f.fields_for(:copy_options, copy_options, nested: false) do |builder|
CopyOptionsForm.new(builder)
end
end
end
end
+15 -1
View File
@@ -33,4 +33,18 @@ See COPYRIGHT and LICENSE files for more details.
header.with_breadcrumbs([{ href: home_path, text: organization_name }, t(:label_project_new)])
end
%>
<%= angular_component_tag "opce-new-project" %>
<%=
render Projects::TemplateSelectComponent.new(
template: @template,
parent: @parent
)
%>
<%=
render Projects::NewComponent.new(
project: @new_project,
template: @template,
copy_options: @copy_options
)
%>
+2 -2
View File
@@ -30,7 +30,7 @@ Rails.application.reloader.to_prepare do
OpenProject::AccessControl.map do |map|
map.project_module nil, order: 100 do
map.permission :add_project,
{ projects: %i[new] },
{ projects: %i[new create] },
permissible_on: :global,
require: :loggedin,
contract_actions: { projects: %i[create] }
@@ -202,7 +202,7 @@ Rails.application.reloader.to_prepare do
require: :member
map.permission :add_subprojects,
{ projects: %i[new] },
{ projects: %i[new create] },
permissible_on: :project,
require: :member
+7
View File
@@ -936,6 +936,11 @@ en:
actionview_instancetag_blank_option: "Please select"
activemodel:
attributes:
projects/copy_options:
dependencies: "Dependencies"
activerecord:
attributes:
announcements:
@@ -3464,6 +3469,8 @@ en:
notice_successful_delete: "Successful deletion."
notice_successful_cancel: "Successful cancellation."
notice_successful_update: "Successful update."
notice_unsuccessful_create: "Creation failed."
notice_unsuccessful_create_with_reason: "Creation failed: %{reason}"
notice_unsuccessful_update: "Update failed."
notice_unsuccessful_update_with_reason: "Update failed: %{reason}"
notice_successful_update_custom_fields_added_to_project: |
+1 -1
View File
@@ -255,7 +255,7 @@ Rails.application.routes.draw do
resource :menu, only: %i[show]
end
resources :projects, except: %i[show edit create update] do
resources :projects, except: %i[show edit update] do
scope module: "projects" do
namespace "settings" do
resource :general, only: %i[show update], controller: "general" do
@@ -0,0 +1,44 @@
/*
* -- copyright
* OpenProject is an open source project management software.
* Copyright (C) the OpenProject GmbH
*
* 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 COPYRIGHT and LICENSE files for more details.
* ++
*/
import { Controller } from '@hotwired/stimulus';
import { IAutocompleteItem } from 'core-app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component';
export default class HighlightWhenValueSelectedController extends Controller {
static targets = ['content'];
static classes = ['highlight'];
declare readonly contentTarget:HTMLElement;
declare readonly highlightClass:string;
itemSelected({ detail: item }:CustomEvent<IAutocompleteItem | null>):void {
this.contentTarget.classList.toggle(this.highlightClass, item != null);
}
}
+2
View File
@@ -21,6 +21,7 @@ import HoverCardTriggerController from './controllers/hover-card-trigger.control
import ScrollIntoViewController from './controllers/scroll-into-view.controller';
import CkeditorFocusController from './controllers/ckeditor-focus.controller';
import AddMeetingParamsController from './controllers/add-meeting-params.controller';
import HighlightWhenValueSelectedController from './controllers/highlight-when-value-selected.controller';
import AutoSubmit from '@stimulus-components/auto-submit';
@@ -59,5 +60,6 @@ instance.register('pattern-input', PatternInputController);
instance.register('scroll-into-view', ScrollIntoViewController);
instance.register('ckeditor-focus', CkeditorFocusController);
instance.register('add-meeting-params', AddMeetingParamsController);
instance.register('highlight-when-value-selected', HighlightWhenValueSelectedController);
instance.register('auto-submit', AutoSubmit);
@@ -0,0 +1,44 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
require "rails_helper"
RSpec.describe Projects::NewComponent, type: :component do
let(:project) { build_stubbed(:project) }
def render_component(**params)
render_inline(described_class.new(project:, **params))
page
end
it "renders a form" do
expect(render_component).to have_css "form"
end
end
@@ -0,0 +1,44 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
require "rails_helper"
RSpec.describe Projects::TemplateSelectComponent, type: :component do
let(:template) { build_stubbed(:template_project) }
def render_component(**params)
render_inline(described_class.new(template:, **params))
page
end
it "renders a form" do
expect(render_component).to have_css "form"
end
end
+179 -14
View File
@@ -32,35 +32,200 @@ require "spec_helper"
RSpec.describe ProjectsController do
shared_let(:admin) { create(:admin) }
let(:non_member) { create(:non_member) }
let(:user) { admin }
before do
allow(controller).to receive(:set_localization)
login_as admin
login_as user
end
describe "#new" do
it "renders 'new'" do
get "new"
expect(response).to be_successful
expect(response).to render_template "new"
end
shared_examples_for "successful requests" do
context "without a parent" do
let(:parent) { nil }
context "by non-admin user with add_project permission" do
let(:non_member_user) { create(:user) }
context "without a template" do
let(:template) { nil }
before do
non_member.add_permission! :add_project
login_as non_member_user
it_behaves_like "successful request"
end
context "with a template" do
let(:template) { create(:template_project) }
it_behaves_like "successful request"
end
end
it "accepts get" do
get :new
context "with a parent" do
let(:parent) { create(:project) }
context "without a template" do
let(:template) { nil }
it_behaves_like "successful request"
end
context "with a template" do
let(:template) { create(:template_project) }
it_behaves_like "successful request"
end
end
end
shared_examples_for "successful request" do
it "renders 'new'", :aggregate_faulures do
expect(response).to be_successful
expect(assigns(:new_project)).to be_a_new(Project)
expect(assigns(:parent)).to eq parent
expect(assigns(:template)).to eq template
expect(response).to render_template "new"
end
end
before do
get :new, params: { parent_id: parent&.id, template_id: template&.id }
end
context "as an admin" do
it_behaves_like "successful requests"
end
context "as a non-admin with global add_project permission" do
let(:user) { create(:user, global_permissions: [:add_project]) }
let(:template) { nil }
context "without a parent" do
let(:parent) { nil }
it_behaves_like "successful request"
end
context "with a parent with public permissions" do
let(:user) { create(:user, global_permissions: [:add_project], member_with_roles: { parent => parent_role }) }
let(:parent_role) { create(:project_role) }
let(:parent) { create(:project) }
it_behaves_like "successful request"
end
end
context "as a non-admin without global add_project permission" do
let(:user) { create(:user, global_permissions: []) }
let(:template) { nil }
context "without a parent" do
let(:parent) { nil }
it "returns 403 Not Authorized" do
expect(response).not_to be_successful
expect(response).to have_http_status :forbidden
end
end
context "with a parent with add_subprojects permissions" do
let(:user) { create(:user, global_permissions: [], member_with_roles: { parent => parent_role }) }
let(:parent_role) { create(:project_role, permissions: [:add_subprojects]) }
let(:parent) { create(:project) }
let(:template) { nil }
it_behaves_like "successful request"
end
end
end
describe "#create" do
context "without a template" do
before do
creation_service = instance_double(Projects::CreateService, call: service_result)
allow(Projects::CreateService)
.to receive(:new)
.with(user: admin)
.and_return(creation_service)
end
context "when service call succeeds" do
let(:project) { build_stubbed(:project) }
let(:service_result) { ServiceResult.success(result: project) }
it "redirects to project show", :aggregate_failures do
post :create, params: { project: { name: "New Project" } }
expect(response).to redirect_to project_path(project)
expect(flash[:notice]).to eq I18n.t(:notice_successful_create)
end
end
context "when service call fails" do
let(:project) { Project.new }
let(:service_result) { ServiceResult.failure(result: project, message: "") }
it "renders new template with errors", :aggregate_failures do
post :create, params: { project: { name: "" } }
expect(response).not_to be_successful
expect(response).to have_http_status :unprocessable_entity
expect(assigns(:new_project)).to be_a_new(Project)
expect(assigns(:new_project)).not_to be_valid
expect(flash[:error]).to start_with I18n.t(:notice_unsuccessful_create_with_reason, reason: "")
expect(response).to render_template "new"
end
end
end
context "with a template" do
let(:template) { create(:template_project) }
before do
copy_service = instance_double(Projects::EnqueueCopyService, call: service_result)
allow(Projects::EnqueueCopyService)
.to receive(:new)
.with(user: admin, model: template)
.and_return(copy_service)
end
context "when service call succeeds" do
let(:job) { CopyProjectJob.new }
let(:service_result) { ServiceResult.success(result: job) }
it "redirects to job status", :aggregate_failures do
post :create, params: {
template_id: template.id,
project: { name: "New Project" },
copy_options: { dependencies: [], send_notifications: false }
}
expect(response).to redirect_to job_status_path(job.job_id)
end
end
context "when service call fails" do
let(:project) { Project.new }
let(:service_result) { ServiceResult.failure(result: project, message: "") }
it "renders new template with errors", :aggregate_failures do
post :create, params: {
template_id: template.id,
project: { name: "" },
copy_options: { dependencies: [], send_notifications: false }
}
expect(response).not_to be_successful
expect(response).to have_http_status :unprocessable_entity
expect(assigns(:new_project)).to be_a_new(Project)
expect(assigns(:new_project)).not_to be_valid
expect(assigns(:template)).not_to be_nil
expect(assigns(:copy_options)).not_to be_nil
expect(flash[:error]).to start_with I18n.t(:notice_unsuccessful_create_with_reason, reason: "")
expect(response).to render_template "new"
end
end
end
end
describe "index.html" do
@@ -81,17 +81,15 @@ RSpec.describe "Global role: Global Create project",
roles: [global_role])
end
let(:name_field) { FormFields::InputFormField.new :name }
current_user { user }
it 'allows creating projects via the "+ Project" button' do
projects_page.visit!
projects_page.navigate_to_new_project_page_from_toolbar_items
name_field.set_value "New project name"
fill_in "Name", with: "New project name"
find("button:not([disabled])", text: "Save").click
click_on "Create"
expect(page).to have_current_path "/projects/new-project-name/"
end
@@ -35,11 +35,14 @@ RSpec.describe "Project attribute help texts", :js do
let(:instance) do
create(:project_help_text,
attribute_name: :status,
help_text: "Some **help text** for status.")
attribute_name: :name,
help_text: "Some **help text** for name.")
create(:project_help_text,
attribute_name: :description,
help_text: "Some **help text** for description.")
create(:project_help_text,
attribute_name: :status,
help_text: "Some **help text** for status.")
end
let(:grid) do
@@ -87,19 +90,27 @@ RSpec.describe "Project attribute help texts", :js do
it_behaves_like "allows to view help texts"
it "shows the help text on the project create form" do
it "shows the help text on the project create form", :selenium do
visit new_project_path
page.find(".op-fieldset--legend", text: "ADVANCED SETTINGS").click
expect(page).to have_field "Name"
expect(page).to have_css(".spot-form-field--label attribute-help-text", wait: 10)
within(:element, "label", text: "Name") do
click_on accessible_name: "Show help text"
end
# Open help text modal
modal.open!
expect(modal.modal_container).to have_css("strong", text: "help text")
modal.expect_edit(editable: user.allowed_globally?(:edit_attribute_help_texts))
expect(page).to have_modal "Name"
modal.close!
within_modal "Name" do
expect(page).to have_css "strong", text: "help text"
expect(page).to have_button "Close"
expect(page).to have_link "Edit"
click_on "Close"
end
expect(page).not_to have_modal "Name"
end
end
+60 -133
View File
@@ -32,7 +32,6 @@ require "spec_helper"
RSpec.describe "Projects", "creation",
:js do
shared_let(:name_field) { FormFields::InputFormField.new :name }
shared_let(:project_custom_field_section) { create(:project_custom_field_section, name: "Section A") }
current_user { create(:admin) }
@@ -49,6 +48,8 @@ RSpec.describe "Projects", "creation",
it "can navigate to the create project page" do
projects_page.navigate_to_new_project_page_from_toolbar_items
expect(page).to have_heading "New project"
expect(page).to have_current_path(new_project_path)
end
end
@@ -56,8 +57,10 @@ RSpec.describe "Projects", "creation",
it "can create a project" do
projects_page.navigate_to_new_project_page_from_toolbar_items
name_field.set_value "Foo bar"
click_button "Save"
expect(page).to have_heading "New project"
fill_in "Name", with: "Foo bar"
click_on "Create"
expect(page).to have_current_path /\/projects\/foo-bar\/?/
expect(page).to have_content "Foo bar"
@@ -66,8 +69,10 @@ RSpec.describe "Projects", "creation",
it "does not create a project with an already existing identifier" do
projects_page.navigate_to_new_project_page_from_toolbar_items
name_field.set_value "Foo project"
click_on "Save"
expect(page).to have_heading "New project"
fill_in "Name", with: "Foo project"
click_on "Create"
expect(page).to have_current_path /\/projects\/foo-project-1\/?/
@@ -77,20 +82,30 @@ RSpec.describe "Projects", "creation",
context "with a multi-select list custom field" do
shared_let(:list_custom_field) do
create(:list_project_custom_field, name: "List CF", multi_value: true, project_custom_field_section:)
create(:list_project_custom_field,
name: "List CF",
is_required: true,
multi_value: true,
project_custom_field_section:)
end
let(:list_field) do
FormFields::SelectFormField.new(
list_custom_field,
selector: "[data-qa-field-name='#{list_custom_field.attribute_name(:kebab_case)}'"
)
end
let(:list_field) { FormFields::SelectFormField.new list_custom_field }
it "can create a project" do
projects_page.navigate_to_new_project_page_from_toolbar_items
name_field.set_value "Foo bar"
expect(page).to have_heading "New project"
find(".op-fieldset--toggle", text: "ADVANCED SETTINGS").click
fill_in "Name", with: "Foo bar"
expect(page).to have_combo_box "List CF *"
list_field.select_option "A", "B"
click_button "Save"
click_on "Create"
expect(page).to have_current_path /\/projects\/foo-bar\/?/
expect(page).to have_content "Foo bar"
@@ -120,18 +135,26 @@ RSpec.describe "Projects", "creation",
shared_let(:version_custom_field) do
create(:version_project_custom_field,
name: "Version CF",
is_required: true,
multi_value: true,
project_custom_field_section:)
end
let(:version_field) { FormFields::SelectFormField.new version_custom_field }
let(:version_field) do
FormFields::SelectFormField.new(
version_custom_field,
selector: "[data-qa-field-name='#{version_custom_field.attribute_name(:kebab_case)}'"
)
end
it "can create a project" do
projects_page.navigate_to_new_project_page_from_toolbar_items
name_field.set_value "Foo bar"
expect(page).to have_heading "New project"
find(".op-fieldset--toggle", text: "ADVANCED SETTINGS").click
fill_in "Name", with: "Foo bar"
expect(page).to have_combo_box "Version CF *"
# expect the versions are grouped by the project name
version_field.expect_option(versions.first.name, grouping: project.name)
@@ -139,7 +162,7 @@ RSpec.describe "Projects", "creation",
version_field.select_option(versions.first.name, versions.last.name)
click_button "Save"
click_on "Create"
expect(page).to have_current_path /\/projects\/foo-bar\/?/
expect(page).to have_content "Foo bar"
@@ -155,7 +178,7 @@ RSpec.describe "Projects", "creation",
it "hides the active field and the identifier" do
visit new_project_path
find(".op-fieldset--toggle", text: "ADVANCED SETTINGS").click
expect(page).to have_heading "New project"
expect(page).to have_no_content "Active"
expect(page).to have_no_content "Identifier"
@@ -184,19 +207,14 @@ RSpec.describe "Projects", "creation",
project_custom_field_section:)
end
it "separates optional and required custom fields for new" do
it "renders required custom fields for new" do
visit new_project_path
expect(page).to have_content "Required Foo"
expect(page).to have_content "Required User"
expect(page).to have_heading "New project"
click_on "Advanced settings"
within(".op-fieldset") do
expect(page).to have_text "Optional Foo"
expect(page).to have_no_text "Required Foo"
expect(page).to have_no_text "Required User"
end
expect(page).to have_field "Required Foo", required: true
expect(page).to have_field "Required User *" # FIXME required: true
expect(page).to have_no_field "Optional Foo"
end
end
@@ -206,10 +224,11 @@ RSpec.describe "Projects", "creation",
end
it "requires the required custom field" do
click_on "Save"
expect(page).to have_heading "New project"
expect(page).to have_content "Required Foo can't be blank"
expect(page).to have_no_content "Optional Foo can't be blank"
click_on "Create"
expect(page).to have_field "Required Foo", validation_error: "can't be blank."
end
end
@@ -222,17 +241,15 @@ RSpec.describe "Projects", "creation",
before do
visit new_project_path
expect(page).to have_heading "New project" # rubocop:disable RSpec/ExpectInHook
fill_in "Name", with: "Foo bar"
fill_in "Required Foo", with: "Required value"
click_on "Advanced settings"
end
it "enables custom fields with provided values for this project" do
fill_in "Optional Foo", with: "Optional value"
fill_in "Unused Foo", with: ""
click_on "Save"
click_on "Create"
expect(page).to have_current_path /\/projects\/foo-bar\/?/
@@ -240,7 +257,7 @@ RSpec.describe "Projects", "creation",
# unused custom field should not be activated
expect(project.project_custom_field_ids).to contain_exactly(
optional_custom_field.id, required_custom_field.id
required_custom_field.id
)
end
@@ -248,13 +265,14 @@ RSpec.describe "Projects", "creation",
shared_let(:custom_field_with_default_value) do
create(:project_custom_field, name: "Foo with default value",
field_format: "string",
is_required: true,
default_value: "Default value",
project_custom_field_section:)
end
it "enables custom fields with default values if not set to blank explicitly" do
# don't touch the default value
click_on "Save"
click_on "Create"
expect(page).to have_current_path /\/projects\/foo-bar\/?/
@@ -268,27 +286,10 @@ RSpec.describe "Projects", "creation",
expect(project.custom_value_for(custom_field_with_default_value).value).to eq("Default value")
end
it "does not enable custom fields with default values if set to blank explicitly" do
# native blank input does not work with this input, using support class here
field = FormFields::InputFormField.new(custom_field_with_default_value)
field.set_value("")
click_on "Save"
expect(page).to have_current_path /\/projects\/foo-bar\/?/
project = Project.last
# custom_field_with_default_value should not be activated
expect(project.project_custom_field_ids).to contain_exactly(
required_custom_field.id
)
end
it "does enable custom fields with default values if overwritten with a new value" do
fill_in "Foo with default value", with: "foo"
click_on "Save"
click_on "Create"
expect(page).to have_current_path /\/projects\/foo-bar\/?/
@@ -303,86 +304,10 @@ RSpec.describe "Projects", "creation",
end
end
context "with correct handling of optional boolean values" do
shared_let(:custom_boolean_field_default_true) do
create(:project_custom_field, name: "Boolean with default true",
field_format: "bool",
default_value: true,
project_custom_field_section:)
end
shared_let(:custom_boolean_field_default_false) do
create(:project_custom_field, name: "Boolean with default false",
field_format: "bool",
default_value: false,
project_custom_field_section:)
end
shared_let(:custom_boolean_field_with_no_default) do
create(:project_custom_field, name: "Boolean with no default",
field_format: "bool",
project_custom_field_section:)
end
it "only enables boolean custom fields with default values if untouched" do
# do not touch any of the boolean fields
click_on "Save"
expect(page).to have_current_path /\/projects\/foo-bar\/?/
project = Project.last
expect(project.project_custom_field_ids).to contain_exactly(
required_custom_field.id,
custom_boolean_field_default_true.id,
custom_boolean_field_default_false.id
)
expect(project.custom_value_for(custom_boolean_field_default_true).typed_value).to be_truthy
expect(project.custom_value_for(custom_boolean_field_default_false).typed_value).to be_falsy
end
it "enables boolean custom fields without default values if set to true explicitly" do
check "Boolean with no default"
click_on "Save"
expect(page).to have_current_path /\/projects\/foo-bar\/?/
project = Project.last
expect(project.project_custom_field_ids).to contain_exactly(
required_custom_field.id,
custom_boolean_field_default_true.id,
custom_boolean_field_default_false.id,
custom_boolean_field_with_no_default.id
)
expect(project.custom_value_for(custom_boolean_field_with_no_default).typed_value).to be_truthy
end
it "enables boolean custom fields with default values if set to false explicitly" do
uncheck "Boolean with default true"
click_on "Save"
expect(page).to have_current_path /\/projects\/foo-bar\/?/
project = Project.last
expect(project.project_custom_field_ids).to contain_exactly(
required_custom_field.id,
custom_boolean_field_default_true.id,
custom_boolean_field_default_false.id
)
expect(project.custom_value_for(custom_boolean_field_default_true).typed_value).to be_falsy
end
end
context "with correct handling of invisible values" do
shared_let(:invisible_field) do
create(:string_project_custom_field, name: "Text for Admins only",
is_required: true,
admin_only: true,
project_custom_field_section:)
end
@@ -393,7 +318,7 @@ RSpec.describe "Projects", "creation",
fill_in "Text for Admins only", with: "foo"
click_on "Save"
click_on "Create"
expect(page).to have_current_path /\/projects\/foo-bar\/?/
@@ -411,9 +336,11 @@ RSpec.describe "Projects", "creation",
current_user { create(:user, global_permissions: %i[add_project]) }
it "does not show invisible fields in the form and thus not activates the invisible field" do
pending "Admin-only project attributes currently prevent users from creating projects (OP#64479)"
expect(page).to have_no_content "Text for Admins only"
click_on "Save"
click_on "Create"
expect(page).to have_current_path /\/projects\/foo-bar\/?/
@@ -1,88 +0,0 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
require "spec_helper"
RSpec.describe "Projects status administration", :js do
include_context "ng-select-autocomplete helpers"
let(:current_user) do
create(:user) do |u|
create(:global_member,
principal: u,
roles: [create(:global_role, permissions: global_permissions)])
end
end
let(:global_permissions) { [:add_project] }
let(:project_permissions) { [:edit_project] }
let!(:project_role) do
create(:project_role, permissions: project_permissions) do |r|
allow(Setting)
.to receive(:new_project_user_role_id)
.and_return(r.id.to_s)
end
end
let(:status_description) { Components::WysiwygEditor.new('[data-qa-field-name="statusExplanation"]') }
let(:name_field) { FormFields::InputFormField.new :name }
let(:status_field) { FormFields::SelectFormField.new :status }
before do
login_as current_user
end
it "allows setting the status on project creation" do
visit new_project_path
# Create the project with status
click_button "Advanced settings"
name_field.set_value "New project"
status_field.select_option "On track"
status_description.set_markdown "Everything is fine at the start"
status_description.expect_supports_macros
click_button "Save"
expect(page).to have_current_path /projects\/new-project\/?/
# Check that the status has been set correctly
visit project_settings_general_path(project_id: "new-project")
status_description.expect_value "Everything is fine at the start"
status_description.set_markdown "Oh no"
click_button "Update status description"
status_description.expect_value "Oh no"
end
end
@@ -31,7 +31,6 @@
require "spec_helper"
RSpec.describe "Subproject creation", :js do
let(:name_field) { FormFields::InputFormField.new :name }
let(:parent_field) { FormFields::SelectFormField.new :parent }
let(:add_subproject_role) { create(:project_role, permissions: %i[edit_project add_subprojects]) }
let(:view_project_role) { create(:project_role, permissions: %i[edit_project]) }
@@ -55,16 +54,20 @@ RSpec.describe "Subproject creation", :js do
end
it "can create a subproject" do
click_link "Subproject"
click_on "New subproject"
expect(page).to have_heading "New project"
fill_in "Name", with: "Foo child"
expect(page).to have_combo_box "Subproject of"
name_field.set_value "Foo child"
parent_field.expect_required
# The other project is not a valid parent since the user is lacking
# the add_subproject permission therein.
parent_field.expect_no_option(other_project.name)
parent_field.expect_selected parent_project.name
click_button "Save"
click_on "Create"
expect(page).to have_current_path /\/projects\/foo-child\/?/
+18 -26
View File
@@ -85,10 +85,7 @@ RSpec.describe "Project templates", :js, with_good_job_batches: [CopyProjectJob,
create(:user, member_with_roles: { template => role })
end
let(:name_field) { FormFields::InputFormField.new :name }
let(:template_field) { FormFields::SelectFormField.new :use_template }
let(:status_field) { FormFields::SelectFormField.new :status }
let(:parent_field) { FormFields::SelectFormField.new :parent }
current_user do
create(:user,
@@ -99,42 +96,37 @@ RSpec.describe "Project templates", :js, with_good_job_batches: [CopyProjectJob,
it "can instantiate the project with the copy permission" do
visit new_project_path
name_field.set_value "Foo bar"
fill_in "Name", with: "Foo bar"
expect(page)
.to have_no_content("COPY OPTIONS")
expect(page).to have_no_selector :fieldset, "Copy options"
template_field.select_option "My template"
# Only when a template is selected, the options are displayed.
# Using this to know when the copy form has been fetched from the backend.
expect(page)
.to have_content("COPY OPTIONS")
expect(page).to have_selector :fieldset, "Copy options"
# FIXME: It should keep the name. See BUG OP#64594 https://community.openproject.org/wp/64594
# expect(page).to have_field "Name", with: "Foo bar"
fill_in "Name", with: "Foo bar"
# It keeps the name
name_field.expect_value "Foo bar"
template_field.expect_selected "My template"
# Updates the identifier in advanced settings
page.find(".op-fieldset--toggle", text: "ADVANCED SETTINGS").click
status_field.expect_selected "ON TRACK"
expect(page).to have_unchecked_field "Send email notifications during the project copy"
# Update status to off track
status_field.select_option "Off track"
parent_field.select_option other_project.name
within_fieldset "Copy options" do
# And allows to deselect copying the members.
uncheck "Project members"
end
page.find(".op-fieldset--toggle", text: "COPY OPTIONS").click
click_on "Create"
# Now shows the send notifications field.
expect(page).to have_css('[data-qa-field-name="sendNotifications"]')
expect(page).to have_dialog "Background job status"
# And allows to deselect copying the members.
uncheck I18n.t(:"projects.copy.members")
page.find("button:not([disabled])", text: "Save").click
expect(page).to have_content I18n.t(:label_copy_project)
expect(page).to have_content I18n.t("job_status_dialog.generic_messages.in_queue")
within_dialog "Background job status" do
expect(page).to have_heading "Copy project"
expect(page).to have_text "The job has been queued and will be processed shortly."
end
# Run background jobs twice: the background job which itself enqueues the mailer job
GoodJob.perform_inline
@@ -0,0 +1,67 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
#
require "spec_helper"
RSpec.describe Projects::CopyOptionsForm, type: :forms do
include_context "with rendered form"
let(:model) { Projects::CopyOptions.new }
shared_examples "rendering dependency checkbox" do |locator|
it "renders checked checkbox for '#{locator}'" do
expect(page).to have_checked_field locator, fieldset: "Copy options"
end
end
describe "dependencies" do
include_examples "rendering dependency checkbox", "Project members"
include_examples "rendering dependency checkbox", "Versions"
include_examples "rendering dependency checkbox", "Work packages: categories"
include_examples "rendering dependency checkbox", "Work packages"
include_examples "rendering dependency checkbox", "Work packages: attachments"
include_examples "rendering dependency checkbox", "Work packages: shares"
include_examples "rendering dependency checkbox", "Wiki pages"
include_examples "rendering dependency checkbox", "Wiki pages: attachments"
include_examples "rendering dependency checkbox", "Forums"
include_examples "rendering dependency checkbox", "Work packages: saved views"
include_examples "rendering dependency checkbox", "Boards"
include_examples "rendering dependency checkbox", "Project overview"
include_examples "rendering dependency checkbox", "File storages"
include_examples "rendering dependency checkbox", "File storages: Project folders"
include_examples "rendering dependency checkbox", "Work packages: file links"
end
describe "notifications" do
it "renders unchecked checkbox for email notifications" do
expect(page).to have_unchecked_field "Send email notifications during the project copy"
end
end
end
+51
View File
@@ -0,0 +1,51 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
#
require "spec_helper"
RSpec.describe Projects::CopyOptions, type: :model do
describe "#dependencies" do
let(:all_dependencies) { Projects::CopyService.copyable_dependencies.pluck(:identifier) }
it "has a default value" do
expect(subject.dependencies).to match_array(all_dependencies)
end
it "validates dependencies" do
expect(subject).to validate_inclusion_of(:dependencies).in_array(all_dependencies)
end
end
describe "#send_notifications" do
it "has a default value" do
expect(subject.send_notifications).to be false
end
end
end
@@ -0,0 +1,131 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
#
require "spec_helper"
RSpec.describe Projects::Settings::CustomFieldsForm, type: :forms do
let(:string_project_custom_field) { create(:string_project_custom_field, name: "String field", is_required: true) }
let(:boolean_project_custom_field) { create(:boolean_project_custom_field, name: "Boolean field", is_required: true) }
let(:text_project_custom_field) { create(:text_project_custom_field, name: "Text field", is_required: true) }
let(:integer_project_custom_field) { create(:integer_project_custom_field, name: "Integer field", is_required: true) }
let(:float_project_custom_field) { create(:float_project_custom_field, name: "Float field", is_required: true) }
let(:date_project_custom_field) { create(:date_project_custom_field, name: "Date field", is_required: true) }
let(:list_project_custom_field) do
create(:list_project_custom_field, name: "List field", is_required: true, possible_values: ["eins", "zwei", "drei"])
end
let(:multi_list_project_custom_field) do
create(:list_project_custom_field,
name: "Multi-list field",
is_required: true,
multi_value: true,
possible_values: ["uno", "due", "tre", "quattro"])
end
let(:version_project_custom_field) { create(:version_project_custom_field, name: "Version field", is_required: true) }
let(:user_project_custom_field) { create(:user_project_custom_field, name: "User field", is_required: true) }
let(:link_project_custom_field) { create(:link_project_custom_field, name: "Link field", is_required: true) }
let(:user) { create(:user) }
let(:version) { create(:version) }
let(:custom_field_values) do
{
"#{string_project_custom_field.id}": "str_val",
"#{boolean_project_custom_field.id}": true,
"#{integer_project_custom_field.id}": 43,
"#{float_project_custom_field.id}": 78.23,
"#{date_project_custom_field.id}}": Date.civil(2024, 0o3, 20),
"#{link_project_custom_field.id}}": "https://rubygems.org/",
"#{list_project_custom_field.id}}": list_project_custom_field.possible_values.first.id,
"#{multi_list_project_custom_field.id}}": multi_list_project_custom_field.possible_values.last(2).map(&:id),
"#{version_project_custom_field.id}}": version,
"#{user_project_custom_field.id}}": user
}
end
let(:model) { create(:project, custom_field_values:) }
let(:current_user) { build_stubbed(:admin) }
current_user { build_stubbed(:admin) }
include_context "with rendered form"
it "renders HTML input fields", :aggregate_failures do
expect(page).to have_field "String field", with: "str_val", required: true
expect(page).to have_checked_field "Boolean field", required: true
expect(page).to have_field "Integer field", type: :number, with: "43", required: true
expect(page).to have_field "Float field", type: :number, with: "78.23", required: true
expect(page).to have_field "Date field", type: :date, with: "2024-03-20", required: true
expect(page).to have_field "Link field", with: "https://rubygems.org/", required: true
end
it "renders list field" do
expect(page).to have_element :label, text: "List field"
label_id = page.find(:element, :label, text: "List field")["for"]
expect(page).to have_element "opce-autocompleter", "data-label-for-id": "\"#{label_id}\"" do |autocompleter|
expect(autocompleter["data-multiple"]).to be_json_eql(%{false})
expect(autocompleter["data-items"]).to have_json_size(3)
expect(autocompleter["data-model"]).to be_json_eql(%{{"name": "eins"}})
end
end
it "renders multi-list field" do
expect(page).to have_element :label, text: "Multi-list field"
label_id = page.find(:element, :label, text: "Multi-list field")["for"]
expect(page).to have_element "opce-autocompleter", "data-label-for-id": "\"#{label_id}\"" do |autocompleter|
expect(autocompleter["data-multiple"]).to be_json_eql(%{true})
expect(autocompleter["data-items"]).to have_json_size(4)
expect(autocompleter["data-model"]).to have_json_size(2)
expect(autocompleter["data-model"]).to be_json_eql(%{[{"name": "tre"}, {"name": "quattro"}]})
end
end
it "renders version field" do
expect(page).to have_element :label, text: "Version field"
label_id = page.find(:element, :label, text: "Version field")["for"]
expect(page).to have_element "opce-autocompleter", "data-label-for-id": "\"#{label_id}\"" do |autocompleter|
expect(autocompleter["data-items"]).to have_json_size(0)
expect(autocompleter["data-model"]).to be_json_eql(%{null})
end
end
it "renders user field" do
expect(page).to have_element :label, text: "User field"
label_id = page.find(:element, :label, text: "User field")["for"]
expect(page).to have_element "opce-user-autocompleter", "data-label-for-id": "\"#{label_id}\"" do |autocompleter|
expect(autocompleter["data-resource"]).to be_json_eql(%{"principals"})
expect(autocompleter["data-url"]).to be_json_eql(%{"/api/v3/principals"})
expect(autocompleter["data-input-value"]).to be_json_eql(%{"#{user.id}"})
end
end
end
@@ -0,0 +1,49 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
#
require "spec_helper"
RSpec.describe Projects::SubmitOrCancel, type: :forms do
include_context "with rendered form"
let(:model) { build_stubbed(:project) }
it "renders a horizontal group" do
expect(page).to have_css ".FormControl-horizontalGroup"
end
it "renders submit button" do
expect(page).to have_button "Create", class: "Button--primary"
end
it "renders cancel link" do
expect(page).to have_link "Cancel", class: "Button--secondary"
end
end
@@ -0,0 +1,62 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
#
require "spec_helper"
RSpec.describe Projects::TemplateAutocompleter, type: :forms do
include ViewComponent::TestHelpers
def render_form
render_in_view_context(template_id, described_class) do |template_id, described_class|
primer_form_with(url: "/foo") do |f|
render(described_class.new(f, template_id:))
end
end
end
before do
render_form
end
let(:template_id) { 1001 }
it "renders field" do
expect(page).to have_element "opce-project-autocompleter", "data-input-name": "\"template_id\"" do |element|
expect(element["data-input-value"]).to eq "1001"
end
end
it "connects Stimulus controller actions" do
expect(page).to have_element "opce-project-autocompleter", "data-input-name": "\"template_id\"" do |element|
expect(element["data-action"]).to include "change->highlight-when-value-selected#itemSelected"
expect(element["data-action"]).to include "change->auto-submit#submit"
end
end
end
+64
View File
@@ -0,0 +1,64 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# 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 COPYRIGHT and LICENSE files for more details.
#++
#
require "spec_helper"
RSpec.describe Projects::TemplateForm, type: :forms do
include ViewComponent::TestHelpers
def render_form
render_in_view_context(
model,
template,
copy_options,
described_class
) do |model, template, copy_options, described_class|
primer_form_with(url: "/foo", model:) do |f|
render(described_class.new(f, template:, copy_options:))
end
end
end
before do
render_form
end
let(:model) { build_stubbed(:project) }
let(:template) { build_stubbed(:template_project) }
let(:copy_options) { Projects::CopyOptions.new }
it "renders hidden field" do
expect(page).to have_field "template_id", type: :hidden, with: template.id
end
it "renders Copy options" do
expect(page).to have_selector :fieldset, "Copy options"
end
end
+8
View File
@@ -59,6 +59,14 @@ RSpec.describe ProjectsController do
end
end
describe "create" do
it do
expect(post("/projects")).to route_to(
controller: "projects", action: "create"
)
end
end
describe "destroy_info" do
it do
expect(get("/projects/123/destroy_info")).to route_to(