Skip to content

Commit

Permalink
Merge pull request #16078 from opf/implementation/55962-add-the-proje…
Browse files Browse the repository at this point in the history
…ct-list-view-including-the-project-folder-custom-column

[#55962] Add the project list view, including the "project folder" custom column
  • Loading branch information
judithroth authored Jul 10, 2024
2 parents bd9c253 + 9b4af6e commit 4479691
Show file tree
Hide file tree
Showing 12 changed files with 351 additions and 11 deletions.
3 changes: 2 additions & 1 deletion app/models/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ class Project < ApplicationRecord

friendly_id :identifier, use: :finders

scopes :allowed_to,
scopes :activated_in_storage,
:allowed_to,
:available_custom_fields,
:visible

Expand Down
53 changes: 53 additions & 0 deletions app/models/projects/scopes/activated_in_storage.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2010-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 Projects::Scopes
module ActivatedInStorage
extend ActiveSupport::Concern

class_methods do
def activated_in_storage(storage_ids)
subquery = project_storages_subquery(storage_ids:)
where(id: subquery)
end

def not_activated_in_storage(storage_ids)
subquery = project_storages_subquery(storage_ids:)
where.not(id: subquery)
end

private

def project_storages_subquery(storage_ids:)
Storages::ProjectStorage
.select(:project_id)
.where(storage_id: storage_ids)
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,13 @@
# associated with a Storage
module Storages::ProjectStorages::Projects
class RowComponent < Projects::RowComponent # rubocop:disable OpenProject/AddPreviewForViewComponent
def project_folder_type
project_folder_mode = table.project_folder_modes_per_project[project.id]
I18n.t("project_storages.project_folder_mode.#{project_folder_mode}")
end

def more_menu_items
@more_menu_items ||= []
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ module Storages::ProjectStorages::Projects
class TableComponent < Projects::TableComponent # rubocop:disable OpenProject/AddPreviewForViewComponent
include OpTurbo::Streamable

options :project_folder_modes_per_project

def columns
@columns ||= query
.selects
.insert(1, ::Queries::Projects::Selects::Default.new(:project_folder_type))
end

def sortable?
false
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,16 @@ def index
@project_query = ProjectQuery.new(
name: "project-storage-mappings-#{@storage.id}"
) do |query|
query.where(:id, "=", @storage.projects.ids)
query.where(:storages, "=", [@storage.id])
query.select(:name)
query.order("lft" => "asc")
end

# Prepare data for project_folder_type column
@project_folder_modes_per_project = Storages::ProjectStorage
.where(storage_id: @storage.id)
.pluck(:project_id, :project_folder_mode)
.to_h
end

def new; end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# frozen_string_literal: true

# -- copyright
# OpenProject is an open source project management software.
# Copyright (C) 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.
# ++
#

# This filter is used to find projects (including archived projects) that use one
# of the given storages ids.
module Queries::Storages::Projects::Filter
class StoragesFilter < ::Queries::Projects::Filters::ProjectFilter
def self.key
:storages
end

def type
:list
end

def allowed_values
@allowed_values ||= Storages::Storage
.pluck(:name, :id)
end

def available?
User.current.admin?
end

def apply_to(_query_scope)
case operator
when "="
super.activated_in_storage(values)
when "!"
super.not_activated_in_storage(values)
else
raise "unsupported operator"
end
end

def where
nil
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ See COPYRIGHT and LICENSE files for more details.
<%=
render(Storages::ProjectStorages::Projects::TableComponent.new(
query: @project_query,
params:
)
project_folder_modes_per_project: @project_folder_modes_per_project,
params:)
)
%>
5 changes: 5 additions & 0 deletions modules/storages/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ en:
permission_view_file_links: View file links
permission_write_files: 'Automatically managed project folders: Write files'
project_module_storages: Files
project_storages:
project_folder_mode:
automatic: Automatically managed
inactive: No specific folder
manual: Existing folder manually managed
storages:
buttons:
complete_without_setup: Complete without it
Expand Down
1 change: 1 addition & 0 deletions modules/storages/lib/open_project/storages/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ def self.permissions
::Queries::Register.register(::ProjectQuery) do
filter ::Queries::Storages::Projects::Filter::StorageIdFilter
filter ::Queries::Storages::Projects::Filter::StorageUrlFilter
filter ::Queries::Storages::Projects::Filter::StoragesFilter
end

