Skip to content

Commit

Permalink
Merge pull request #15609 from opf/public-project-lists
Browse files Browse the repository at this point in the history
Allow ProjectQuery to be marked as public
  • Loading branch information
klaustopher committed May 27, 2024
2 parents 2c06ed3 + f81c34b commit 62d0543
Show file tree
Hide file tree
Showing 23 changed files with 585 additions and 84 deletions.
24 changes: 23 additions & 1 deletion app/components/projects/index_page_header_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,29 @@
end

if query.persisted?
# TODO: Remove section when the sharing modal is implemented (https://community.openproject.org/projects/openproject/work_packages/55163)
if can_publish?
if query.public?
menu.with_item(
label: t(:button_unpublish),
scheme: :danger,
href: unpublish_projects_query_path(query),
content_arguments: { data: { method: :post } }
) do |item|
item.with_leading_visual_icon(icon: 'eye-closed')
end
else
menu.with_item(
label: t(:button_publish),
scheme: :default,
href: publish_projects_query_path(query),
content_arguments: { data: { method: :post } }
) do |item|
item.with_leading_visual_icon(icon: 'eye')
end
end
end

menu.with_item(
label: t(:button_delete),
scheme: :danger,
Expand All @@ -92,7 +115,6 @@
end
end
%>
<%= render(Projects::ConfigureViewModalComponent.new(query:)) %>
<%= render(Projects::DeleteListModalComponent.new(query:)) if query.persisted? %>
<%= render(Projects::ExportListModalComponent.new(query:)) %>
Expand Down
30 changes: 28 additions & 2 deletions app/components/projects/index_page_header_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,35 @@ def may_save_as? = current_user.logged?

def can_save_as? = may_save_as? && query.changed?

def can_save? = can_save_as? && query.persisted? && query.user == current_user
def can_save?
return false unless current_user.logged?
return false unless query.persisted?
return false unless query.changed?

def can_rename? = may_save_as? && query.persisted? && query.user == current_user && !query.changed?
if query.public?
current_user.allowed_globally?(:manage_public_project_queries)
else
query.user == current_user
end
end

def can_rename?
return false unless current_user.logged?
return false unless query.persisted?
return false if query.changed?

if query.public?
current_user.allowed_globally?(:manage_public_project_queries)
else
query.user == current_user
end
end

def can_publish?
OpenProject::FeatureDecisions.project_list_sharing_active? &&
current_user.allowed_globally?(:manage_public_project_queries) &&
query.persisted?
end

def show_state?
state == :show
Expand Down
42 changes: 21 additions & 21 deletions app/components/projects/row_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,19 @@ def hierarchy

def favored
render(Primer::Beta::IconButton.new(
icon: currently_favored? ? "star-fill" : "star",
scheme: :invisible,
mobile_icon: currently_favored? ? "star-fill" : "star",
size: :medium,
tag: :a,
tooltip_direction: :e,
href: helpers.build_favorite_path(project, format: :html),
data: { method: currently_favored? ? :delete : :post },
classes: currently_favored? ? "op-primer--star-icon " : "op-project-row-component--favorite",
label: currently_favored? ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite),
aria: { label: currently_favored? ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite) },
test_selector: 'project-list-favorite-button'
))
icon: currently_favored? ? "star-fill" : "star",
scheme: :invisible,
mobile_icon: currently_favored? ? "star-fill" : "star",
size: :medium,
tag: :a,
tooltip_direction: :e,
href: helpers.build_favorite_path(project, format: :html),
data: { method: currently_favored? ? :delete : :post },
classes: currently_favored? ? "op-primer--star-icon " : "op-project-row-component--favorite",
label: currently_favored? ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite),
aria: { label: currently_favored? ? I18n.t(:button_unfavorite) : I18n.t(:button_favorite) },
test_selector: "project-list-favorite-button"
))
end

