Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#61398] Version autocompleter for filter values on the project list #17893

Draft
wants to merge 16 commits into
base: dev
Choose a base branch
from
Draft
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
27 changes: 23 additions & 4 deletions app/components/filter/filter_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,20 +70,39 @@ def additional_filter_attributes(filter)
{ autocomplete_options: project_autocomplete_options }
when Queries::Filters::Shared::CustomFields::User
{ autocomplete_options: user_autocomplete_options }
when Queries::Filters::Shared::CustomFields::ListOptional,
Queries::Projects::Filters::ProjectStatusFilter,
when Queries::Filters::Shared::CustomFields::ListOptional
{ autocomplete_options: custom_field_list_autocomplete_options(filter) }
when Queries::Projects::Filters::ProjectStatusFilter,
Queries::Projects::Filters::TypeFilter
{ autocomplete_options: list_autocomplete_options(filter) }
else
{}
end
end

def custom_field_list_autocomplete_options(filter)
options = if filter.custom_field.version?
{
items: filter.allowed_values.map { |name, id, project_name| { name:, id:, project_name: } },
groupBy: "project_name"
}
else
{ items: filter.allowed_values.map { |name, id| { name:, id: } } }
end

autocomplete_options.merge(options).merge(model: filter.values)
end

def list_autocomplete_options(filter)
autocomplete_options.merge(
items: filter.allowed_values.map { |name, id| { name:, id: } },
model: filter.values
)
end

def autocomplete_options
{
component: "opce-autocompleter",
items: filter.allowed_values.map { |name, id| { name:, id: } },
model: filter.values,
bindValue: "id",
bindLabel: "name",
hideSelected: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class CustomFields::Inputs::MultiVersionSelectList < CustomFields::Inputs::Base:
list.option(
label: version.name,
value: version.id,
group_by: version.project.name,
selected: selected?(version)
)
end
Expand Down
4 changes: 3 additions & 1 deletion app/forms/custom_fields/inputs/single_version_select_list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ class CustomFields::Inputs::SingleVersionSelectList < CustomFields::Inputs::Base
custom_value_form.autocompleter(**version_input_attributes) do |list|
assignable_custom_field_values(@custom_field).each do |version|
list.option(
label: version.name, value: version.id,
label: version.name,
value: version.id,
group_by: version.project.name,
selected: selected?(version)
)
end
Expand Down
18 changes: 8 additions & 10 deletions app/forms/custom_fields/inputs/version_select.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,23 +38,21 @@ def version_input_attributes
end

def additional_attributes
autocomplete_options = { groupBy: "group_by" }

if @object.blank? || (@object.respond_to?(:project) && @object.project.blank?)
{
autocomplete_options: {
disabled: true,
placeholder: I18n.t("custom_fields.placeholder_version_select")
}
}
else
{}
autocomplete_options[:disabled] = true
autocomplete_options[:placeholder] = I18n.t("custom_fields.placeholder_version_select")
end

{ autocomplete_options: }
end

def assignable_versions(only_open:)
if @object.is_a?(Project)
@object.assignable_versions(only_open: only_open)
@object.assignable_versions(only_open:)
elsif @object.respond_to?(:project) && @object.project.present?
@object.project.assignable_versions(only_open: only_open)
@object.project.assignable_versions(only_open:)
else
Version.none
end
Expand Down
121 changes: 34 additions & 87 deletions app/helpers/custom_fields_helper.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
Expand Down Expand Up @@ -62,87 +64,13 @@ def custom_fields_tabs
]
end

# Return custom field html tag corresponding to its format
def custom_field_tag(name, custom_value) # rubocop:disable Metrics/AbcSize,Metrics/PerceivedComplexity
custom_field = custom_value.custom_field
field_name = "#{name}[custom_field_values][#{custom_field.id}]"
field_id = "#{name}_custom_field_values_#{custom_field.id}"

field_format = OpenProject::CustomFieldFormat.find_by(name: custom_field.field_format)

