Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f19a585
Add Inline SVG dependency
myabc Nov 11, 2025
2c3a84d
Implement advanced check, radio form controls
myabc Nov 14, 2025
d18e913
[#69073] Rework Template Selector on Create form
myabc Nov 14, 2025
fc0df1c
Limit (clamp) UG template description to 3 lines
myabc Nov 14, 2025
cc0c239
Pass :current_user explicitly to component
myabc Nov 16, 2025
6f69140
Indent caption, aligning with label text
myabc Nov 16, 2025
de219c8
Remove redundant name parameter from radio_button calls in specs (#21…
Copilot Nov 17, 2025
5e7841f
reorder allowed types to put them in order portfolio > program > project
toy Nov 10, 2025
2b134e9
cleanup using tap and memoization Projects::IndexSubHeaderComponent#a…
toy Nov 10, 2025
65b48bd
remove unnecessary usage of Project.workspace_types[:xxx]
toy Nov 12, 2025
6aa6e00
keep workspace type url when selecting template
toy Nov 12, 2025
4cb4ed7
Explicit value for key :project instead of defaulting to it
toy Nov 17, 2025
ae50386
Fix bug on calling workspace_icon with the wrong argument, fix specs.
dombesz Nov 17, 2025
5fe87f7
Merge pull request #21062 from opf/new-project-template-selector-fixes
toy Nov 17, 2025
e858346
Use the apropriate project, portfolio, program name for the blank pro…
dombesz Nov 17, 2025
a4fa97d
Remove unnecessary .html_safe methods.
dombesz Nov 18, 2025
c516e42
Remove icon from the template selector
HDinger Nov 18, 2025
79bf18c
Move component specific styles to an own file
HDinger Nov 18, 2025
2d3c093
Merge branch 'dev' into feature/68856-new-project-template-selector
dombesz Nov 18, 2025
7c6ef35
Update specs to use accessible_description from capybara.
dombesz Nov 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ gem "view_component", "~> 4.1.1"
# Lookbook
gem "lookbook", "2.3.13"

gem "inline_svg", "~> 1.10.0"
Comment thread
HDinger marked this conversation as resolved.

# Require factory_bot for usage with openproject plugins testing
gem "factory_bot", "~> 6.5.6", require: false
# require factory_bot_rails for convenience in core development
Expand Down
5 changes: 5 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,9 @@ GEM
ostruct
ice_cube (0.17.0)
ice_nine (0.11.2)
inline_svg (1.10.0)
activesupport (>= 3.0)
nokogiri (>= 1.6)
interception (0.5)
io-console (0.8.1)
irb (1.15.3)
Expand Down Expand Up @@ -1610,6 +1613,7 @@ DEPENDENCIES
i18n-tasks (~> 1.0.13)
ice_cube (~> 0.17.0)
ice_nine
inline_svg (~> 1.10.0)
json_schemer (~> 2.4.0)
json_spec (~> 1.1.4)
ladle
Expand Down Expand Up @@ -1946,6 +1950,7 @@ CHECKSUMS
icalendar (2.12.1) sha256=ecff56c550aed551f29ad1faad0da54bf62362dfaf22a428bd7ad782938fe764
ice_cube (0.17.0) sha256=32deb45dda4b4acc53505c2f581f6d32b5afc04d29b9004769944a0df5a5fcbe
ice_nine (0.11.2) sha256=5d506a7d2723d5592dc121b9928e4931742730131f22a1a37649df1c1e2e63db
inline_svg (1.10.0) sha256=5b652934236fd9f8adc61f3fd6e208b7ca3282698b19f28659971da84bf9a10f
interception (0.5) sha256=a53818d636752a8df90d8c1bb2f7b6e13a7b828543cb02b50fbde98b849d7907
io-console (0.8.1) sha256=1e15440a6b2f67b6ea496df7c474ed62c860ad11237f29b3bd187f054b925fcb
irb (1.15.3) sha256=4349edff1efa7ff7bfd34cb9df74a133a588ba88c2718098b3b4468b81184aaa
Expand Down
23 changes: 10 additions & 13 deletions app/components/projects/index_sub_header_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,26 +72,23 @@ def clear_button_id
end

def new_workspace_path(type)
case type
when Project.workspace_types[:project]
new_project_path
when Project.workspace_types[:portfolio]
new_portfolio_path
when Project.workspace_types[:program]
new_program_path
end
return unless Project.workspace_types.key?(type)

url_for([:new, type.to_sym])
end

def new_workspace_label(type)
I18n.t(:"label_#{type}")
end

def allowed_new_workspace_types
allowed_types = []
allowed_types << Project.workspace_types[:project] if @current_user.allowed_globally?(:add_project)
allowed_types << Project.workspace_types[:portfolio] if @current_user.allowed_globally?(:add_portfolios) && OpenProject::FeatureDecisions.portfolio_models_active?
allowed_types << Project.workspace_types[:program] if @current_user.allowed_globally?(:add_programs) && OpenProject::FeatureDecisions.portfolio_models_active?
allowed_types
@allowed_new_workspace_types ||= [].tap do |types|
if OpenProject::FeatureDecisions.portfolio_models_active?
types << "portfolio" if @current_user.allowed_globally?(:add_portfolios)
types << "program" if @current_user.allowed_globally?(:add_programs)
end
types << "project" if @current_user.allowed_globally?(:add_project)
end
end

def for_a_single_new_allowed_type
Expand Down
31 changes: 2 additions & 29 deletions app/components/projects/template_select_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -30,42 +30,15 @@ See COPYRIGHT and LICENSE files for more details.
<%=
component_wrapper do
settings_primer_form_with(
url: new_project_path,
url: new_workspace_path,
method: :get,
data: {
turbo_frame: Projects::NewComponent.wrapper_key,
controller: "auto-submit",
auto_submit_delay_value: 0
}
) do |f|
render(
OpPrimer::InsetBoxComponent.new(my: 3)
) do
template_id_value = template_id
parent_id_value = parent_id
render_inline_form(f) do |form|
form.project_autocompleter(
name: :template_id,
label: I18n.t("create_project.template_label"),
autocomplete_options: {
focusDirectly: false,
dropdownPosition: "bottom",
inputValue: template_id_value,
placeholder: I18n.t("label_none_parentheses"),
filters: [
{ name: "user_action", operator: "=", values: ["projects/copy"] },
{ name: "templated", operator: "=", values: ["t"] }
],
data: {
action: "change->auto-submit#submit",
"qa-field-name": "use_template"
}
}
)

form.hidden name: :parent_id, value: parent_id_value
end
end
render Projects::TemplateSelectForm.new(f, template_id:, parent_id:, workspace_type: project.workspace_type, current_user:)
end
end
%>
16 changes: 15 additions & 1 deletion app/components/projects/template_select_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,28 @@

module Projects
class TemplateSelectComponent < ApplicationComponent
extend Dry::Initializer[undefined: false]
include ApplicationHelper
include OpPrimer::ComponentHelpers
include OpTurbo::Streamable

options :template, :parent
option :project
option :template
option :parent, optional: true
option :current_user, default: -> { User.current }

private

def new_workspace_path
workspace_type = if Project.workspace_types.key?(project.workspace_type)
project.workspace_type.to_sym
else
:project
end

url_for([:new, workspace_type])
end

def template_id = template&.id
def parent_id = parent&.id
end
Expand Down
107 changes: 107 additions & 0 deletions app/forms/projects/template_select_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# 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 TemplateSelectForm < ApplicationForm
extend Dry::Initializer[undefined: false]
include OpenProject::TextFormatting

BLANK_VALUE = ""
private_constant :BLANK_VALUE

option :template_id
option :parent_id, optional: true
option :workspace_type
option :current_user, default: -> { User.current }

delegate :strip_tags, to: :@view_context

form do |f|
f.advanced_radio_button_group(
name: :template_id,
label: I18n.t("create_project.template_label"),
scope_name_to_model: false,
data: {
action: "change->auto-submit#submit",
qa_field_name: "use_template"
}
) do |group|
group.radio_button(
value: BLANK_VALUE,
label: blank_template_label,
id: "template_id_blank",
caption: blank_template_caption,
checked: template_id.blank?
)

available_templates.each do |template|
group.radio_button(
value: template.id,
label: template.name,
caption: format_caption(template.description),
checked: template.id == template_id
)
end
end

f.hidden(name: :parent_id, value: parent_id, scope_name_to_model: false) if parent_id
end

private

def available_templates
@available_templates ||= Project
.visible(current_user)
.active
.templated
.order(name: :asc)
end

def format_caption(text)
Comment thread
myabc marked this conversation as resolved.
return I18n.t("create_project.blank_description") if text.blank?

render(Primer::Beta::Text.new(classes: %w[line-clamp-3 lh-default])) do
strip_tags(format_text(text))
end
end

def blank_template_label
return unless Project.workspace_types.key?(workspace_type)

I18n.t("create_#{workspace_type}.blank_template.label")
end

def blank_template_caption
return unless Project.workspace_types.key?(workspace_type)

I18n.t("create_#{workspace_type}.blank_template.description")
end
end
end
27 changes: 10 additions & 17 deletions app/helpers/workspace_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,17 @@
#++

module WorkspaceHelper
WORKSPACE_ICON_MAPPING = {
project: :project,
portfolio: :briefcase,
program: :"project-roadmap"
}.with_indifferent_access.freeze

def new_workspace_title(workspace)
if workspace.project?
I18n.t(:label_project_new)
elsif workspace.portfolio?
I18n.t(:label_portfolio_new)
elsif workspace.program?
I18n.t(:label_program_new)
end
end
return unless Project.workspace_types.key?(workspace.workspace_type)

def workspace_icon(type)
case type
when Project.workspace_types[:portfolio]
:briefcase
when Project.workspace_types[:program]
:"project-roadmap"
else
:project
end
I18n.t(:"label_#{workspace.workspace_type}_new")
end

def workspace_icon(type) = WORKSPACE_ICON_MAPPING[type]
end
2 changes: 1 addition & 1 deletion app/seeders/demo_data/project_seeder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ def project_attributes # rubocop:disable Metrics/AbcSize
enabled_module_names: project_data.lookup("modules"),
types: Type.all,
parent: Project.find_by(identifier: project_data.lookup("parent")),
workspace_type: Project.workspace_types[:project]
workspace_type: "project"
}
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/seeders/development_data/projects_seeder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def project_data(identifier)
identifier:,
enabled_module_names: project_modules,
types: Type.all,
workspace_type: Project.workspace_types[:project]
workspace_type: "project"
}
end