def currently_favored?
Expand Down Expand Up @@ -197,7 +197,7 @@ def column_css_class(column)
def additional_css_class(column)
if column.attribute == :name
"project--hierarchy #{project.archived? ? 'archived' : ''}"
elsif [:status_explanation, :description].include?(column.attribute)
elsif %i[status_explanation description].include?(column.attribute)
"project-long-text-container"
elsif custom_field_column?(column)
cf = column.custom_field
Expand Down Expand Up @@ -254,7 +254,7 @@ def more_menu_favorite_item
href: helpers.build_favorite_path(project, format: :html),
data: { method: :post },
label: I18n.t(:button_favorite),
aria: { label: I18n.t(:button_favorite) },
aria: { label: I18n.t(:button_favorite) }
}
end

Expand All @@ -269,7 +269,7 @@ def more_menu_unfavorite_item
data: { method: :delete },
classes: "op-primer--star-icon",
label: I18n.t(:button_unfavorite),
aria: { label: I18n.t(:button_unfavorite) },
aria: { label: I18n.t(:button_unfavorite) }
}
end

Expand Down Expand Up @@ -302,7 +302,7 @@ def more_menu_activity_item
scheme: :default,
icon: :check,
label: I18n.t(:label_project_activity),
href: project_activity_index_path(project, event_types: ["project_attributes"]),
href: project_activity_index_path(project, event_types: ["project_attributes"])
}
end
end
Expand All @@ -317,7 +317,7 @@ def more_menu_archive_item
data: {
confirm: t("project.archive.are_you_sure", name: project.name),
method: :post
},
}
}
end
end
Expand All @@ -340,7 +340,7 @@ def more_menu_copy_item
scheme: :default,
icon: :copy,
label: I18n.t(:button_copy),
href: copy_project_path(project),
href: copy_project_path(project)
}
end
end
Expand All @@ -351,7 +351,7 @@ def more_menu_delete_item
scheme: :danger,
icon: :trash,
label: I18n.t(:button_delete),
href: confirm_destroy_project_path(project),
href: confirm_destroy_project_path(project)
}
end
end
Expand All @@ -361,7 +361,7 @@ def user_can_view_project?
end

def custom_field_column?(column)
column.is_a?(Queries::Projects::Selects::CustomField)
column.is_a?(::Queries::Projects::Selects::CustomField)
end
end
end
24 changes: 11 additions & 13 deletions app/components/projects/table_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,10 @@ def build_sort_header(column, options)
# We don't return the project row
# but the [project, level] array from the helper
def rows
@rows ||=
begin
projects_enumerator = ->(model) { to_enum(:projects_with_levels_order_sensitive, model).to_a }
instance_exec(model, &projects_enumerator)
end
@rows ||= begin
projects_enumerator = ->(model) { to_enum(:projects_with_levels_order_sensitive, model).to_a }
instance_exec(model, &projects_enumerator)
end
end

def initialize_sorted_model
Expand Down Expand Up @@ -113,15 +112,14 @@ def sortable_column?(select)
end

def columns
@columns ||=
begin
columns = query.selects.reject { |select| select.is_a?(Queries::Selects::NotExistingSelect) }
@columns ||= begin
columns = query.selects.reject { |select| select.is_a?(::Queries::Selects::NotExistingSelect) }

index = columns.index { |column| column.attribute == :name }
columns.insert(index, Queries::Projects::Selects::Default.new(:hierarchy)) if index
index = columns.index { |column| column.attribute == :name }
columns.insert(index, ::Queries::Projects::Selects::Default.new(:hierarchy)) if index

columns
end
columns
end
end

def projects(query)
Expand Down Expand Up @@ -156,7 +154,7 @@ def projects_with_level(projects, &)
end

def favored_project_ids
@favored_projects ||= Favorite.where(user: current_user, favored_type: 'Project').pluck(:favored_id)
@favored_project_ids ||= Favorite.where(user: current_user, favored_type: "Project").pluck(:favored_id)
end

def sorted_by_lft?
Expand Down
25 changes: 22 additions & 3 deletions app/contracts/queries/projects/project_queries/base_contract.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,36 @@ def self.model
presence: true,
length: { maximum: 255 }

