consolidate lazy loading and filter components

This commit is contained in:
ulferts
2025-12-11 16:30:48 +01:00
parent 48fadd73dd
commit ffcd4cdfbc
11 changed files with 259 additions and 358 deletions
+148 -109
View File
@@ -1,113 +1,152 @@
<%= form_tag({},
method: :get,
data: {
action: 'submit->filter--filters-form#sendForm:prevent'
}) do %>
<fieldset class="advanced-filters--container">
<a title="<%= t('js.close_form_title') %>"
class="advanced-filters--close icon-context icon-close"
data-action="filter--filters-form#toggleDisplayFilters"></a>
<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) %>
<%=
helpers.content_tag :div,
class: filter_classes,
data: {
"filter--filters-form-target": "filterForm"
} do
%>
<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 %>
<% if lazy_loaded? %>
<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>
<%=
helpers.turbo_frame_tag(
TURBO_FRAME_ID,
src: lazy_turbo_frame_src,
target: "_top",
loading: "lazy",
data: { turbo: false }
) do
render Primer::Alpha::SkeletonBox.new(
width: "100%",
height: skeleton_height
)
end
%>
<% else %>
<%= helpers.turbo_frame_tag TURBO_FRAME_ID do %>
<%= form_tag(
{},
method: :get,
data: {
action: "submit->filter--filters-form#sendForm:prevent"
}
) do %>
<fieldset class="advanced-filters--container">
<a title="<%= t("js.close_form_title") %>"
href=""
class="advanced-filters--close icon-context icon-close"
data-action="filter--filters-form#toggleDisplayFilters"></a>
<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>
<% 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>
<li class="advanced-filters--controls">
<%= submit_tag t('button_apply'), class: 'button -small -primary', name: nil %>
</li>
</ul>
</fieldset>
<% end %>
<% end %>
<% end %>
+24
View File
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,111 +0,0 @@
<%= form_tag(
{},
method: :get,
data: {
action: "submit->filter--filters-form#sendForm:prevent"
}
) do %>
<% operators_without_values = %w[* !* t w] %>
<fieldset class="advanced-filters--container">
<a title="<%= t("js.close_form_title") %>"
href=""
class="advanced-filters--close icon-context icon-close"
data-action="filter--filters-form#toggleDisplayFilters"></a>
<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 %>
<% 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 %>
<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>
</ul>
</fieldset>
<% end %>
@@ -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)
+1 -5
View File
@@ -1,5 +1 @@
<%=
turbo_frame_tag "portfolios_filters" do
render(Projects::ProjectsFiltersComponent.new(query: @query))
end
%>
<%= render(Projects::ProjectsFiltersComponent.new(query: @query)) %>
+1 -5
View File
@@ -1,5 +1 @@
<%=
turbo_frame_tag "projects_filters" do
render(Projects::ProjectsFiltersComponent.new(query: @query))
end
%>
<%= render(Projects::ProjectsFiltersComponent.new(query: @query)) %>
@@ -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
%>