tag = case field_format.try(:edit_as)
when "date"
angular_component_tag "opce-basic-single-date-picker",
inputs: {
required: custom_field.is_required,
value: custom_value.value,
id: field_id,
name: field_name
}
when "text"
styled_text_area_tag(field_name, custom_value.value, id: field_id, rows: 3, container_class: "-middle",
required: custom_field.is_required)
when "bool"
hidden_tag = hidden_field_tag(field_name, "0")
checkbox_tag = styled_check_box_tag(field_name, "1", custom_value.typed_value, id: field_id,
required: custom_field.is_required)
hidden_tag + checkbox_tag
when "list"
blank_option = if custom_field.is_required? && custom_field.default_value.blank?
"<option value=\"\">--- #{I18n.t(:actionview_instancetag_blank_option)} ---</option>"
elsif custom_field.is_required? && custom_field.default_value.present?
""
else
"<option></option>"
end

options = blank_option.html_safe + options_for_select(custom_field.possible_values_options(custom_value.customized),
custom_value.value)

styled_select_tag(field_name, options, id: field_id, container_class: "-middle", required: custom_field.is_required)
else
styled_text_field_tag(field_name, custom_value.value, id: field_id, container_class: "-middle",
required: custom_field.is_required)
end

tag = content_tag :span, tag, lang: custom_field.name_locale, class: "form--field-container"

if custom_value.errors.empty?
tag
else
ActionView::Base.wrap_with_error_span(tag, custom_value, "value")
end
end

# Return custom field label tag
def custom_field_label_tag(name, custom_value)
content_tag "label", h(custom_value.custom_field.name) +
(custom_value.custom_field.is_required? ? content_tag("span", " *", class: "required") : ""),
for: "#{name}_custom_field_values_#{custom_value.custom_field.id}",
class: "form--label #{custom_value.errors.empty? ? nil : 'error'}",
lang: custom_value.custom_field.name_locale
end

def hidden_custom_field_label_tag(name, custom_value)
content_tag "label", h(custom_value.custom_field.name) +
(custom_value.custom_field.is_required? ? content_tag("span", " *", class: "required") : ""),
for: "#{name}_custom_field_values_#{custom_value.custom_field.id}",
class: "hidden-for-sighted",
lang: custom_value.custom_field.name_locale
end

def blank_custom_field_label_tag(name, custom_field)
content_tag "label", h(custom_field.name) +
(custom_field.is_required? ? content_tag("span", " *", class: "required") : ""),
for: "#{name}_custom_field_values_#{custom_field.id}",
class: "form--label"
end

# Return custom field tag with its label tag
def custom_field_tag_with_label(name, custom_value)
custom_field_label_tag(name, custom_value) + custom_field_tag(name, custom_value)
end