validate :user_is_current_user_and_logged_in
validate :name_select_included
validate :existing_selects
validate :user_is_logged_in
validate :allowed_to_modify_private_query
validate :allowed_to_modify_public_query

protected

def user_is_current_user_and_logged_in
unless user.logged? && user == model.user
def user_is_logged_in
unless user.logged?
errors.add :base, :error_unauthorized
end
end

def allowed_to_modify_private_query
return if model.public?

if model.user != user
errors.add :base, :can_only_be_modified_by_owner
end
end

def allowed_to_modify_public_query
return unless model.public?

unless user.allowed_globally?(:manage_public_project_queries)
errors.add :base, :need_permission_to_modify_public_query
end
end

def name_select_included
if model.selects.none? { |s| s.attribute == :name }
errors.add :selects, :name_not_included
Expand Down
33 changes: 33 additions & 0 deletions app/contracts/queries/projects/project_queries/publish_contract.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2024 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 Queries::Projects::ProjectQueries
class PublishContract < BaseContract
attribute :public
end
end
60 changes: 38 additions & 22 deletions app/controllers/projects/queries_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,17 @@ class Projects::QueriesController < ApplicationController

# No need for a more specific authorization check. That is carried out in the contracts.
before_action :require_login
before_action :find_query, only: %i[rename update destroy]
before_action :find_query, only: %i[show rename update destroy publish unpublish]
before_action :build_query_or_deny_access, only: %i[new create]

current_menu_item [:new, :rename, :create, :update] do
:projects
end

def show
redirect_to projects_path(query_id: @query.id)
end

def new
render template: "/projects/index",
layout: "global",
Expand All @@ -55,35 +59,31 @@ def create
.new(from: @query, user: current_user)
.call(permitted_query_params)

if call.success?
flash[:notice] = I18n.t("lists.create.success")

redirect_to projects_path(query_id: call.result.id)
else
flash[:error] = I18n.t("lists.create.failure", errors: call.errors.full_messages.join("\n"))

render template: "/projects/index",
layout: "global",
locals: { query: call.result, state: :edit }
end
render_result(call, success_i18n_key: "lists.create.success", error_i18n_key: "lists.create.failure")
end

def update
call = Queries::Projects::ProjectQueries::UpdateService
.new(user: current_user, model: @query)
.call(permitted_query_params)

if call.success?
flash[:notice] = I18n.t("lists.update.success")
render_result(call, success_i18n_key: "lists.update.success", error_i18n_key: "lists.update.failure")
end

redirect_to projects_path(query_id: call.result.id)
else
flash[:error] = I18n.t("lists.update.failure", errors: call.errors.full_messages.join("\n"))
def publish
call = Queries::Projects::ProjectQueries::PublishService
.new(user: current_user, model: @query)
.call(public: true)

render template: "/projects/index",
layout: "global",
locals: { query: call.result, state: :edit }
end
render_result(call, success_i18n_key: "lists.publish.success", error_i18n_key: "lists.publish.failure")
end

def unpublish
call = Queries::Projects::ProjectQueries::PublishService
.new(user: current_user, model: @query)
.call(public: false)

render_result(call, success_i18n_key: "lists.unpublish.success", error_i18n_key: "lists.unpublish.failure")
end

def destroy
Expand All @@ -95,7 +95,23 @@ def destroy

private

def render_result(service_call, success_i18n_key:, error_i18n_key:) # rubocop:disable Metrics/AbcSize
modified_query = service_call.result

if service_call.success?
flash[:notice] = I18n.t(success_i18n_key)

redirect_to modified_query.visible? ? projects_path(query_id: modified_query.id) : projects_path
else
flash[:error] = I18n.t(error_i18n_key, errors: service_call.errors.full_messages.join("\n"))

render template: "/projects/index",
layout: "global",
locals: { query: modified_query, state: :edit }
end
end

def find_query
@query = Queries::Projects::ProjectQuery.find(params[:id])
@query = Queries::Projects::ProjectQuery.visible(current_user).find(params[:id])
end
end
Loading

0 comments on commit 62d0543

Please sign in to comment.