Expand Down
4 changes: 3 additions & 1 deletion app/views/projects/new.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ See COPYRIGHT and LICENSE files for more details.

<%=
render Projects::TemplateSelectComponent.new(
project: @new_project,
template: @template,
parent: @parent
parent: @parent,
current_user:
)
%>

Expand Down
14 changes: 14 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2236,6 +2236,20 @@ en:
template_label: "Use template"
copy_options:
dependencies_label: "Copy from template"
blank_template:
label: "Blank project"
description: Start from scratch. Manually add project attributes, members and modules.
blank_description: No description provided.

create_portfolio:
blank_template:
label: "Blank portfolio"
description: Start from scratch. Manually add portfolio attributes, members and modules.

create_program:
blank_template:
label: "Blank program"
description: Start from scratch. Manually add program attributes, members and modules.

create_wiki_page: "Create new wiki page"
create_wiki_page_button: "Wiki page"
Expand Down
1 change: 1 addition & 0 deletions frontend/src/global_styles/content/_index.sass
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
@import activity_list
@import activity_days
@import hierachy_custom_field_layout
@import text_utils

@import menus/menu_blocks
@import editor/index
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/global_styles/content/_text_utils.sass
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@for $i from 1 through 6
.line-clamp-#{$i}
@include line-clamp($i)
1 change: 1 addition & 0 deletions frontend/src/global_styles/openproject.sass
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@
// Component specific Styles
@import "../../../app/components/_index.sass"
@import "../../../app/forms/_index.sass"
@import "../../../lib/primer/open_project/forms/_index.sass"
6 changes: 6 additions & 0 deletions frontend/src/global_styles/openproject/_mixins.sass
Original file line number Diff line number Diff line change
Expand Up @@ -388,3 +388,9 @@ $scrollbar-size: 10px
@media (hover: hover)
&:hover
border-color: var(--borderColor-default) !important

@mixin line-clamp($value)
display: -webkit-box
-webkit-line-clamp: $value
-webkit-box-orient: vertical
overflow: hidden
3 changes: 1 addition & 2 deletions lib/api/v3/projects/create_form_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ class CreateFormAPI < ::API::OpenProjectAPI

post &::API::V3::Utilities::Endpoints::CreateForm.new(model: Project,
params_modifier: ->(attributes) {
attributes[:workspace_type] = Project.workspace_types[:project]
attributes
attributes.merge!(workspace_type: "project")
})
.mount
end
Expand Down
3 changes: 1 addition & 2 deletions lib/api/v3/projects/projects_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,7 @@ class ProjectsAPI < ::API::OpenProjectAPI

post &::API::V3::Utilities::Endpoints::Create.new(model: Project,
params_modifier: ->(attributes) {
attributes[:workspace_type] = Project.workspace_types[:project]
attributes
attributes.merge!(workspace_type: "project")
})
.mount

Expand Down
1 change: 1 addition & 0 deletions lib/primer/open_project/forms/_index.sass
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import "advanced_form_group"
Loading
Loading