def custom_field_tag_for_bulk_edit(name, custom_field, project = nil) # rubocop:disable Metrics/AbcSize
field_name = "#{name}[custom_field_values][#{custom_field.id}]"
field_id = "#{name}_custom_field_values_#{custom_field.id}"
Expand All @@ -158,22 +86,20 @@ def custom_field_tag_for_bulk_edit(name, custom_field, project = nil) # rubocop:
when "text"
styled_text_area_tag(field_name, "", id: field_id, rows: 3, with_text_formatting: true)
when "bool"
styled_select_tag(field_name, options_for_select([[I18n.t(:label_no_change_option), ""],
([I18n.t(:label_none), "none"] unless custom_field.required?),
[I18n.t(:general_text_yes), "1"],
[I18n.t(:general_text_no), "0"]].compact), id: field_id)
styled_select_tag(field_name,
options_for_select([([I18n.t(:label_none), "none"] unless custom_field.required?),
[I18n.t(:general_text_yes), "1"],
[I18n.t(:general_text_no), "0"]].compact),
id: field_id,
include_blank: I18n.t(:label_no_change_option))
when "list"
base_options = [[I18n.t(:label_no_change_option), ""]]
unless custom_field.required?
unset_label = custom_field.field_format == "user" ? :label_nobody : :label_none
base_options << [I18n.t(unset_label), "none"]
end
styled_select_tag(field_name,
options_for_select(base_options + custom_field.possible_values_options(project)),
options_for_list(custom_field, project),
id: field_id,
multiple: custom_field.multi_value?)
multiple: custom_field.multi_value?,
include_blank: I18n.t(:label_no_change_option))
when "hierarchy"
base_options = [[I18n.t(:label_no_change_option), ""]]
base_options = []
result = CustomFields::Hierarchy::HierarchicalItemService.new
.get_descendants(item: custom_field.hierarchy_root, include_self: false)
.either(
Expand All @@ -184,7 +110,11 @@ def custom_field_tag_for_bulk_edit(name, custom_field, project = nil) # rubocop:
label = item.short.present? ? "#{item.label} (#{item.short})" : item.label
[label, item.id]
end
styled_select_tag(field_name, options_for_select(options), id: field_id, multiple: custom_field.multi_value?)
styled_select_tag(field_name,
options_for_select(options),
id: field_id,
multiple: custom_field.multi_value?,
include_blank: I18n.t(:label_no_change_option))
else
styled_text_field_tag(field_name, "", id: field_id)
end
Expand Down Expand Up @@ -221,4 +151,21 @@ def label_for_custom_field_format(format_string)

"#{label}#{suffix}"
end

def options_for_list(custom_field, project)
base_options = []
unless custom_field.required?
unset_label = custom_field.field_format == "user" ? :label_nobody : :label_none
base_options << [I18n.t(unset_label), "none"]
end

possible_values = custom_field.possible_values_options(project)
options = if custom_field.version?
grouped_options_for_select(possible_values.group_by(&:last).to_a)
else
options_for_select(possible_values)
end

options_for_select(base_options) + options
end
end
83 changes: 50 additions & 33 deletions app/models/custom_field.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
Expand Down Expand Up @@ -163,8 +165,10 @@ def value_of(value)
# You MUST NOT pass a customizable if this CF has any other format
def possible_values(obj = nil)
case field_format
when "user", "version"
possible_values_options(obj).map(&:last)
when "user"
possible_users(obj).pluck(:id).map(&:to_s)
when "version"
possible_versions(obj).pluck(:id).map(&:to_s)
when "list"
custom_options
else
Expand Down Expand Up @@ -314,31 +318,26 @@ def cache_key

private

def possible_versions(obj)
project = deduce_project(obj)
deduce_versions(project)
end

def possible_version_values_options(obj)
mapped_with_deduced_project(obj) do |project|
if project&.persisted?
project.shared_versions
else
Version.systemwide
end
end
possible_versions(obj).references(:project)
.sort
.map { |u| [u.name, u.id.to_s, u.project.name] }
end

def possible_users(obj)
project = deduce_project(obj)
deduce_principals(project)
end

def possible_user_values_options(obj)
mapped_with_deduced_project(obj) do |project|
scope = if project&.persisted?
project.principals
else
Principal
.in_visible_project_or_me(User.current)
end

user_format_columns = User::USER_FORMATS_STRUCTURE[Setting.user_format].map(&:to_s)
# Always include lastname if not already included, as Groups always need a lastname (alias for name)
user_format_columns << "lastname" unless user_format_columns.include?("lastname")

scope.select(*user_format_columns, "id", "type")
end
possible_users(obj).select(*user_format_columns, "id", "type")
.sort
.map { |u| [u.name, u.id.to_s] }
end

def possible_list_values_options
Expand All @@ -353,18 +352,36 @@ def possible_values_from_arg(arg)
end
end

def mapped_with_deduced_project(project)
project = if project.is_a?(Project)
project
elsif project.respond_to?(:project)
project.project
end
def deduce_project(project)
if project.is_a?(Project)
project
elsif project.respond_to?(:project)
project.project
end
end

def deduce_principals(project)
if project&.persisted?
project.principals
else
Principal
.in_visible_project_or_me(User.current)
end
end

result = yield project
def deduce_versions(project)
if project&.persisted?
project.shared_versions
else
Version.systemwide
end
end

result
.sort
.map { |u| [u.name, u.id.to_s] }
def user_format_columns
user_format_columns = User::USER_FORMATS_STRUCTURE[Setting.user_format].map(&:to_s)
# Always include lastname if not already included, as Groups always need a lastname (alias for name)
user_format_columns << "lastname" unless user_format_columns.include?("lastname")
user_format_columns
end

def destroy_help_text
Expand Down
4 changes: 3 additions & 1 deletion app/models/projects/versions.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
Expand Down Expand Up @@ -51,7 +53,7 @@ def shared_versions
Version.shared_with(self)
end

# Returns all versions a work package can be assigned to. Opposed to
# Returns all versions a work package can be assigned to. Opposed to
# #shared_versions this returns an array of Versions, not a scope.
#
# The main benefit is in scenarios where work packages' projects are eager
Expand Down
Loading
Loading