<%= select_tag 'add_filter_select',
options_from_collection_for_select(
- allowed_filters(query),
- :name,
- :human_name,
- disabled: query.filters.map(&:name)
+ allowed_filters,
+ :name,
+ :human_name,
+ disabled: query.filters.map(&:name)
),
prompt: t(:actionview_instancetag_blank_option),
class: 'advanced-filters--select',
@@ -110,12 +110,12 @@
<% unless EnterpriseToken.allows_to?(:custom_fields_in_projects_list)%>
<%=
- angular_component_tag 'op-enterprise-banner',
- inputs: {
- collapsible: true,
- textMessage: t('ee.upsale.project_filters.description_html'),
- moreInfoLink: OpenProject::Static::Links.links[:enterprise_docs][:custom_field_projects][:href],
- }
+ helpers.angular_component_tag 'op-enterprise-banner',
+ inputs: {
+ collapsible: true,
+ textMessage: t('ee.upsale.project_filters.description_html'),
+ moreInfoLink: OpenProject::Static::Links.links[:enterprise_docs][:custom_field_projects][:href],
+ }
%>
<% end %>
diff --git a/app/components/projects/filters_component.rb b/app/components/projects/filters_component.rb
new file mode 100644
index 000000000000..c25894c717b2
--- /dev/null
+++ b/app/components/projects/filters_component.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+# -- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2010-2023 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 Projects::FiltersComponent < ApplicationComponent
+ options :query
+
+ def allowed_filters
+ query
+ .available_filters
+ .select { |f| allowed_filter?(f) }
+ .sort_by(&:human_name)
+ end
+
+ private
+
+ def allowed_filter?(filter)
+ allowlist = [
+ Queries::Projects::Filters::ActiveFilter,
+ Queries::Projects::Filters::TemplatedFilter,
+ Queries::Projects::Filters::PublicFilter,
+ Queries::Projects::Filters::ProjectStatusFilter,
+ Queries::Projects::Filters::MemberOfFilter,
+ Queries::Projects::Filters::CreatedAtFilter,
+ Queries::Projects::Filters::LatestActivityAtFilter,
+ Queries::Projects::Filters::NameAndIdentifierFilter,
+ Queries::Projects::Filters::TypeFilter
+ ]
+ allowlist << Queries::Filters::Shared::CustomFields::Base if EnterpriseToken.allows_to?(:custom_fields_in_projects_list)
+
+ allowlist.detect { |clazz| filter.is_a? clazz }
+ end
+end
diff --git a/app/components/projects/index_page_header_component.html.erb b/app/components/projects/index_page_header_component.html.erb
new file mode 100644
index 000000000000..4fc49406a67a
--- /dev/null
+++ b/app/components/projects/index_page_header_component.html.erb
@@ -0,0 +1,100 @@
+<%= render(Primer::OpenProject::PageHeader.new) do |header| %>
+ <% header.with_title { t(:label_project_plural) } %>
+
+ <% header.with_actions do %>
+ <% if current_user.allowed_globally?(:add_project) %>
+ <%= render(
+ Primer::Beta::Button.new(
+ tag: :a,
+ href: new_project_path,
+ scheme: :primary,
+ size: :medium,
+ aria: { label: I18n.t(:label_project_new) },
+ mr: BUTTON_MARGIN_RIGHT,
+ data: { 'test-selector': 'project-new-button' }
+ )
+ ) do |button|
+ button.with_leading_visual_icon(icon: :plus)
+ Project.model_name.human
+ end
+ %>
+ <% end %>
+
+ <%= render(
+ Primer::Beta::IconButton.new(
+ icon: :filter,
+ size: :medium,
+ aria: { label: t(:label_filters_toggle) },
+ mr: BUTTON_MARGIN_RIGHT,
+ data: { 'project-target': 'filterFormToggle',
+ 'action': 'project#toggleDisplayFilters',
+ 'test-selector': 'project-filter-toggle' }
+ )
+ )
+ %>
+
+ <%= render(
+ Primer::Beta::Button.new(
+ tag: :a,
+ href: activities_path,
+ size: :medium,
+ type: :submit,
+ aria: { label: t(:label_overall_activity) },
+ mr: BUTTON_MARGIN_RIGHT
+ )
+ ) do
+ t(:label_overall_activity)
+ end
+ %>
+
+ <%= render(
+ Primer::Beta::Button.new(
+ tag: :a,
+ href: gantt_portfolio_query_link,
+ size: :medium,
+ disabled: gantt_portfolio_project_ids.empty?,
+ type: :submit,
+ aria: { label: t('projects.index.open_as_gantt') },
+ mr: BUTTON_MARGIN_RIGHT,
+ id: 'projects-index-open-as-gantt',
+ target: '_blank'
+ )
+ ) do |button|
+ button.with_leading_visual_icon(icon: 'op-view-timeline')
+ button.with_trailing_visual_icon(icon: 'link-external')
+ button.with_tooltip(text: gantt_portfolio_title)
+
+ t('projects.index.open_as_gantt')
+ end %>
+
+ <%= render(Primer::Alpha::ActionMenu.new) do |menu|
+ menu.with_show_button(icon: 'op-kebab-vertical', 'aria-label': t(:label_more), data: { 'test-selector': 'project-more-dropdown-menu' })
+
+ if current_user.admin?
+ menu.with_item(
+ label: t('button_configure'),
+ href: admin_settings_projects_path,
+ content_arguments: { target: '_blank' }
+ ) do |item|
+ item.with_leading_visual_icon(icon: :gear)
+ end
+ end
+
+ menu.with_item(
+ label: t('js.label_export'),
+ content_arguments: { 'data-show-dialog-id': 'project-export-dialog' }
+ ) do |item|
+ item.with_leading_visual_icon(icon: 'op-file-download')
+ end
+ end
+ %>
+ <% end %>
+<% end %>
+
+<%= render(Primer::Alpha::Dialog.new(title: t('js.label_export'),
+ id: 'project-export-dialog')) do |d|
+ d.with_header(variant: :large)
+ d.with_body do
+ render partial: 'project_export_modal', locals: { query: query }
+ end
+end %>
diff --git a/app/components/projects/index_page_header_component.rb b/app/components/projects/index_page_header_component.rb
new file mode 100644
index 000000000000..bfd7af52b0e0
--- /dev/null
+++ b/app/components/projects/index_page_header_component.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+# -- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2010-2023 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 Projects::IndexPageHeaderComponent < ApplicationComponent
+ options :projects,
+ :current_user,
+ :query
+
+ BUTTON_MARGIN_RIGHT = 2
+
+ def gantt_portfolio_query_link
+ generator = ::Projects::GanttQueryGeneratorService.new(gantt_portfolio_project_ids)
+ work_packages_path query_props: generator.call
+ end
+
+ def gantt_portfolio_project_ids
+ @gantt_portfolio_project_ids ||= projects
+ .where(active: true)
+ .select(:id)
+ .uniq
+ .pluck(:id)
+ end
+
+ def gantt_portfolio_title
+ title = t('projects.index.open_as_gantt_title')
+
+ if current_user.admin?
+ title << ' '
+ title << t('projects.index.open_as_gantt_title_admin')
+ end
+
+ title
+ end
+end
diff --git a/app/components/projects/storage_information_component.html.erb b/app/components/projects/storage_information_component.html.erb
new file mode 100644
index 000000000000..92a6c1658bce
--- /dev/null
+++ b/app/components/projects/storage_information_component.html.erb
@@ -0,0 +1,6 @@
+
+ <%= helpers.op_icon('icon-info1') %>
+ <%= t(:label_projects_storage_information,
+ count: Project.count,
+ storage: number_to_human_size(Project.total_projects_size, precision: 2)) %>
+
diff --git a/app/components/projects/storage_information_component.rb b/app/components/projects/storage_information_component.rb
new file mode 100644
index 000000000000..e42c11bfa2f2
--- /dev/null
+++ b/app/components/projects/storage_information_component.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+# -- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2010-2023 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 Projects::StorageInformationComponent < ApplicationComponent
+ options :current_user
+
+ def render?
+ current_user.admin?
+ end
+end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index ffb6aa4b7898..d7b2a1f0eb4b 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -33,30 +33,6 @@ def show_filters_section?
params[:filters].present? && !params.key?(:hide_filters_section)
end
- def allowed_filters(query)
- query
- .available_filters
- .select { |f| whitelisted_project_filter?(f) }
- .sort_by(&:human_name)
- end
-
- def whitelisted_project_filter?(filter)
- whitelist = [
- Queries::Projects::Filters::ActiveFilter,
- Queries::Projects::Filters::TemplatedFilter,
- Queries::Projects::Filters::PublicFilter,
- Queries::Projects::Filters::ProjectStatusFilter,
- Queries::Projects::Filters::MemberOfFilter,
- Queries::Projects::Filters::CreatedAtFilter,
- Queries::Projects::Filters::LatestActivityAtFilter,
- Queries::Projects::Filters::NameAndIdentifierFilter,
- Queries::Projects::Filters::TypeFilter
- ]
- whitelist << Queries::Filters::Shared::CustomFields::Base if EnterpriseToken.allows_to?(:custom_fields_in_projects_list)
-
- whitelist.detect { |clazz| filter.is_a? clazz }
- end
-
def no_projects_result_box_params
if User.current.allowed_globally?(:add_project)
{ action_url: new_project_path, display_action: true }
@@ -290,30 +266,6 @@ def allowed_parent_projects(project)
.assignable_parents
end
- def gantt_portfolio_query_link(filtered_project_ids)
- generator = ::Projects::GanttQueryGeneratorService.new(filtered_project_ids)
- work_packages_path query_props: generator.call
- end
-
- def gantt_portfolio_project_ids(project_scope)
- project_scope
- .where(active: true)
- .select(:id)
- .uniq
- .pluck(:id)
- end
-
- def gantt_portfolio_title
- title = t('projects.index.open_as_gantt_title')
-
- if current_user.admin?
- title << ' '
- title << t('projects.index.open_as_gantt_title_admin')
- end
-
- title
- end
-
def short_project_description(project, length = 255)
unless project.description.present?
return ''
diff --git a/app/views/projects/_project_export_modal.html.erb b/app/views/projects/_project_export_modal.html.erb
index 5595041f9253..7a6d9cf73d63 100644
--- a/app/views/projects/_project_export_modal.html.erb
+++ b/app/views/projects/_project_export_modal.html.erb
@@ -27,32 +27,15 @@ See COPYRIGHT and LICENSE files for more details.
++#%>
-
-
-
-
-
- <% supported_export_formats.each do |key| %>
- -
- <% filter_params = safe_query_params ['filters', 'sortBy'] %>
- <%= link_to url_for(action: 'index', format: key, **filter_params),
- class: 'op-export-options--option-link' do %>
- <%= op_icon("icon-big icon-export-#{key}") %>
- <%= t("export.format.#{key}") %>
- <% end %>
-
+
+ <% supported_export_formats.each do |key| %>
+ -
+ <% filter_params = safe_query_params ['filters', 'sortBy'] %>
+ <%= link_to url_for(action: 'index', format: key, **filter_params),
+ class: 'op-export-options--option-link' do %>
+ <%= op_icon("icon-big icon-export-#{key}") %>
+ <%= t("export.format.#{key}") %>
<% end %>
-
-
-
-
-
-
-
-
+
+ <% end %>
+
diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb
index 7340ae16ce4a..9a9020828102 100644
--- a/app/views/projects/index.html.erb
+++ b/app/views/projects/index.html.erb
@@ -29,73 +29,17 @@ See COPYRIGHT and LICENSE files for more details.
<% html_title(t(:label_project_plural)) -%>
- <%= toolbar title: t(:label_project_plural), html: { class: '-with-dropdown' } do %>
- <% if User.current.allowed_globally?(:add_project) %>
-
- <%= link_to new_project_path,
- { class: 'button -alt-highlight',
- aria: { label: t(:label_project_new) },
- title: t(:label_project_new) } do %>
- <%= op_icon('button--icon icon-add') %>
- <%= Project.model_name.human %>
- <% end %>
-
- <% end %>
-
-
-
-
- <%= link_to t(:label_overall_activity), activities_path, class: 'button' %>
-
-
- <% gantt_project_ids = gantt_portfolio_project_ids(@projects) %>
- <%= link_to gantt_portfolio_query_link(gantt_project_ids),
- disabled: gantt_project_ids.empty?,
- class: "button #{gantt_project_ids.empty? ? '-disabled' : ''}",
- title: gantt_portfolio_title,
- target: '_blank' do %>
- <%= op_icon("button--icon icon-view-timeline") %>
- <%= t('projects.index.open_as_gantt') %>
- <%= op_icon("button--icon icon-external-link") %>
- <% end %>
-
-
-
- <%= op_icon('button--icon icon-show-more') %>
-
-
-
- <% end %>
-
- <%= render partial: 'projects/filters/form', locals: { query: @query } %>
+ data-application-target="dynamic"
+ data-project-display-filters-value="<%= show_filters_section? %>">
+ <%= render Projects::IndexPageHeaderComponent.new(
+ projects: @projects,
+ query: @query,
+ current_user:
+ ) %>
+
+ <%= render Projects::FiltersComponent.new(
+ query: @query,
+ ) %>
<%= render Projects::TableComponent.new(
rows: @projects,
@@ -103,12 +47,7 @@ See COPYRIGHT and LICENSE files for more details.
orders: @orders,
params: params) %>
- <% if User.current.admin? %>
-
- <%= op_icon('icon-info1') %>
- <%= t(:label_projects_storage_information,
- count: Project.count,
- storage: number_to_human_size(Project.total_projects_size, precision: 2)) %>
-
- <% end %>
+ <%= render Projects::StorageInformationComponent.new(
+ current_user: current_user
+ ) %>
diff --git a/frontend/src/stimulus/controllers/dynamic/project.controller.ts b/frontend/src/stimulus/controllers/dynamic/project.controller.ts
index 23316d632a6e..e69201c8f8ec 100644
--- a/frontend/src/stimulus/controllers/dynamic/project.controller.ts
+++ b/frontend/src/stimulus/controllers/dynamic/project.controller.ts
@@ -71,9 +71,31 @@ export default class ProjectController extends Controller {
declare readonly singleDayTargets:HTMLInputElement[];
declare readonly simpleValueTargets:HTMLInputElement[];
- toggleFilterForm() {
- this.filterFormToggleTarget.classList.toggle('-active');
- this.filterFormTarget.classList.toggle('-expanded');
+ static values = {
+ displayFilters: { type: Boolean, default: false },
+ };
+
+ declare displayFiltersValue:boolean;
+
+ toggleDisplayFilters() {
+ this.displayFiltersValue = !this.displayFiltersValue;
+ }
+
+ displayFiltersValueChanged() {
+ this.toggleButtonActive();
+ this.toggleFilterFormVisible();
+ }
+
+ toggleButtonActive() {
+ if (this.displayFiltersValue) {
+ this.filterFormToggleTarget.setAttribute('aria-disabled', 'true');
+ } else {
+ this.filterFormToggleTarget.removeAttribute('aria-disabled');
+ }
+ }
+
+ toggleFilterFormVisible() {
+ this.filterFormTarget.classList.toggle('-expanded', this.displayFiltersValue);
}
toggleMultiSelect({ params: { filterName } }:{ params:{ filterName:string } }) {
diff --git a/spec/features/projects/projects_portfolio_spec.rb b/spec/features/projects/projects_portfolio_spec.rb
index 22164b0de5e5..2519a25d8325 100644
--- a/spec/features/projects/projects_portfolio_spec.rb
+++ b/spec/features/projects/projects_portfolio_spec.rb
@@ -53,7 +53,7 @@
projects_page.open_filters
projects_page.filter_by_active('yes')
- expect(page).to have_selector('.button.-disabled', text: 'Open as Gantt view', wait: 10)
+ projects_page.expect_gantt_button(disabled: true)
end
end
@@ -63,7 +63,7 @@
it 'disables the button' do
visit projects_path
- expect(page).to have_selector('.button.-disabled', text: 'Open as Gantt view')
+ projects_page.expect_gantt_button(disabled: true)
end
end
diff --git a/spec/support/pages/projects/index.rb b/spec/support/pages/projects/index.rb
index fc1eaabc484b..2cf33fcd3078 100644
--- a/spec/support/pages/projects/index.rb
+++ b/spec/support/pages/projects/index.rb
@@ -88,6 +88,11 @@ def expect_filter_set(filter_name)
visible: :hidden)
end
+ def expect_gantt_button(disabled: false)
+ expect(page).to have_selector("button#{disabled ? '[disabled]' : ''}",
+ text: 'Open as Gantt view')
+ end
+
def filter_by_active(value)
set_filter('active',
'Active',
@@ -182,15 +187,15 @@ def set_custom_field_filter(selected_filter, human_operator, values)
def open_filters
retry_block do
- click_button('Show/hide filters')
+ page.find('[data-test-selector="project-filter-toggle"]').click
page.find_field('Add filter', visible: true)
end
end
def click_more_menu_item(item)
page.find('[data-test-selector="project-more-dropdown-menu"]').click
- page.within('.menu-drop-down-container') do
- click_link(item)
+ page.within('.ActionListWrap') do
+ click(item)
end
end
@@ -218,9 +223,7 @@ def navigate_to_new_project_page_from_global_sidebar
end
def navigate_to_new_project_page_from_toolbar_items
- within '.toolbar-items' do
- click_on 'New project'
- end
+ find('[data-test-selector="project-new-button"]').click
end
private