diff --git a/app/components/filter/filter_component.html.erb b/app/components/filter/filter_component.html.erb
index 60c8fa830f8..7a76975b058 100644
--- a/app/components/filter/filter_component.html.erb
+++ b/app/components/filter/filter_component.html.erb
@@ -1,113 +1,152 @@
-<%= form_tag({},
- method: :get,
- data: {
- action: 'submit->filter--filters-form#sendForm:prevent'
- }) do %>
-
+ <% end %>
+ <% end %>
<% end %>
diff --git a/app/components/filter/filter_component.rb b/app/components/filter/filter_component.rb
index fc9f6e1faf3..8591710d472 100644
--- a/app/components/filter/filter_component.rb
+++ b/app/components/filter/filter_component.rb
@@ -31,9 +31,12 @@ module Filter
# rubocop:disable OpenProject/AddPreviewForViewComponent
class FilterComponent < ApplicationComponent
OPERATORS_WITHOUT_VALUES = %w[* !* t w].freeze
+ TURBO_FRAME_ID = "filter_component"
# rubocop:enable OpenProject/AddPreviewForViewComponent
options :query
+ options :lazy_loaded_path
+ options :initially_expanded
# Returns filters, active and inactive.
# In case a filter is active, the active one will be preferred over the inactive one.
@@ -58,6 +61,27 @@ module Filter
OPERATORS_WITHOUT_VALUES.include?(operator)
end
+ def lazy_loaded? = !!lazy_loaded_path
+
+ def initially_expanded? = initially_expanded || false
+
+ def turbo_requests? = false
+
+ def skeleton_height
+ # This is an approximation.
+ # * 100 for the padding and the filter selection
+ # * 40 per filter and their bottom margin. But the height of the filters vary unfortunately.
+ "#{100 + (query.filters.count * 40)}px"
+ end
+
+ def filter_classes
+ "op-filters-form op-filters-form_top-margin #{'-expanded' if initially_expanded?}"
+ end
+
+ def lazy_turbo_frame_src
+ public_send(lazy_loaded_path, **params.permit(:filters, :columns, :sortBy, :id, :query_id))
+ end
+
protected
# With this method we can pass additional options for each type of filter into the frontend. This is especially
diff --git a/app/components/portfolios/index_sub_header_component.html.erb b/app/components/portfolios/index_sub_header_component.html.erb
index 33bf648768b..151e7a2ef26 100644
--- a/app/components/portfolios/index_sub_header_component.html.erb
+++ b/app/components/portfolios/index_sub_header_component.html.erb
@@ -33,25 +33,14 @@
end
end
- subheader.with_bottom_pane_component classes: filter_classes,
- data: {
- "filter--filters-form-target": "filterForm"
- } do
- helpers.turbo_frame_tag(
- "portfolios_filters",
- src: portfolios_filters_path(
- **params.permit(:filters, :columns, :sortBy, :id, :query_id)
- ),
- target: "_top",
- loading: "lazy",
- data: { turbo: false }
- ) do
- render Primer::Alpha::SkeletonBox.new(
- width: "100%",
- height: skeleton_height,
- mt: 3
+ subheader.with_bottom_pane_component do
+ render(
+ Projects::ProjectsFiltersComponent.new(
+ query: @query,
+ lazy_loaded_path: :portfolios_filters_path,
+ initially_expanded: filters_expanded?
)
- end
+ )
end
end
end
diff --git a/app/components/portfolios/index_sub_header_component.rb b/app/components/portfolios/index_sub_header_component.rb
index 47d8187e260..cc5eb426281 100644
--- a/app/components/portfolios/index_sub_header_component.rb
+++ b/app/components/portfolios/index_sub_header_component.rb
@@ -74,19 +74,8 @@ module Portfolios
@current_user.allowed_globally?(:add_portfolios)
end
- def skeleton_height
- # This is an approximation.
- # * 100 for the padding and the filter selection
- # * 40 per filter and their bottom margin. But the height of the filters vary unfortunately.
- "#{100 + (@query.filters.count * 40)}px"
- end
-
def filters_expanded?
params[:filters].present?
end
-
- def filter_classes
- "op-filters-form op-filters-form_top-margin #{'-expanded' if filters_expanded?}"
- end
end
end
diff --git a/app/components/projects/index_sub_header_component.html.erb b/app/components/projects/index_sub_header_component.html.erb
index 4333467cbcc..cf8e4a5369e 100644
--- a/app/components/projects/index_sub_header_component.html.erb
+++ b/app/components/projects/index_sub_header_component.html.erb
@@ -59,25 +59,14 @@
end
end
- subheader.with_bottom_pane_component classes: filter_classes,
- data: {
- "filter--filters-form-target": "filterForm"
- } do
- helpers.turbo_frame_tag(
- "projects_filters",
- src: projects_filters_path(
- **params.permit(:filters, :columns, :sortBy, :id, :query_id)
- ),
- target: "_top",
- loading: "lazy",
- data: { turbo: false }
- ) do
- render Primer::Alpha::SkeletonBox.new(
- width: "100%",
- height: skeleton_height,
- mt: 3
+ subheader.with_bottom_pane_component do
+ render(
+ Projects::ProjectsFiltersComponent.new(
+ query: @query,
+ lazy_loaded_path: :projects_filters_path,
+ initially_expanded: filters_expanded?
)
- end
+ )
end
end
end
diff --git a/app/components/projects/index_sub_header_component.rb b/app/components/projects/index_sub_header_component.rb
index 73c96f6a67a..e231720bed3 100644
--- a/app/components/projects/index_sub_header_component.rb
+++ b/app/components/projects/index_sub_header_component.rb
@@ -110,19 +110,8 @@ module Projects
true
end
- def skeleton_height
- # This is an approximation.
- # * 100 for the padding and the filter selection
- # * 40 per filter and their bottom margin. But the height of the filters vary unfortunately.
- "#{100 + (@query.filters.count * 40)}px"
- end
-
def filters_expanded?
params[:filters].present?
end
-
- def filter_classes
- "op-filters-form op-filters-form_top-margin #{'-expanded' if filters_expanded?}"
- end
end
end
diff --git a/app/components/projects/projects_filters_component.html.erb b/app/components/projects/projects_filters_component.html.erb
deleted file mode 100644
index 0d8b5b65095..00000000000
--- a/app/components/projects/projects_filters_component.html.erb
+++ /dev/null
@@ -1,111 +0,0 @@
-<%= form_tag(
- {},
- method: :get,
- data: {
- action: "submit->filter--filters-form#sendForm:prevent"
- }
- ) do %>
- <% operators_without_values = %w[* !* t w] %>
-
- "
- href=""
- class="advanced-filters--close icon-context icon-close"
- data-action="filter--filters-form#toggleDisplayFilters">
-
-
- <% each_filter do |filter, filter_active, additional_options| %>
- <% filter_boolean = filter.is_a?(Queries::Filters::Shared::BooleanFilter) %>
- <% autocomplete_filter = additional_options.key?(:autocomplete_options) %>
- - "
- data-filter-name="<%= filter.name %>"
- data-filter-type="<%= filter.type %>"
- data-filter--filters-form-target="filter">
-
- <% 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 %>
- <% value_visibility = operators_without_values.include?(selected_operator) ? "hidden" : "" %>
- <% if autocomplete_filter %>
- <%= render partial: "filters/autocomplete",
- locals: { value_visibility: value_visibility,
- filter: filter,
- autocomplete_options: additional_options[:autocomplete_options] } %>
- <% elsif filter_boolean %>
- <%= render partial: "filters/boolean",
- locals: { value_visibility: value_visibility,
- filter: filter } %>
- <% elsif %i(list list_optional list_all).include? filter.type %>
- <%= render partial: "filters/list/input_options",
- locals: { value_visibility: value_visibility,
- filter: filter } %>
- <% elsif [:datetime_past, :date].include? filter.type %>
- <%= render partial: "filters/date/input_options",
- locals: { value_visibility: value_visibility,
- filter: filter,
- selected_operator: selected_operator } %>
- <% else %>
- <%# All other simple types %>
- <%= render partial: "filters/text",
- locals: { value_visibility: value_visibility,
- filter: filter } %>
- <% end %>
-
-
- <% end %>
- - "
- data-filter--filters-form-target="spacer">
- -
-
-
-
-
- <%= 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"
- } %>
-
-
-
-
-<% end %>
diff --git a/app/components/projects/projects_filters_component.rb b/app/components/projects/projects_filters_component.rb
index 1f24f0baac6..8cd9e4c88cb 100644
--- a/app/components/projects/projects_filters_component.rb
+++ b/app/components/projects/projects_filters_component.rb
@@ -28,19 +28,13 @@
# See COPYRIGHT and LICENSE files for more details.
# ++
-# rubocop:disable OpenProject/AddPreviewForViewComponent
class Projects::ProjectsFiltersComponent < Filter::FilterComponent
- # rubocop:enable OpenProject/AddPreviewForViewComponent
def allowed_filters
super
.select { |f| allowed_filter?(f) }
.sort_by(&:human_name)
end
- def turbo_requests?
- true
- end
-
private
def allowed_filter?(filter)
diff --git a/app/views/portfolios/filters/show.html.erb b/app/views/portfolios/filters/show.html.erb
index 7b5fd3f5196..9160a58357c 100644
--- a/app/views/portfolios/filters/show.html.erb
+++ b/app/views/portfolios/filters/show.html.erb
@@ -1,5 +1 @@
-<%=
- turbo_frame_tag "portfolios_filters" do
- render(Projects::ProjectsFiltersComponent.new(query: @query))
- end
-%>
+<%= render(Projects::ProjectsFiltersComponent.new(query: @query)) %>
diff --git a/app/views/projects/filters/show.html.erb b/app/views/projects/filters/show.html.erb
index 1043e6fd6e5..9160a58357c 100644
--- a/app/views/projects/filters/show.html.erb
+++ b/app/views/projects/filters/show.html.erb
@@ -1,5 +1 @@
-<%=
- turbo_frame_tag "projects_filters" do
- render(Projects::ProjectsFiltersComponent.new(query: @query))
- end
-%>
+<%= render(Projects::ProjectsFiltersComponent.new(query: @query)) %>
diff --git a/modules/meeting/app/components/meetings/index_sub_header_component.html.erb b/modules/meeting/app/components/meetings/index_sub_header_component.html.erb
index 35b7699e836..0e780665bcf 100644
--- a/modules/meeting/app/components/meetings/index_sub_header_component.html.erb
+++ b/modules/meeting/app/components/meetings/index_sub_header_component.html.erb
@@ -1,71 +1,78 @@
-<%= render(
- Primer::OpenProject::SubHeader.new(
- data: {
- controller: "filter--filters-form",
- "filter--filters-form-output-format-value": "json",
- "filter--filters-form-display-filters-value": filters_expanded?
- }
+<%=
+ render(
+ Primer::OpenProject::SubHeader.new(
+ data: {
+ controller: "filter--filters-form",
+ "filter--filters-form-output-format-value": "json",
+ "filter--filters-form-display-filters-value": filters_expanded?
+ }
+ )
+ ) do |subheader|
+ subheader.with_filter_component do
+ render(Meetings::MeetingFilterButtonComponent.new(query: @query, project: @project))
+ end
+
+ subheader.with_segmented_control("aria-label": I18n.t(:label_meeting_date_time)) do |control|
+ control.with_item(
+ tag: :a,
+ icon: :"arrow-right",
+ href: dynamic_path,
+ label: t(:label_upcoming_meetings_short),
+ title: t(:label_upcoming_meetings),
+ selected: upcoming_query?
)
- ) do |subheader|
- subheader.with_filter_component do
- render(Meetings::MeetingFilterButtonComponent.new(query: @query, project: @project))
- end
+ control.with_item(
+ tag: :a,
+ icon: :history,
+ href: dynamic_path(upcoming: false),
+ label: t(:label_past_meetings_short),
+ title: t(:label_past_meetings),
+ selected: !upcoming_query?
+ )
+ end
- subheader.with_segmented_control("aria-label": I18n.t(:label_meeting_date_time)) do |control|
- control.with_item(
+ if render_create_button?
+ subheader.with_action_menu(
+ leading_icon: :plus,
+ trailing_icon: :"triangle-down",
+ label: label_text,
+ anchor_align: :end,
+ size: :small,
+ button_arguments: {
+ scheme: :primary,
+ id: id,
+ aria: { label: accessibility_label_text },
+ test_selector: "add-meeting-button"
+ }
+ ) do |menu|
+ menu.with_item(
+ label: I18n.t("meeting.types.one_time"),
tag: :a,
- icon: :"arrow-right",
- href: dynamic_path,
- label: t(:label_upcoming_meetings_short),
- title: t(:label_upcoming_meetings),
- selected: upcoming_query?
- )
- control.with_item(
+ href: polymorphic_path([:new_dialog, @project, :meetings]),
+ content_arguments: { data: { controller: "async-dialog" } }
+ ) do |item|
+ item.with_description.with_content(t("meeting.types.structured_text"))
+ end
+
+ menu.with_item(
+ label: I18n.t("meeting.types.recurring"),
tag: :a,
- icon: :history,
- href: dynamic_path(upcoming: false),
- label: t(:label_past_meetings_short),
- title: t(:label_past_meetings),
- selected: !upcoming_query?
- )
- end
-
- if render_create_button?
- subheader.with_action_menu(leading_icon: :plus,
- trailing_icon: :"triangle-down",
- label: label_text,
- anchor_align: :end,
- size: :small,
- button_arguments: { scheme: :primary,
- id: id,
- aria: { label: accessibility_label_text },
- test_selector: "add-meeting-button" }) do |menu|
- menu.with_item(
- label: I18n.t("meeting.types.one_time"),
- tag: :a,
- href: polymorphic_path([:new_dialog, @project, :meetings]),
- content_arguments: { data: { controller: "async-dialog" } }
- ) do |item|
- item.with_description.with_content(t("meeting.types.structured_text"))
- end
-
- menu.with_item(
- label: I18n.t("meeting.types.recurring"),
- tag: :a,
- href: polymorphic_path([:new_dialog, @project, :meetings], type: :recurring),
- content_arguments: { data: { controller: "async-dialog" } }
- ) do |item|
- item.with_description.with_content(t("meeting.types.recurring_text"))
- end
+ href: polymorphic_path([:new_dialog, @project, :meetings], type: :recurring),
+ content_arguments: { data: { controller: "async-dialog" } }
+ ) do |item|
+ item.with_description.with_content(t("meeting.types.recurring_text"))
end
end
+ end
- subheader.with_bottom_pane_component(
- classes: "op-filters-form op-filters-form_top-margin #{"-expanded" if filters_expanded?}",
- data: {
- 'filter--filters-form-target': 'filterForm',
- }
- ) do
- render(Meetings::MeetingFiltersComponent.new(query: @query, project: @project))
- end
- end %>
+ subheader.with_bottom_pane_component do
+ render(
+ Meetings::MeetingFiltersComponent.new(
+ query: @query,
+ project: @project,
+ initially_expanded: filters_expanded?
+ )
+ )
+ end
+ end
+%>