::Queries::Register.register(::Queries::Storages::FileLinks::FileLinkQuery) do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,30 @@
with_flag: { enable_storage_for_multiple_projects: true } do
shared_let(:admin) { create(:admin, preferences: { time_zone: "Etc/UTC" }) }
shared_let(:non_admin) { create(:user) }

shared_let(:project) { create(:project, name: "My active Project") }
shared_let(:archived_project) { create(:project, active: false, name: "My archived Project") }
shared_let(:storage) { create(:nextcloud_storage, name: "My Nextcloud Storage") }
shared_let(:project_storage) { create :project_storage, project:, storage: }
shared_let(:archived_project_project_storage) { create :project_storage, project: archived_project, storage: }
shared_let(:archived_project) do
create(:project,
active: false,
name: "My archived Project")
end
shared_let(:storage) do
create(:nextcloud_storage,
:as_automatically_managed,
name: "My Nextcloud Storage")
end
shared_let(:project_storage) do
create(:project_storage,
project:,
storage:,
project_folder_mode: "automatic")
end
shared_let(:archived_project_project_storage) do
create(:project_storage,
project: archived_project,
storage:,
project_folder_mode: "inactive")
end

current_user { admin }

Expand Down Expand Up @@ -76,10 +95,19 @@
end
end

aggregate_failures "shows the correct project mappings" do
aggregate_failures "shows the correct table headers" do
within "#project-table" do
expect(page)
.to have_css("th", text: "NAME")
expect(page)
.to have_css("th", text: "PROJECT FOLDER TYPE")
end
end

aggregate_failures "shows the correct project mappings including archived projects and their folder modes" do
within "#project-table" do
expect(page).to have_text(project.name)
expect(page).to have_text(archived_project.name)
expect(page).to have_text("#{project.name} Automatically managed")
expect(page).to have_text("#{archived_project.name} No specific folder")
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#-- 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.
#++

require "spec_helper"

RSpec.describe Queries::Storages::Projects::Filter::StoragesFilter do
it_behaves_like "basic query filter" do
let(:class_key) { :storages }
let(:type) { :list }
end

it_behaves_like "list query filter", scope: false do
shared_let(:project) { create(:project) }
shared_let(:storage1) { create(:storage, :as_generic) }
shared_let(:storage2) { create(:storage, :as_generic) }

let(:valid_values) do
[storage1.id, storage2.id]
end
let(:name) { "Available project attributes" }

describe "#apply_to" do
let(:values) { valid_values }

let(:project_ids_from_project_storages_handwritten_sql) do
<<-SQL.squish
SELECT "project_storages"."project_id"
FROM "project_storages"
WHERE "project_storages"."storage_id"
IN (#{values.join(', ')})
SQL
end

context 'for "="' do
let(:operator) { "=" }

it "is the same as handwriting the query" do
handwritten_scope_sql = <<-SQL.squish
SELECT "projects".* FROM "projects"
WHERE "projects"."id" IN (#{project_ids_from_project_storages_handwritten_sql})
SQL

expect(instance.apply_to(Project).to_sql).to eql handwritten_scope_sql
end
end

context 'for "!"' do
let(:operator) { "!" }

it "is the same as handwriting the query" do
handwritten_scope_sql = <<-SQL.squish
SELECT "projects".* FROM "projects"
WHERE "projects"."id" NOT IN (#{project_ids_from_project_storages_handwritten_sql})
SQL

expect(instance.apply_to(Project).to_sql).to eql handwritten_scope_sql
end
end

context "for an unsupported operator" do
let(:operator) { "!=" }

it "raises an error" do
expect { instance.apply_to(Project) }.to raise_error("unsupported operator")
end
end
end

describe "#allowed_values" do
it "is a list of the possible values" do
expected = [
[storage1.name, storage1.id],
[storage2.name, storage2.id]
]

expect(instance.allowed_values).to match_array(expected)
end
end
end
end
Loading

0 comments on commit 4479691

Please sign in to comment.