From ffcd4cdfbc5a1f8bcd770f1cdec2ad5ec555e77a Mon Sep 17 00:00:00 2001 From: ulferts Date: Thu, 11 Dec 2025 16:30:48 +0100 Subject: [PATCH] consolidate lazy loading and filter components --- .../filter/filter_component.html.erb | 257 ++++++++++-------- app/components/filter/filter_component.rb | 24 ++ .../index_sub_header_component.html.erb | 25 +- .../portfolios/index_sub_header_component.rb | 11 - .../index_sub_header_component.html.erb | 25 +- .../projects/index_sub_header_component.rb | 11 - .../projects_filters_component.html.erb | 111 -------- .../projects/projects_filters_component.rb | 6 - app/views/portfolios/filters/show.html.erb | 6 +- app/views/projects/filters/show.html.erb | 6 +- .../index_sub_header_component.html.erb | 135 ++++----- 11 files changed, 259 insertions(+), 358 deletions(-) delete mode 100644 app/components/projects/projects_filters_component.html.erb 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 %> -
- - <%= t(:label_filter_plural) %> - -
+ <% 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"> - <%= t(:label_filter_plural) %> - -
-<% 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 +%>