Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/components/_index.sass
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
@import "work_packages/progress/modal_body_component"
@import "work_packages/reminder/modal_body_component"
@import "work_packages/split_view_component"
@import "header/project_select_component"
@import "homescreen/link_component"
@import "homescreen/links_component"
@import "homescreen/new_features_component"
81 changes: 81 additions & 0 deletions app/components/header/project_select_component.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<%#-- 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.

++#%>

<%= render(
Primer::Alpha::Overlay.new(
title: t("header_project_select.title"),
visually_hide_title: true,
anchor_side: :outside_bottom,
anchor_align: :start,
size: :auto,
padding: :none
)
) do |overlay| %>
<% overlay.with_show_button(
id: "projects-menu",
classes: "op-project-select--trigger-button",
scheme: :invisible,
data: { "test-selector": "op-projects-menu" }
) do |button| %>
<%= trigger_label %>
<% button.with_trailing_visual_icon(icon: :"triangle-down") %>
<% end %>

<% overlay.with_body(classes: "op-project-select--body", data: { controller: "header-project-select" }) do %>
<%= render(
Primer::OpenProject::FilterableTreeView.new(
src: tree_src,
include_sub_items_check_box_arguments: { hidden: true },
filter_mode_control_arguments: logged_in? ? {} : { hidden: true }
)
) do |tree_view| %>
<% if logged_in? %>
<% tree_view.with_filter_mode(name: "all", label: t("filterable_tree_view.filter_mode.all"), selected: true) %>
<% tree_view.with_filter_mode(name: "favorited", label: t("header_project_select.favorites")) %>
<% end %>
<% end %>

<% unless OpenProject::FeatureDecisions.portfolio_models_active? %>
<%= helpers.flex_layout(justify_content: :flex_end, p: 2) do |bar| %>
<% bar.with_column(mr: 2) do %>
<%= render(Primer::Beta::Button.new(tag: :a, href: projects_path)) do %>
<%= t("header_project_select.all_projects") %>
<% end %>
<% end %>
<% if can_create_projects? %>
<% bar.with_column do %>
<%= render(Primer::Beta::Button.new(tag: :a, href: new_project_path, scheme: :primary)) do %>
<%= t("header_project_select.new_project") %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
61 changes: 61 additions & 0 deletions app/components/header/project_select_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# 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.
#++

module Header
class ProjectSelectComponent < ApplicationComponent
include OpenProject::StaticRouting::UrlHelpers

def initialize(current_project: nil, current_user: User.current, current_menu_item: nil)
super()
@current_project = current_project
@current_user = current_user
@current_menu_item = current_menu_item
end

def trigger_label
@current_project&.name || I18n.t("header_project_select.trigger_no_project")
end

def tree_src
header_projects_path(
current_project_id: @current_project&.id,
jump: @current_menu_item.presence
)
end

def can_create_projects?
@current_user.allowed_globally?(:add_project)
end

def logged_in?
@current_user.logged?
end
end
end
41 changes: 41 additions & 0 deletions app/components/header/project_select_component.sass
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//-- 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.
//++

.op-project-select
&--body
max-height: 75dvh
overflow: hidden
margin: var(--base-size-16)
margin-right: 0

&--trigger-button
min-width: unset
display: block

.Button-label
@include text-shortener
118 changes: 118 additions & 0 deletions app/controllers/header/projects_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# 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 Header::ProjectsController < ApplicationController
no_authorization_required! :index

MAX_NUMBER_OF_PROJECTS = 300

def index
@current_project_id = params[:current_project_id]&.to_i
@jump = params[:jump].presence
@projects = load_projects
@favorited_ids = load_favorited_ids
@tree = build_tree(@projects)

render layout: false
end

private

def query
params[:query].to_s.strip
end

def filter_mode
params[:filter_mode].to_s
end

def load_projects
projects = base_scope.to_a
ensure_current_project_present(projects)
end

def base_scope
scope = Project.visible.active.order(:lft).limit(MAX_NUMBER_OF_PROJECTS)
scope = scope.where("LOWER(name) LIKE LOWER(?)", "%#{ActiveRecord::Base.sanitize_sql_like(query)}%") if query.present?
scope = scope.where(id: favorite_project_ids) if filter_mode == "favorited" && User.current.logged?
scope
end

def ensure_current_project_present(projects)
return projects if query.present? || @current_project_id.blank?
return projects if projects.any? { |p| p.id == @current_project_id }

current = Project.visible.active.find_by(id: @current_project_id)
return projects unless current

(projects + current.self_and_ancestors.active.to_a).uniq(&:id).sort_by(&:lft)
end

def favorite_project_ids
Favorite.where(favorited_type: "Project", user_id: User.current.id).select(:favorited_id)
end

def load_favorited_ids
return Set.new unless User.current.logged?

Favorite
.where(favorited_type: "Project", user_id: User.current.id, favorited_id: @projects.map(&:id))
.pluck(:favorited_id)
.to_set
end

# Builds a nested structure from a flat, lft-ordered list of projects.
# Projects whose parent is not in the result set appear as roots.
# Each level is sorted alphabetically by project name.
def build_tree(projects)
nodes = projects.index_by(&:id).transform_values { |p| { project: p, children: [] } }

roots = []
projects.each do |project|
node = nodes[project.id]
parent = nodes[project.parent_id]

if parent
parent[:children] << node
else
roots << node
end
end

sort_nodes(roots)
end

def sort_nodes(nodes)
nodes.sort_by { |n| n[:project].name.downcase }.each do |node|
node[:children] = sort_nodes(node[:children])
node[:expanded] = node[:children].any? { |c| c[:project].id == @current_project_id || c[:expanded] }
end
end
end
81 changes: 81 additions & 0 deletions app/views/header/projects/_node.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<%#-- 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.

++#%>

<% project = node[:project] %>
<% children = node[:children] %>
<% is_current = project.id == current_project_id %>
<% is_favorited = favorited_ids.include?(project.id) %>
<% href = jump.present? ? project_path(project.identifier, jump:) : project_path(project.identifier) %>
<% is_expanded = node[:expanded] %>

<% add_icons = ->(tree_node) do
if is_current
tree_node.with_trailing_action_button(
icon: :"x-circle",
tag: :a,
href: home_path(jump:),
show_tooltip: false,
aria: { label: t("header_project_select.leave_project") }
)
end
if is_favorited
tree_node.with_trailing_visual_icon(icon: :"star-fill", classes: "op-primer--star-icon")
end
end %>

<% if children.any? %>
<% component.with_sub_tree(
label: project.name,
select_variant: :none,
current: is_current,
expanded: is_expanded,
href:,
data: { node_id: project.id }
) do |sub| %>
<% add_icons.call(sub) %>
<% children.each do |child_node| %>
<%= render "header/projects/node",
component: sub,
node: child_node,
current_project_id: current_project_id,
favorited_ids: favorited_ids,
jump: %>
<% end %>
<% end %>
<% else %>
<% component.with_leaf(
label: project.name,
select_variant: :none,
current: is_current,
href:,
data: { node_id: project.id }
) do |leaf| %>
<% add_icons.call(leaf) %>
<% end %>
<% end %>
Loading
Loading