mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Turn partials into form components. wp/74380
This commit is contained in:
@@ -26,134 +26,29 @@
|
||||
<% else %>
|
||||
|
||||
<%= helpers.turbo_frame_tag TURBO_FRAME_ID do %>
|
||||
<%= form_tag(
|
||||
{},
|
||||
<%= primer_form_with(
|
||||
url: {},
|
||||
method: :get,
|
||||
data: {
|
||||
action: "submit->filter--filters-form#sendForm:prevent"
|
||||
}
|
||||
) do %>
|
||||
<fieldset class="advanced-filters--container">
|
||||
<%=
|
||||
render(
|
||||
Primer::Beta::IconButton.new(
|
||||
icon: :x,
|
||||
scheme: :invisible,
|
||||
classes: "advanced-filters--close",
|
||||
tooltip_direction: :se,
|
||||
aria: { label: t("js.close_form_title") },
|
||||
data: { action: "filter--filters-form#toggleDisplayFilters" }
|
||||
)
|
||||
) do |f| %>
|
||||
<%=
|
||||
render(
|
||||
Primer::Beta::IconButton.new(
|
||||
icon: :x,
|
||||
scheme: :invisible,
|
||||
classes: "advanced-filters--close",
|
||||
tooltip_direction: :se,
|
||||
aria: { label: t("js.close_form_title") },
|
||||
data: { action: "filter--filters-form#toggleDisplayFilters" }
|
||||
)
|
||||
%>
|
||||
<legend><%= t(:label_filter_plural) %></legend>
|
||||
<ul class="advanced-filters--filters">
|
||||
<% each_filter do |filter, filter_active, additional_options| %>
|
||||
<% filter_boolean = filter.is_a?(Queries::Filters::Shared::BooleanFilter) %>
|
||||
<% autocomplete_filter = additional_options.key?(:autocomplete_options) %>
|
||||
|
||||
<li class="advanced-filters--filter <%= filter_active ? "" : "hidden" %>"
|
||||
data-filter-name="<%= filter.name %>"
|
||||
data-filter-type="<%= filter.type %>"
|
||||
data-filter--filters-form-target="filter">
|
||||
<label class="advanced-filters--filter-name" for="<%= filter.name %>">
|
||||
<%= filter.human_name %>
|
||||
</label>
|
||||
<% selected_operator = filter.operator || filter.default_operator.symbol %>
|
||||
<%= content_tag :div, class: "advanced-filters--filter-operator", style: filter_boolean ? "display:none" : "" do %>
|
||||
<%= select_tag :operator,
|
||||
options_from_collection_for_select(
|
||||
filter.available_operators,
|
||||
:symbol,
|
||||
:human_name,
|
||||
selected_operator
|
||||
),
|
||||
class: "advanced-filters--select",
|
||||
data: {
|
||||
action: "change->filter--filters-form#setValueVisibility",
|
||||
"filter--filters-form-filter-name-param": filter.name,
|
||||
"filter--filters-form-target": "operator",
|
||||
"filter-name": filter.name
|
||||
} %>
|
||||
<% end %>
|
||||
<% if autocomplete_filter %>
|
||||
<%= render partial: "filters/autocomplete",
|
||||
locals: { value_visibility: value_hidden_class(selected_operator),
|
||||
filter: filter,
|
||||
autocomplete_options: additional_options[:autocomplete_options] } %>
|
||||
<% elsif filter_boolean %>
|
||||
<%= render partial: "filters/boolean",
|
||||
locals: { value_visibility: value_hidden_class(selected_operator),
|
||||
filter: filter } %>
|
||||
<% elsif %i(list list_optional list_all).include? filter.type %>
|
||||
<%= render partial: "filters/list/input_options",
|
||||
locals: { value_visibility: value_hidden_class(selected_operator),
|
||||
filter: filter } %>
|
||||
<% elsif [:datetime_past, :date].include? filter.type %>
|
||||
<%= render partial: "filters/date/input_options",
|
||||
locals: { value_visibility: value_hidden_class(selected_operator),
|
||||
filter: filter,
|
||||
selected_operator: selected_operator } %>
|
||||
<% else %>
|
||||
<%# All other simple types %>
|
||||
<%= render partial: "filters/text",
|
||||
locals: { value_visibility: value_hidden_class(selected_operator),
|
||||
filter: filter } %>
|
||||
<% end %>
|
||||
|
||||
<div class="advanced-filters--remove-filter">
|
||||
<a href=""
|
||||
class="filter_rem"
|
||||
data-action="click->filter--filters-form#removeFilter"
|
||||
data-filter--filters-form-filter-name-param="<%= filter.name %>">
|
||||
<%= helpers.op_icon("icon-close advanced-filters--remove-filter-icon", title: I18n.t("js.button_delete")) %>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
|
||||
<li class="advanced-filters--spacer <%= query.filters.blank? ? "hidden" : "" %>"
|
||||
data-filter--filters-form-target="spacer"></li>
|
||||
|
||||
<li class="advanced-filters--add-filter">
|
||||
<!-- Add filters -->
|
||||
<label for="add_filter_select" aria-hidden="true" class="advanced-filters--add-filter-label ng-binding">
|
||||
<%= helpers.op_icon("icon-add icon4") %>
|
||||
<%= t(:label_filter_add) %>:
|
||||
</label>
|
||||
<label for="add_filter_select" class="sr-only ng-binding">
|
||||
<%= t(:label_filter_add) %>
|
||||
<%= t("js.filter.description.text_open_filter") %>
|
||||
<%= t("js.filter.description.text_close_filter") %>
|
||||
</label>
|
||||
|
||||
<div class="advanced-filters--add-filter-value">
|
||||
<%= select_tag "add_filter_select",
|
||||
options_from_collection_for_select(
|
||||
allowed_filters,
|
||||
:name,
|
||||
:human_name,
|
||||
disabled: query.filters.map(&:name)
|
||||
),
|
||||
prompt: t(:actionview_instancetag_blank_option),
|
||||
class: "advanced-filters--select",
|
||||
focus: "false",
|
||||
"aria-invalid": "false",
|
||||
data: {
|
||||
"filter--filters-form-target": "addFilterSelect",
|
||||
action: "change->filter--filters-form#addFilter:prevent"
|
||||
} %>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<% unless turbo_requests? %>
|
||||
<li class="advanced-filters--controls">
|
||||
<%= submit_tag t("button_apply"), class: "button -small -primary", name: nil %>
|
||||
</li>
|
||||
<% end %>
|
||||
|
||||
</ul>
|
||||
</fieldset>
|
||||
)
|
||||
%>
|
||||
<%= render(Primer::Forms::FormList.new(*inputs(f))) %>
|
||||
<% unless turbo_requests? %>
|
||||
<%= render(Primer::Beta::Button.new(type: :submit, scheme: :primary)) { t("button_apply") } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
# ++
|
||||
module Filter
|
||||
class FilterComponent < ApplicationComponent
|
||||
OPERATORS_WITHOUT_VALUES = %w[* !* t w].freeze
|
||||
TURBO_FRAME_ID = "filter_component"
|
||||
|
||||
options :query
|
||||
@@ -38,10 +37,22 @@ module Filter
|
||||
options lazy_loaded_path: false
|
||||
options initially_expanded: false
|
||||
|
||||
# Returns filters, active and inactive.
|
||||
def inputs(form)
|
||||
filter_forms = map_filter do |filter, active, additional_attributes|
|
||||
filter_form_class(filter, additional_attributes).new(form, filter:, additional_attributes:, active:)
|
||||
end
|
||||
|
||||
filter_forms << Filters::Inputs::AddFilterForm.new(
|
||||
form,
|
||||
allowed_filters:,
|
||||
active_filter_names: query.filters.map(&:name)
|
||||
)
|
||||
end
|
||||
|
||||
# Maps over all filters (active and inactive).
|
||||
# In case a filter is active, the active one will be preferred over the inactive one.
|
||||
def each_filter
|
||||
allowed_filters.each do |allowed_filter|
|
||||
def map_filter
|
||||
allowed_filters.map do |allowed_filter|
|
||||
active_filter = query.find_active_filter(allowed_filter.name)
|
||||
filter = active_filter || allowed_filter
|
||||
|
||||
@@ -53,14 +64,6 @@ module Filter
|
||||
query.available_advanced_filters
|
||||
end
|
||||
|
||||
def value_hidden_class(selected_operator)
|
||||
operator_without_value?(selected_operator) ? "hidden" : ""
|
||||
end
|
||||
|
||||
def operator_without_value?(operator)
|
||||
OPERATORS_WITHOUT_VALUES.include?(operator)
|
||||
end
|
||||
|
||||
def lazy_loaded? = !!lazy_loaded_path
|
||||
|
||||
def initially_expanded? = initially_expanded
|
||||
@@ -90,7 +93,7 @@ module Filter
|
||||
# When the method is overwritten in a subclass, the subclass should call super(filter) to get the default attributes.
|
||||
#
|
||||
# @param filter [QueryFilter] the filter for which we want to pass additional attributes
|
||||
# @return [Hash] the additional attributes for the filter, that will be yielded in the each_filter method
|
||||
# @return [Hash] the additional attributes for the filter, yielded in map_filter
|
||||
def additional_filter_attributes(filter)
|
||||
case filter
|
||||
when Queries::Filters::Shared::ProjectFilter::Required,
|
||||
@@ -110,18 +113,36 @@ module Filter
|
||||
end
|
||||
end
|
||||
|
||||
def filter_form_class(filter, additional_attributes)
|
||||
if filter.is_a?(Queries::Filters::Shared::BooleanFilter)
|
||||
Filters::Inputs::BooleanForm
|
||||
elsif additional_attributes.key?(:autocomplete_options)
|
||||
Filters::Inputs::AutocompleteForm
|
||||
elsif filter.type.in? %i[list list_optional list_all]
|
||||
Filters::Inputs::ListForm
|
||||
elsif filter.type.in? %i[datetime_past date]
|
||||
Filters::Inputs::DateForm
|
||||
else
|
||||
Filters::Inputs::TextForm
|
||||
end
|
||||
end
|
||||
|
||||
def custom_field_list_autocomplete_options(filter)
|
||||
all_items = if filter.custom_field.version?
|
||||
filter.allowed_values.map { |name, id, project_name| { name:, id:, project_name: } }
|
||||
else
|
||||
filter.allowed_values.map { |name, id| { name:, id: } }
|
||||
end
|
||||
all_items = custom_field_allowed_items(filter)
|
||||
selected = filter.values
|
||||
options = { items: all_items }
|
||||
options[:groupBy] = "project_name" if filter.custom_field.version?
|
||||
autocomplete_options.merge(options).merge(model: all_items.select { |item| selected.include?(item[:id]) })
|
||||
end
|
||||
|
||||
def custom_field_allowed_items(filter)
|
||||
if filter.custom_field.version?
|
||||
filter.allowed_values.map { |name, id, project_name| { name:, id:, project_name: } }
|
||||
else
|
||||
filter.allowed_values.map { |name, id| { name:, id: } }
|
||||
end
|
||||
end
|
||||
|
||||
def custom_field_hierarchy_autocomplete_options(filter)
|
||||
items = filter.allowed_values.map do |name, id|
|
||||
path = name.split(" / ")
|
||||
|
||||
@@ -4,22 +4,6 @@
|
||||
&.-expanded
|
||||
display: block
|
||||
.advanced-filters--filter-value
|
||||
// visibility based on operator type
|
||||
&.hidden
|
||||
visibility: hidden
|
||||
height: 55px
|
||||
|
||||
// visibility for list value selectors
|
||||
.multi-select
|
||||
display: none
|
||||
.single-select
|
||||
display: block
|
||||
&.multi-value
|
||||
.multi-select
|
||||
display: block
|
||||
.single-select
|
||||
display: none
|
||||
|
||||
// visibility for datetime_past value selectors
|
||||
&.between-dates
|
||||
>.on-date,
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# 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.
|
||||
#++
|
||||
|
||||
class Filters::Inputs::AddFilterForm < ApplicationForm
|
||||
def initialize(allowed_filters:, active_filter_names:)
|
||||
super()
|
||||
@allowed_filters = allowed_filters
|
||||
@active_filter_names = active_filter_names
|
||||
end
|
||||
|
||||
form do |form|
|
||||
form.select_list(
|
||||
name: :add_filter_select,
|
||||
label: I18n.t(:label_filter_add),
|
||||
scope_name_to_model: false,
|
||||
prompt: I18n.t(:actionview_instancetag_blank_option),
|
||||
data: {
|
||||
"filter--filters-form-target": "addFilterSelect",
|
||||
action: "change->filter--filters-form#addFilter:prevent"
|
||||
}
|
||||
) do |select|
|
||||
@allowed_filters.each do |filter|
|
||||
select.option(
|
||||
label: filter.human_name,
|
||||
value: filter.name,
|
||||
disabled: @active_filter_names.include?(filter.name)
|
||||
)
|
||||
end
|
||||
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.
|
||||
#++
|
||||
|
||||
class Filters::Inputs::AutocompleteForm < Filters::Inputs::BaseFilterForm
|
||||
def add_operand(group)
|
||||
group.autocompleter(
|
||||
name: "v-#{@filter.class.key}",
|
||||
label: @filter.human_name,
|
||||
visually_hide_label: true,
|
||||
required: true,
|
||||
include_blank: false,
|
||||
wrapper_data_attributes: {
|
||||
"filter--filters-form-target": "filterValueContainer",
|
||||
"filter-name": @filter.name,
|
||||
"filter-autocomplete": "true"
|
||||
},
|
||||
autocomplete_options: @additional_attributes[:autocomplete_options].merge(
|
||||
multiple: true,
|
||||
multipleAsSeparateInputs: false,
|
||||
inputName: "value",
|
||||
inputValue: @filter.values
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,110 @@
|
||||
# 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.
|
||||
#++
|
||||
|
||||
class Filters::Inputs::BaseFilterForm < ApplicationForm
|
||||
def initialize(filter:, additional_attributes:, active:)
|
||||
super()
|
||||
@filter = filter
|
||||
@additional_attributes = additional_attributes
|
||||
@active = active
|
||||
end
|
||||
|
||||
def self.inherited(subclass)
|
||||
super
|
||||
subclass.form do |form|
|
||||
form.group(**filter_row_arguments) do |group|
|
||||
add_operator(group)
|
||||
add_operand(group)
|
||||
add_delete_button(group)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def add_operand(_group)
|
||||
raise SubclassResponsibilityError
|
||||
end
|
||||
|
||||
def filter_row_arguments
|
||||
args = {
|
||||
layout: :horizontal,
|
||||
classes: "advanced-filters--filter",
|
||||
data: {
|
||||
"filter--filters-form-target": "filter",
|
||||
"filter-name": @filter.name,
|
||||
"filter-type": @filter.type
|
||||
}
|
||||
}
|
||||
args[:hidden] = "hidden" unless @active
|
||||
args
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_operator(group)
|
||||
selected_operator = @filter.operator || @filter.default_operator.symbol
|
||||
|
||||
group.select_list(
|
||||
name: :"operator_#{@filter.name}",
|
||||
label: @filter.human_name,
|
||||
scope_name_to_model: false,
|
||||
hidden: @filter.is_a?(Queries::Filters::Shared::BooleanFilter),
|
||||
data: {
|
||||
action: "change->filter--filters-form#setValueVisibility",
|
||||
"filter--filters-form-filter-name-param": @filter.name,
|
||||
"filter--filters-form-target": "operator",
|
||||
"filter-name": @filter.name
|
||||
}
|
||||
) do |select|
|
||||
@filter.available_operators.each do |op|
|
||||
select.option(
|
||||
label: op.human_name,
|
||||
value: op.symbol,
|
||||
selected: op.symbol == selected_operator
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def add_delete_button(group)
|
||||
group.button(
|
||||
name: :remove_filter,
|
||||
label: I18n.t("button_delete"),
|
||||
scheme: :danger,
|
||||
data: {
|
||||
action: "click->filter--filters-form#removeFilter",
|
||||
"filter--filters-form-filter-name-param": @filter.name
|
||||
}
|
||||
) do |button|
|
||||
button.with_leading_visual_icon(icon: :trash)
|
||||
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.
|
||||
#++
|
||||
|
||||
class Filters::Inputs::BooleanForm < Filters::Inputs::BaseFilterForm
|
||||
def add_operand(group)
|
||||
group.check_box(
|
||||
label: @filter.human_name,
|
||||
name: "v-#{@filter.class.key}",
|
||||
value: "t",
|
||||
unchecked_value: "f",
|
||||
checked: @filter.values.first == "t"
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filter_row_arguments
|
||||
super.tap do |args|
|
||||
args[:data][:"filter--filters-form-target"] = "filter filterValueContainer"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,114 @@
|
||||
# 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.
|
||||
#++
|
||||
|
||||
class Filters::Inputs::DateForm < Filters::Inputs::BaseFilterForm
|
||||
DAYS_OPERATORS = %w[>t- <t- t- <t+ >t+ t+].freeze
|
||||
|
||||
def add_operand(group)
|
||||
filter_name = @filter.name
|
||||
filter_values = @filter.values
|
||||
fmt = date_format
|
||||
|
||||
days_value = fmt == "days" ? filter_values.fetch(0, "") : nil
|
||||
on_date_value = fmt == "on-date" ? filter_values.fetch(0, "") : nil
|
||||
from_value = fmt == "between-dates" ? filter_values.fetch(0, "") : nil
|
||||
to_value = fmt == "between-dates" ? filter_values.fetch(1, "") : nil
|
||||
|
||||
group.multi(name: filter_name, label: filter_name, visually_hide_label: true,
|
||||
class: ["advanced-filters--filter-value"],
|
||||
data: {
|
||||
"filter--filters-form-target": "filterValueContainer",
|
||||
"filter-name": filter_name
|
||||
}) do |builder|
|
||||
days_div(builder, filter_name, days_value)
|
||||
on_date_div(builder, filter_name, on_date_value)
|
||||
between_dates_div(builder, filter_name, from_value, to_value)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def date_format
|
||||
op = @filter.operator || @filter.default_operator.symbol
|
||||
@date_format ||= if DAYS_OPERATORS.include?(op)
|
||||
"days"
|
||||
elsif op == "=d"
|
||||
"on-date"
|
||||
elsif op == "<>d"
|
||||
"between-dates"
|
||||
end
|
||||
end
|
||||
|
||||
def days_div(builder, filter_name, value)
|
||||
field_arguments = {
|
||||
id: "#{filter_name}_value",
|
||||
name: :days,
|
||||
label: I18n.t("datetime.units.day.other"),
|
||||
visually_hide_label: true,
|
||||
trailing_visual: { text: { text: I18n.t("datetime.units.day.other") } },
|
||||
scope_name_to_model: false,
|
||||
value:,
|
||||
hidden: value.nil?,
|
||||
data: {
|
||||
"filter--filters-form-target": "days",
|
||||
"filter-name": filter_name
|
||||
}
|
||||
}
|
||||
|
||||
builder.text_field(**field_arguments, type: :number, step: "any", class: "days")
|
||||
end
|
||||
|
||||
def on_date_div(builder, filter_name, value)
|
||||
builder.single_date_picker(
|
||||
name: :singleDay,
|
||||
label: :singleDay,
|
||||
hidden: value.nil?,
|
||||
leading_visual: { icon: :calendar },
|
||||
value:,
|
||||
data: {
|
||||
"filter-name": filter_name
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def between_dates_div(builder, filter_name, from_value, to_value)
|
||||
value = [from_value, to_value].compact.join(" - ").presence
|
||||
builder.range_date_picker(
|
||||
name: :dateRange,
|
||||
label: :dateRange,
|
||||
hidden: value.nil?,
|
||||
leading_visual: { icon: :calendar },
|
||||
value:,
|
||||
data: {
|
||||
"filter-name": filter_name
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,69 @@
|
||||
# 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.
|
||||
#++
|
||||
|
||||
class Filters::Inputs::ListForm < Filters::Inputs::BaseFilterForm
|
||||
def add_operand(group)
|
||||
filter_name = @filter.name
|
||||
filter_values = @filter.values || []
|
||||
allowed_values = @filter.allowed_values
|
||||
multi_value = filter_values.size > 1
|
||||
|
||||
group.multi(name: :value, label: :value, visually_hide_label: true,
|
||||
class: ["advanced-filters--filter-value"],
|
||||
data: {
|
||||
"filter--filters-form-target": "filterValueContainer",
|
||||
"filter-name": filter_name,
|
||||
"multi-value": multi_value.to_s
|
||||
}) do |builder|
|
||||
builder.select_with_toggle(
|
||||
name: :single, label: :single,
|
||||
allowed_values:,
|
||||
selected_values: [filter_values.first].compact,
|
||||
multiple: false, collapse: false, filter_name:,
|
||||
hidden: multi_value,
|
||||
data: {
|
||||
"filter--filters-form-target": "filterValueSelect",
|
||||
"filter-name": filter_name
|
||||
}
|
||||
)
|
||||
builder.select_with_toggle(
|
||||
name: :multi, label: :multi,
|
||||
allowed_values:,
|
||||
selected_values: filter_values,
|
||||
multiple: true, collapse: true, filter_name:,
|
||||
hidden: !multi_value,
|
||||
data: {
|
||||
"filter--filters-form-target": "filterValueSelect",
|
||||
"filter-name": filter_name
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
<%= render(FormControl.new(input: @input)) do %>
|
||||
<%= content_tag(:div, **@field_wrap_arguments) do %>
|
||||
<%= builder.select(@input.name, options,
|
||||
{ selected: @input.selected_values }.merge(@input.select_arguments),
|
||||
**@input.input_arguments) %>
|
||||
<%= render(Primer::Beta::Button.new(scheme: :invisible, type: :button,
|
||||
aria: { label: I18n.t(:label_enable_multi_select) },
|
||||
data: button_data)) do |btn| %>
|
||||
<% btn.with_leading_visual_icon(icon: @input.collapse? ? :dash : :plus) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,40 @@
|
||||
# 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.
|
||||
#++
|
||||
|
||||
class Filters::Inputs::SelectWithToggleComponent < Primer::Forms::Select
|
||||
def button_data
|
||||
{
|
||||
action: "click->filter--filters-form#toggleMultiSelect",
|
||||
"filter--filters-form-filter-name-param": @input.filter_name,
|
||||
"filter-name": @input.filter_name,
|
||||
collapse: @input.collapse?.to_s
|
||||
}
|
||||
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.
|
||||
#++
|
||||
|
||||
class Filters::Inputs::SelectWithToggleInput < Primer::Forms::Dsl::SelectInput
|
||||
attr_reader :selected_values, :filter_name
|
||||
|
||||
def initialize(name:, label:, allowed_values:, selected_values:, collapse:, filter_name:, **system_arguments)
|
||||
@selected_values = Array(selected_values)
|
||||
@collapse = collapse
|
||||
@filter_name = filter_name
|
||||
super(name:, label:, **system_arguments) do |select|
|
||||
allowed_values.each { |opt_label, opt_value| select.option(label: opt_label, value: opt_value) }
|
||||
end
|
||||
end
|
||||
|
||||
def collapse? = @collapse
|
||||
|
||||
def to_component
|
||||
Filters::Inputs::SelectWithToggleComponent.new(input: self)
|
||||
end
|
||||
|
||||
def type
|
||||
:select_with_toggle
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,50 @@
|
||||
# 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.
|
||||
#++
|
||||
|
||||
class Filters::Inputs::TextForm < Filters::Inputs::BaseFilterForm
|
||||
def add_operand(group)
|
||||
field_arguments = {
|
||||
name: :"v-#{@filter.class.key}",
|
||||
label: @filter.human_name,
|
||||
visually_hide_label: true,
|
||||
scope_name_to_model: false,
|
||||
value: @filter.values.first,
|
||||
data: {
|
||||
"filter--filters-form-target": "filterValueContainer simpleValue",
|
||||
"filter-name": @filter.name
|
||||
}
|
||||
}
|
||||
if @filter.type.in? %i[integer float]
|
||||
group.text_field(**field_arguments, type: :number, step: "any")
|
||||
else
|
||||
group.text_field(**field_arguments)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -31,4 +31,5 @@
|
||||
Rails.application.config.to_prepare do
|
||||
Primer::Forms::Dsl::FormObject.include(Primer::OpenProject::Forms::Dsl::InputMethods)
|
||||
Primer::Forms::Dsl::InputGroup.include(Primer::OpenProject::Forms::Dsl::InputMethods)
|
||||
Primer::Forms::Dsl::MultiInput.include(Primer::OpenProject::Forms::Dsl::InputMethods)
|
||||
end
|
||||
|
||||
+2
@@ -4,6 +4,7 @@
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
class="spot-input op-basic-range-datepicker--input"
|
||||
data-filter--filters-form-target="dateRange"
|
||||
data-test-selector="op-basic-range-date-picker"
|
||||
[ngClass]="inputClassNames"
|
||||
[attr.data-value]="value"
|
||||
@@ -50,6 +51,7 @@
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
data-filter--filters-form-target="dateRange"
|
||||
data-test-selector="op-basic-range-date-picker--value"
|
||||
[attr.data-value]="stringValue"
|
||||
[id]="id"
|
||||
|
||||
@@ -136,6 +136,12 @@ $advanced-filters--grid-gap: 10px
|
||||
&.hidden
|
||||
display: none !important
|
||||
|
||||
.op-filters-form .hidden
|
||||
display: none !important
|
||||
|
||||
.op-filters-form .FormControl:has([data-filter--filters-form-target~="filterValueContainer"].hidden)
|
||||
display: none !important
|
||||
|
||||
.work-packages-embedded-view--container .advanced-filters--container
|
||||
margin: 0 0 1rem 0
|
||||
|
||||
|
||||
@@ -161,6 +161,9 @@ select
|
||||
background-position: right !important
|
||||
background-image: var(--select-arrow-bg-color-url) !important
|
||||
|
||||
&[multiple=multiple]
|
||||
height: $height * 2
|
||||
|
||||
&>option
|
||||
background-color: var(--body-background)
|
||||
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
&-input-wrap[invalid='true'] input:not(:focus)
|
||||
border-color: var(--control-borderColor-danger)
|
||||
|
||||
&-spacingWrapper > :empty
|
||||
&-spacingWrapper > :empty,
|
||||
&-spacingWrapper:not(:has(> :not([hidden])))
|
||||
display: none
|
||||
|
||||
.UnderlineNav
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
hideElement,
|
||||
showElement,
|
||||
} from 'core-app/shared/helpers/dom-helpers';
|
||||
import { PrimerMultiInputElement } from '@primer/view-components/app/lib/primer/forms/primer_multi_input';
|
||||
|
||||
interface PrimerTextFieldElement extends HTMLElement {
|
||||
inputElement:HTMLInputElement;
|
||||
@@ -56,12 +57,12 @@ export default class FiltersFormController extends Controller {
|
||||
'simpleFilter',
|
||||
'filter',
|
||||
'addFilterSelect',
|
||||
'spacer',
|
||||
'operator',
|
||||
'filterValueContainer',
|
||||
'filterValueSelect',
|
||||
'days',
|
||||
'singleDay',
|
||||
'dateRange',
|
||||
'simpleValue',
|
||||
];
|
||||
|
||||
@@ -70,12 +71,12 @@ export default class FiltersFormController extends Controller {
|
||||
declare readonly simpleFilterTargets:HTMLElement[];
|
||||
declare readonly filterTargets:HTMLElement[];
|
||||
declare readonly addFilterSelectTarget:HTMLSelectElement;
|
||||
declare readonly spacerTarget:HTMLElement;
|
||||
declare readonly operatorTargets:HTMLSelectElement[];
|
||||
declare readonly filterValueContainerTargets:HTMLElement[];
|
||||
declare readonly filterValueSelectTargets:HTMLSelectElement[];
|
||||
declare readonly daysTargets:HTMLInputElement[];
|
||||
declare readonly singleDayTargets:HTMLInputElement[];
|
||||
declare readonly dateRangeTargets:HTMLInputElement[];
|
||||
declare readonly simpleValueTargets:HTMLInputElement[];
|
||||
|
||||
declare readonly hasFilterFormToggleTarget:boolean;
|
||||
@@ -140,6 +141,13 @@ export default class FiltersFormController extends Controller {
|
||||
|
||||
filterValueContainerTargetConnected(target:HTMLElement) {
|
||||
this.addChangeListener(target);
|
||||
const filterName = target.getAttribute('data-filter-name');
|
||||
if (filterName) {
|
||||
const operator = this.findTargetByName(filterName, this.operatorTargets);
|
||||
if (operator && this.noValueOperators.includes(operator.value)) {
|
||||
target.setAttribute('hidden', '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filterValueSelectTargetConnected(target:HTMLElement) {
|
||||
@@ -152,6 +160,31 @@ export default class FiltersFormController extends Controller {
|
||||
|
||||
singleDayTargetConnected(target:HTMLElement) {
|
||||
this.addChangeListener(target);
|
||||
this.registerAngularPickerWithMultiInput(target, 'opce-basic-single-date-picker');
|
||||
}
|
||||
|
||||
dateRangeTargetConnected(target:HTMLElement) {
|
||||
this.addChangeListener(target);
|
||||
this.registerAngularPickerWithMultiInput(target, 'opce-range-date-picker');
|
||||
}
|
||||
|
||||
// angular_component_tag serialises @Input() bindings as JSON in data-* attributes, so the
|
||||
// Angular date picker wrapper's data-name holds a JSON-encoded value that primer-multi-input
|
||||
// cannot use to identify its child fields. date_picker.html.erb therefore also sets
|
||||
// data-multi-input-name as a plain string with the intended field name.
|
||||
// This method is called when the Stimulus target connects, i.e. after Angular has already
|
||||
// bootstrapped the component and consumed its data-* attributes, so we can safely overwrite
|
||||
// data-name with the plain string from data-multi-input-name without interfering with Angular.
|
||||
// primer-multi-input.activateField() then uses data-name to show/hide the correct picker
|
||||
// when the filter operator changes.
|
||||
private registerAngularPickerWithMultiInput(target:HTMLElement, tagName:string) {
|
||||
const wrapper = target.closest<HTMLElement>(tagName);
|
||||
if (wrapper?.closest('primer-multi-input')) {
|
||||
const multiInputName = wrapper.getAttribute('data-multi-input-name');
|
||||
if (multiInputName) {
|
||||
wrapper.setAttribute('data-name', multiInputName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
simpleValueTargetDisconnected(target:HTMLElement) {
|
||||
@@ -178,6 +211,10 @@ export default class FiltersFormController extends Controller {
|
||||
this.removeChangeListener(target);
|
||||
}
|
||||
|
||||
dateRangeTargetDisconnected(target:HTMLElement) {
|
||||
this.removeChangeListener(target);
|
||||
}
|
||||
|
||||
filterFormTargetConnected() {
|
||||
// Didn't really change, but we need to ensure that the visibility of the target is correct.
|
||||
// This is caused by there first being a skeleton form, which allows the user to already
|
||||
@@ -217,17 +254,22 @@ export default class FiltersFormController extends Controller {
|
||||
|
||||
toggleMultiSelect({ params: { filterName } }:{ params:{ filterName:string } }) {
|
||||
const valueContainer = this.findTargetByName(filterName, this.filterValueContainerTargets);
|
||||
const singleSelect = this.findTargetByName<HTMLSelectElement>(filterName, this.filterValueSelectTargets, (selectField) => !selectField.multiple);
|
||||
const multiSelect = this.findTargetByName<HTMLSelectElement>(filterName, this.filterValueSelectTargets, (selectField) => selectField.multiple);
|
||||
if (valueContainer && singleSelect && multiSelect) {
|
||||
if (valueContainer.classList.contains('multi-value')) {
|
||||
const valueToSelect = this.getValueToSelect(multiSelect);
|
||||
this.setSelectOptions(singleSelect, valueToSelect);
|
||||
const singleSelect = this.findTargetByName<HTMLSelectElement>(filterName, this.filterValueSelectTargets, (s) => !s.multiple);
|
||||
const multiSelect = this.findTargetByName<HTMLSelectElement>(filterName, this.filterValueSelectTargets, (s) => s.multiple);
|
||||
const multiInput = valueContainer?.querySelector<PrimerMultiInputElement>('primer-multi-input');
|
||||
|
||||
if (valueContainer && singleSelect && multiSelect && multiInput) {
|
||||
const isMultiMode = valueContainer.dataset.multiValue === 'true';
|
||||
|
||||
if (isMultiMode) {
|
||||
this.setSelectOptions(singleSelect, this.getValueToSelect(multiSelect));
|
||||
multiInput.activateField('single');
|
||||
} else {
|
||||
const valueToSelect = this.getValueToSelect(singleSelect);
|
||||
this.setSelectOptions(multiSelect, valueToSelect);
|
||||
this.setSelectOptions(multiSelect, this.getValueToSelect(singleSelect));
|
||||
multiInput.activateField('multi');
|
||||
}
|
||||
valueContainer.classList.toggle('multi-value');
|
||||
|
||||
valueContainer.dataset.multiValue = isMultiMode ? 'false' : 'true';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,11 +311,10 @@ export default class FiltersFormController extends Controller {
|
||||
addFilterByName(filterName:string) {
|
||||
const selectedFilter = this.findTargetByName(filterName, this.filterTargets);
|
||||
if (selectedFilter) {
|
||||
selectedFilter.classList.remove('hidden');
|
||||
selectedFilter.removeAttribute('hidden');
|
||||
}
|
||||
this.addFilterSelectTarget.selectedOptions[0].disabled = true;
|
||||
this.addFilterSelectTarget.selectedIndex = 0;
|
||||
this.setSpacerVisibility();
|
||||
|
||||
this.focusFilterValueIfPossible(selectedFilter);
|
||||
|
||||
@@ -313,12 +354,11 @@ export default class FiltersFormController extends Controller {
|
||||
|
||||
removeFilter({ params: { filterName } }:{ params:{ filterName:string } }) {
|
||||
const filterToRemove = this.findTargetByName(filterName, this.filterTargets);
|
||||
filterToRemove?.classList.add('hidden');
|
||||
filterToRemove?.setAttribute('hidden', '');
|
||||
|
||||
const selectOptions = Array.from(this.addFilterSelectTarget.options);
|
||||
const removedFilterOption = selectOptions.find((option) => option.value === filterName);
|
||||
removedFilterOption?.removeAttribute('disabled');
|
||||
this.setSpacerVisibility();
|
||||
|
||||
if (this.performTurboRequestsValue) {
|
||||
this.sendForm();
|
||||
@@ -340,18 +380,6 @@ export default class FiltersFormController extends Controller {
|
||||
inputElement.dispatchEvent(inputEvent);
|
||||
}
|
||||
|
||||
private setSpacerVisibility() {
|
||||
if (this.anyFiltersStillVisible()) {
|
||||
this.spacerTarget.classList.remove('hidden');
|
||||
} else {
|
||||
this.spacerTarget.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
private anyFiltersStillVisible() {
|
||||
return this.filterTargets.some((filter) => !filter.classList.contains('hidden'));
|
||||
}
|
||||
|
||||
private readonly noValueOperators = ['*', '!*', 't', 'w'];
|
||||
private readonly daysOperators = ['>t-', '<t-', 't-', '<t+', '>t+', 't+'];
|
||||
private readonly onDateOperator = '=d';
|
||||
@@ -362,23 +390,18 @@ export default class FiltersFormController extends Controller {
|
||||
const valueContainer = this.findTargetByName(filterName, this.filterValueContainerTargets);
|
||||
if (valueContainer) {
|
||||
if (this.noValueOperators.includes(selectedOperator)) {
|
||||
valueContainer.classList.add('hidden');
|
||||
valueContainer.setAttribute('hidden', '');
|
||||
} else {
|
||||
valueContainer.classList.remove('hidden');
|
||||
valueContainer.removeAttribute('hidden');
|
||||
}
|
||||
|
||||
const multiInput = valueContainer.querySelector<PrimerMultiInputElement>('primer-multi-input');
|
||||
if (this.daysOperators.includes(selectedOperator)) {
|
||||
valueContainer.classList.add('days');
|
||||
valueContainer.classList.remove('on-date');
|
||||
valueContainer.classList.remove('between-dates');
|
||||
multiInput?.activateField('days');
|
||||
} else if (selectedOperator === this.onDateOperator) {
|
||||
valueContainer.classList.add('on-date');
|
||||
valueContainer.classList.remove('days');
|
||||
valueContainer.classList.remove('between-dates');
|
||||
multiInput?.activateField('singleDay');
|
||||
} else if (selectedOperator === this.betweenDatesOperator) {
|
||||
valueContainer.classList.add('between-dates');
|
||||
valueContainer.classList.remove('days');
|
||||
valueContainer.classList.remove('on-date');
|
||||
multiInput?.activateField('dateRange');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -455,7 +478,7 @@ export default class FiltersFormController extends Controller {
|
||||
}
|
||||
|
||||
private parseAdvancedFilters():InternalFilterValue[] {
|
||||
const advancedFilters = this.filterTargets.filter((filter) => !filter.classList.contains('hidden'));
|
||||
const advancedFilters = this.filterTargets.filter((filter) => !filter.hasAttribute('hidden'));
|
||||
const filters:InternalFilterValue[] = [];
|
||||
|
||||
advancedFilters.forEach((filter) => {
|
||||
@@ -533,9 +556,10 @@ export default class FiltersFormController extends Controller {
|
||||
}
|
||||
|
||||
private parseSelectFilterValue(valueContainer:HTMLElement, filterName:string) {
|
||||
const isMultiMode = valueContainer.dataset.multiValue === 'true';
|
||||
let selectFields;
|
||||
|
||||
if (valueContainer.classList.contains('multi-value')) {
|
||||
if (isMultiMode) {
|
||||
selectFields = this.filterValueSelectTargets.filter((selectField) => selectField.multiple && selectField.getAttribute('data-filter-name') === filterName);
|
||||
} else {
|
||||
selectFields = this.filterValueSelectTargets.filter((selectField) => !selectField.multiple && selectField.getAttribute('data-filter-name') === filterName);
|
||||
@@ -550,20 +574,21 @@ export default class FiltersFormController extends Controller {
|
||||
return null;
|
||||
}
|
||||
|
||||
private parseDateFilterValue(valueContainer:HTMLElement, filterName:string) {
|
||||
private parseDateFilterValue(_valueContainer:HTMLElement, filterName:string) {
|
||||
let value;
|
||||
const operator = this.findTargetByName(filterName, this.operatorTargets)?.value;
|
||||
|
||||
if (valueContainer.classList.contains('days')) {
|
||||
if (operator && this.daysOperators.includes(operator)) {
|
||||
const dateValue = this.findTargetByName(filterName, this.daysTargets)?.value;
|
||||
|
||||
value = _.without([dateValue], '');
|
||||
} else if (valueContainer.classList.contains('on-date')) {
|
||||
const dateValue = this.findTargetById(`on-date-value-${filterName}`, this.singleDayTargets)?.value;
|
||||
} else if (operator === this.onDateOperator) {
|
||||
const dateValue = this.findTargetById(filterName, this.singleDayTargets)?.value;
|
||||
|
||||
value = _.without([dateValue], '');
|
||||
} else if (valueContainer.classList.contains('between-dates')) {
|
||||
const fromValue = this.findTargetById(`between-dates-from-value-${filterName}`, this.singleDayTargets)?.value;
|
||||
const toValue = this.findTargetById(`between-dates-to-value-${filterName}`, this.singleDayTargets)?.value;
|
||||
} else if (operator === this.betweenDatesOperator) {
|
||||
const rangeValue = this.findTargetById(filterName, this.dateRangeTargets)?.value;
|
||||
const [fromValue, toValue] = rangeValue?.split(' - ') ?? [];
|
||||
|
||||
value = [fromValue, toValue];
|
||||
}
|
||||
|
||||
@@ -40,6 +40,10 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
<% end %>
|
||||
<%= render Primer::ConditionalWrapper.new(condition: @input.auto_check_src, tag: "auto-check", csrf: auto_check_authenticity_token, src: @input.auto_check_src) do %>
|
||||
<%= angular_component_tag @datepicker_options.fetch(:component),
|
||||
data: {
|
||||
targets: @input.input_arguments.dig(:data, :targets),
|
||||
"multi-input-name": @input.input_arguments.dig(:data, :name)
|
||||
}.compact,
|
||||
inputs: @datepicker_options.merge(
|
||||
id: @datepicker_options.fetch(:id) { builder.field_id(@input.name) },
|
||||
name: @datepicker_options.fetch(:name) { builder.field_name(@input.name) },
|
||||
|
||||
@@ -11,6 +11,10 @@ module Primer
|
||||
super(**decorate_options(**), &)
|
||||
end
|
||||
|
||||
def select_with_toggle(**, &)
|
||||
add_input Filters::Inputs::SelectWithToggleInput.new(builder:, form:, **decorate_options(**), &)
|
||||
end
|
||||
|
||||
def check_box(**, &)
|
||||
super(**decorate_options(**), &)
|
||||
end
|
||||
|
||||
@@ -224,7 +224,7 @@ RSpec.describe "Projects lists columns", :js, with_settings: { login_required?:
|
||||
# Filter component is visible
|
||||
expect(page).to have_select("add_filter_select")
|
||||
# Filter for column is visible and can now be specified by the user
|
||||
expect(page).to have_css(".advanced-filters--filter-name[for='created_at']")
|
||||
expect(page).to have_css(".advanced-filters--filter[data-filter-name='created_at']")
|
||||
|
||||
# The correct filter input field has focus
|
||||
expect(page.has_focus_on?(".advanced-filters--filter-value input#created_at_value")).to be(true)
|
||||
@@ -238,7 +238,7 @@ RSpec.describe "Projects lists columns", :js, with_settings: { login_required?:
|
||||
# Filter component is visible
|
||||
expect(page).to have_select("add_filter_select")
|
||||
# Filter for column is visible. Note that the filter name is different from the column attribute!
|
||||
expect(page).to have_css(".advanced-filters--filter-name[for='project_status_code']")
|
||||
expect(page).to have_css(".advanced-filters--filter[data-filter-name='project_status_code']")
|
||||
end
|
||||
|
||||
it "does not offer to filter if the column has no associated filter" do
|
||||
|
||||
@@ -142,7 +142,7 @@ RSpec.describe "Projects list filters", :js, with_settings: { login_required?: f
|
||||
load_and_open_filters admin
|
||||
|
||||
# value selection defaults to "active"'
|
||||
expect(page).to have_css('li[data-filter-name="active"]')
|
||||
expect(page).to have_css('.advanced-filters--filter[data-filter-name="active"]')
|
||||
|
||||
projects_page.expect_projects_listed(parent_project,
|
||||
child_project,
|
||||
@@ -601,7 +601,7 @@ RSpec.describe "Projects list filters", :js, with_settings: { login_required?: f
|
||||
projects_page.expect_projects_listed(project)
|
||||
|
||||
# switching to multiselect keeps the current selection
|
||||
cf_filter = page.find("li[data-filter-name='#{list_custom_field.column_name}']")
|
||||
cf_filter = page.find(".advanced-filters--filter[data-filter-name='#{list_custom_field.column_name}']")
|
||||
|
||||
select_value_id = "#{list_custom_field.column_name}_value"
|
||||
|
||||
@@ -614,7 +614,7 @@ RSpec.describe "Projects list filters", :js, with_settings: { login_required?: f
|
||||
projects_page.expect_projects_not_listed(development_project)
|
||||
projects_page.expect_projects_listed(project)
|
||||
|
||||
cf_filter = page.find("li[data-filter-name='#{list_custom_field.column_name}']")
|
||||
cf_filter = page.find(".advanced-filters--filter[data-filter-name='#{list_custom_field.column_name}']")
|
||||
within(cf_filter) do
|
||||
# Query has two values for that filter.
|
||||
projects_page.expect_ng_value_label(select_value_id,
|
||||
|
||||
@@ -43,12 +43,12 @@ module Components
|
||||
if filter_name == "name_and_identifier"
|
||||
expect(page.find_by_id(filter_name).value).not_to be_empty
|
||||
elsif value
|
||||
within("li[data-filter-name='#{filter_name}']") do
|
||||
within(".advanced-filters--filter[data-filter-name='#{filter_name}']") do
|
||||
expect(page).to have_css(".advanced-filters--filter-value", text: value, visible: :all)
|
||||
end
|
||||
else
|
||||
expect(page)
|
||||
.to have_css("li[data-filter-name='#{filter_name}']")
|
||||
.to have_css(".advanced-filters--filter[data-filter-name='#{filter_name}']")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -119,29 +119,28 @@ module Components
|
||||
|
||||
def select_filter(name, human_name)
|
||||
select human_name, from: "add_filter_select"
|
||||
page.find("li[data-filter-name='#{name}']")
|
||||
page.find(".advanced-filters--filter[data-filter-name='#{name}']")
|
||||
end
|
||||
|
||||
def remove_filter(name)
|
||||
if name == "name_and_identifier"
|
||||
page.find_by_id("name_and_identifier").find(:xpath, "following-sibling::button").click
|
||||
else
|
||||
page.find("li[data-filter-name='#{name}'] .filter_rem").click
|
||||
page.find(".advanced-filters--filter[data-filter-name='#{name}'] .advanced-filters--remove-filter").click
|
||||
end
|
||||
end
|
||||
|
||||
def set_toggle_filter(values)
|
||||
should_active = values.first == "yes"
|
||||
is_active = page.has_selector? '[data-test-selector="spot-switch-handle"][data-qa-active]'
|
||||
checkbox = page.find('input[type="checkbox"]')
|
||||
is_active = checkbox.checked?
|
||||
|
||||
if should_active != is_active
|
||||
page.find('[data-test-selector="spot-switch-handle"]').click
|
||||
end
|
||||
checkbox.click if should_active != is_active
|
||||
|
||||
if should_active
|
||||
expect(page).to have_css('[data-test-selector="spot-switch-handle"][data-qa-active]')
|
||||
expect(page).to have_field(type: :checkbox, checked: true)
|
||||
else
|
||||
expect(page).to have_css('[data-test-selector="spot-switch-handle"]:not([data-qa-active])')
|
||||
expect(page).to have_field(type: :checkbox, checked: false)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -238,10 +238,12 @@ module Pages
|
||||
end
|
||||
|
||||
def set_advanced_filter(name, human_name, human_operator = nil, values = [], send_keys: false)
|
||||
selected_filter = select_filter(name, human_name)
|
||||
select_filter(name, human_name)
|
||||
|
||||
# Re-find after select_filter, as adding the filter triggers DOM updates
|
||||
# that make the returned reference immediately stale (ObsoleteNode)
|
||||
selected_filter = page.find(".advanced-filters--filter[data-filter-name='#{name}']")
|
||||
|
||||
# Detect filter type before apply_operator, which may trigger Turbo stream
|
||||
# updates that make the selected_filter node reference stale (ObsoleteNode)
|
||||
is_autocomplete = autocomplete_filter?(selected_filter)
|
||||
is_date_or_datetime = date_filter?(selected_filter) || date_time_filter?(selected_filter)
|
||||
|
||||
@@ -251,8 +253,8 @@ module Pages
|
||||
|
||||
return unless values.any?
|
||||
|
||||
# Re-find element as apply_operator may have triggered DOM updates
|
||||
selected_filter = page.find("li[data-filter-name='#{name}']")
|
||||
# Re-find again as apply_operator may have triggered further DOM updates
|
||||
selected_filter = page.find(".advanced-filters--filter[data-filter-name='#{name}']")
|
||||
|
||||
within(selected_filter) do
|
||||
if boolean_filter?(name)
|
||||
|
||||
Reference in New Issue
Block a user