Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ class SwitchFormComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
include OpTurbo::Streamable

# Two places switch a project's variant through the same service, so each names the
# route it posts to rather than one of them being the default.
def initialize(project:, source:, url:, selected: source, validation_message: nil)
super()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,30 @@ See COPYRIGHT and LICENSE files for more details.
++#%>

<%= component_wrapper do %>
<%= render(Primer::OpenProject::SubHeader.new(test_selector: "type-projects-sub-header")) do |sub_header| %>
<%= render(
Primer::OpenProject::SubHeader.new(
test_selector: "type-projects-sub-header",
data: filters_form_attributes
)
) do |sub_header| %>
<%
sub_header.with_filter_input(
name: name_filter_key,
label: t("projects.index.search.label"),
value: name_filter_value,
placeholder: t("projects.index.search.placeholder"),
leading_visual: { icon: :search, size: :small },
clear_button_id:,
data: name_filter_attributes
)
%>

<% if variant_filter_available? %>
<% sub_header.with_quick_filter do %>
<%= render(variant_filter_component) %>
<% end %>
<% end %>

<%
sub_header.with_action_button(
scheme: :primary,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,46 @@ class SubHeaderComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
include OpTurbo::Streamable

def initialize(variant:)
def initialize(type:, variant:, query:)
super()

@type = type
@variant = variant
@query = query
end

def filters_form_attributes
{
controller: "filter--filters-form",
"filter--filters-form-perform-turbo-requests-value": true,
"filter--filters-form-clear-button-id-value": clear_button_id
}
end

def name_filter_attributes
{
"filter-name": name_filter_key,
"filter-type": "string",
"filter-operator": "~",
"filter--filters-form-target": "simpleFilter filterValueContainer simpleValue"
}
end

def name_filter_key = ::Queries::Projects::Filters::NameAndIdentifierFilter.key.to_s

def name_filter_value = query.find_active_filter(name_filter_key.to_sym)&.values&.first

def clear_button_id = "type-projects-filters-clear-button"

def variant_filter_available? = variant.default?

def variant_filter_component
VariantFilterComponent.new(type:, variant:, query:)
end

private

attr_reader :variant
attr_reader :type, :variant, :query

def add_path = url_helpers.new_link_type_projects_path(**variant.path_args)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,14 @@ def use_quick_action_table_headers? = false

def variant = params[:variant]

# Inviting the admin to add a project reads as a lie when they have just searched for one:
Comment thread
mrmir marked this conversation as resolved.
# the table is empty because of the filter, not because the variant has no projects.
def empty_row_message
I18n.t("types.edit.projects.empty_state.description")
if params[:filtered]
I18n.t("types.edit.projects.empty_state.no_results")
else
I18n.t("types.edit.projects.empty_state.description")
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ def label_for(project)
return project.name if applied.nil? || applied == variant

render(Primer::BaseComponent.new(tag: :span, display: :inline_flex, align_items: :center)) do
safe_join([project.name, render(Primer::Beta::Label.new(scheme: :secondary, ml: 2)) { applied.composite_name }])
safe_join([project.name,
render(Primer::Beta::Text.new(font_weight: :bold, ml: 2)) { applied.composite_name }])
end
end

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# 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 WorkPackageTypes
module ProjectsTab
class VariantFilterComponent < OpPrimer::QuickFilter::SelectPanelComponent
def initialize(type:, variant:, query:)
@type = type
@variant = variant

super(name: TypeVariant.model_name.human, query:, filter_key: :type_variant_id, path_args: [])

type.variants.in_display_order.each do |sibling|
with_item(label: "#{sibling.composite_name} (#{project_counts[sibling.id]})", value: sibling.id)
end
end

private

def project_counts
@project_counts ||= Hash.new(0).merge(
ProjectType.where(type_id: @type.id).group(:variant_id).count
)
end

def base_url = tab_path(base_url_params)

def item_href(value)
selected = other_filters + [{ @filter_key.to_s => { "operator" => @operator, "values" => [value.to_s] } }]

tab_path(filters: selected.to_json)
end

def tab_path(params)
helpers.edit_type_projects_path(**@variant.path_args, **params)
end
end
end
end
87 changes: 75 additions & 12 deletions app/controllers/work_package_types/projects_tab_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,27 @@ class ProjectsTabController < BaseTabController
include OpTurbo::ComponentStream
include TypeDeactivationErrorMessage

VARIANT_FILTER_KEY = ::Queries::Projects::Filters::TypeVariantFilter.key.to_s
NAME_FILTER_KEY = ::Queries::Projects::Filters::NameAndIdentifierFilter.key.to_s

before_action :load_query, only: %i[edit update link unlink switch enable_all_projects]
before_action :load_linked_project, only: %i[unlink new_switch switch]

current_menu_item [:edit, :update] do
:types
end

def edit; end
helper_method :projects_table_component, :sub_header_component

def edit
respond_to do |format|
format.html
format.turbo_stream do
update_via_turbo_stream(component: projects_table_component)
respond_with_turbo_streams
end
end
end

def update
result = sync_projects(desired_project_ids)
Expand Down Expand Up @@ -96,15 +109,15 @@ def unlink

def new_switch
respond_with_dialog ::Projects::Settings::WorkPackages::Types::SwitchDialogComponent.new(
project: @linked_project, source: @variant, url: switch_path
project: @linked_project, source: applied_variant, url: switch_path
)
end

def switch
target = @type.variants.find_by(id: params[:target_id])
result = ::Projects::Types::SwitchVariantService
.new(user: current_user, model: @linked_project)
.call(source: @variant, target:)
.call(source: applied_variant, target:)

result.on_success { on_switched(target) }
result.on_failure { on_switch_refused(target, result) }
Expand All @@ -129,16 +142,58 @@ def enable_all_projects

def load_query
@query = ProjectQuery.new(name: "work-package-type-variant-projects-#{@variant.id}") do |query|
query.where(:type_variant_id, "=", [@variant.id.to_s])
query.where(:type_variant_id, "=", filtered_variant_ids)
query.where(:name_and_identifier, "~", [project_name_term]) if project_name_term.present?
query.select(:name)
query.order("lft" => "asc")
end
end

# The text input and the variant panel both write the one `filters` param, which is what lets
# them narrow the table together rather than overwriting each other.

def project_name_term
@project_name_term ||= values_for(NAME_FILTER_KEY).first.to_s.strip
end

def filtered_variant_ids
(requested_variant_ids & own_variant_ids).presence || default_variant_ids
end

def default_variant_ids
params[:variant_id].present? ? [@variant.id.to_s] : own_variant_ids
end

def own_variant_ids
@own_variant_ids ||= @type.variants.pluck(:id).map(&:to_s)
end

def requested_variant_ids = values_for(VARIANT_FILTER_KEY)

def values_for(filter_key)
requested_filters
.select { |filter| filter[:attribute].to_s == filter_key }
.flat_map { |filter| Array(filter[:values]).map(&:to_s) }
end

# Parsed by the app's own parser rather than by hand: the filter form writes
# `name ~ "term"` by default and only switches to JSON when told to, and both reach here.
def requested_filters
return @requested_filters if defined?(@requested_filters)

@requested_filters = params[:filters].blank? ? [] : Array(::Queries::ParamsParser.parse(params)[:filters])
rescue StandardError
@requested_filters = []
end

def load_linked_project
@linked_project = ::Project.find(params.expect(:project_id))
end

def applied_variant
@applied_variant ||= @linked_project.type_variant(@type)
end

def switch_path
switch_type_projects_path(**@variant.path_args, project_id: @linked_project.id)
end
Expand Down Expand Up @@ -203,21 +258,29 @@ def on_switch_refused(target, result)
update_via_turbo_stream(
component: ::Projects::Settings::WorkPackages::Types::SwitchFormComponent.new(
project: @linked_project,
source: @variant,
selected: target || @variant,
source: applied_variant,
selected: target || applied_variant,
validation_message: message,
url: switch_path
)
)
end

def refresh_projects
update_via_turbo_stream(
component: ProjectsTab::TableComponent.new(
query: @query, params: params.merge(variant: @variant, url_for_action: :edit)
)
update_via_turbo_stream(component: projects_table_component)
replace_via_turbo_stream(component: sub_header_component)
end

# Built in one place so the first render and every stream that replaces it cannot drift.
def sub_header_component
ProjectsTab::SubHeaderComponent.new(type: @type, variant: @variant, query: @query)
end

def projects_table_component
ProjectsTab::TableComponent.new(
query: @query,
params: params.merge(variant: @variant, filtered: requested_filters.any?, url_for_action: :edit)
)
replace_via_turbo_stream(component: ProjectsTab::SubHeaderComponent.new(variant: @variant))
end

def refuse_empty_selection
Expand Down Expand Up @@ -294,7 +357,7 @@ def blocked_message(project_ids)
end

def removal_refusal_message(result)
return type_deactivation_error_message(@variant, project: @linked_project) if blocked?(@linked_project)
return type_deactivation_error_message(applied_variant, project: @linked_project) if blocked?(@linked_project)

"#{@linked_project.name}: #{result.errors.full_messages.to_sentence}"
end
Expand Down
13 changes: 3 additions & 10 deletions app/views/work_package_types/projects_tab/edit.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,6 @@ See COPYRIGHT and LICENSE files for more details.

<%= render ::Types::EditPageHeaderComponent.new(type: @type, variant: @variant, tabs: types_tabs) %>

<%= render(WorkPackageTypes::ProjectsTab::SubHeaderComponent.new(variant: @variant)) %>

<%=
render(
WorkPackageTypes::ProjectsTab::TableComponent.new(
query: @query,
params: params.merge(variant: @variant, url_for_action: :edit)
)
)
%>
<%= render(sub_header_component) %>

<%= render(projects_table_component) %>
1 change: 1 addition & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6312,6 +6312,7 @@ en:
disable_all: Disable for all projects
empty_state:
description: Add a project to use this configuration there.
no_results: No matching projects.
enable_all: Enable for all projects
select_projects: Select projects
select_projects_description: Select the projects in which you would like to use this type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def render_tree(projects)
it "labels the node with that variant" do
render_tree([project])

expect(page).to have_css(".Label", text: other_variant.composite_name)
expect(page).to have_css(".text-bold", text: other_variant.composite_name)
end
end

Expand All @@ -83,7 +83,7 @@ def render_tree(projects)
render_tree([project])

expect(page).to have_text("Bookshop")
expect(page).to have_no_css(".Label")
expect(page).to have_no_css(".text-bold")
end

it "cannot be picked again" do
Expand All @@ -100,7 +100,7 @@ def render_tree(projects)
render_tree([project])

expect(page).to have_text("Bookshop")
expect(page).to have_no_css(".Label")
expect(page).to have_no_css(".text-bold")
end
end
end
Loading
Loading