diff --git a/app/controllers/project_folders_controller.rb b/app/controllers/project_folders_controller.rb
new file mode 100644
index 000000000..926b23954
--- /dev/null
+++ b/app/controllers/project_folders_controller.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+class ProjectFoldersController < ApplicationController
+ before_action :check_manage_permissions, only: %i(move_to)
+
+ def move_to
+ destination_folder = current_team.project_folders.find(move_params[:id])
+ destination_folder.transaction do
+ move_projects(destination_folder)
+ move_folders(destination_folder)
+ end
+ respond_to do |format|
+ format.json { render json: { flash: I18n.t('projects.move.success_flash') } }
+ end
+ rescue StandardError => e
+ Rails.logger.error e.message
+ Rails.logger.error e.backtrace.join("\n")
+ respond_to do |format|
+ format.json { render json: { flash: I18n.t('projects.move.error_flash') }, status: :bad_request }
+ end
+ end
+
+ private
+
+ def check_manage_permissions
+ render_403 unless can_update_team?(current_team)
+ end
+
+ def move_params
+ params.require(:id)
+ params.require(:movables)
+ params.permit(:id, movables: %i(type id))
+ end
+
+ def move_projects(destination_folder)
+ project_ids = move_params[:movables].collect { |movable| movable[:id] if movable[:type] == 'project' }.compact
+ return if project_ids.blank?
+
+ current_team.projects.where(id: project_ids).each { |p| p.update!(project_folder: destination_folder) }
+ end
+
+ def move_folders(destination_folder)
+ folder_ids = move_params[:movables].collect { |movable| movable[:id] if movable[:type] == 'project_folder' }.compact
+ return if folder_ids.blank?
+
+ current_team.project_folders.where(id: folder_ids).each { |f| f.update!(parent_folder: destination_folder) }
+ end
+end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 1807d164e..ff97036b4 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -416,6 +416,9 @@ en:
update:
success_flash: "Project %{name} successfully updated."
error_flash: "Project %{name} not updated."
+ move:
+ success_flash: "You have successfully moved the selected project(s)/folder(s) to another folder."
+ error_flash: "An error occurred. The selected project(s)/folder(s) have not been moved."
archive:
success_flash: "Project %{name} successfully archived."
error_flash: "Project %{name} not archived."
diff --git a/config/routes.rb b/config/routes.rb
index 344c5ef1d..df47b9f48 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -305,6 +305,10 @@ Rails.application.routes.draw do
resources :project_folders, only: [] do
get '/', to: 'projects#index'
get 'cards', to: 'projects#cards'
+
+ member do
+ post 'move_to', to: 'project_folders#move_to', defaults: { format: 'json' }
+ end
end
resources :experiments do
diff --git a/spec/controllers/project_folders_controller_spec.rb b/spec/controllers/project_folders_controller_spec.rb
new file mode 100644
index 000000000..b8dee3bef
--- /dev/null
+++ b/spec/controllers/project_folders_controller_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe ProjectFoldersController, type: :controller do
+ login_user
+ render_views
+
+ let!(:user) { subject.current_user }
+ let!(:team) { create :team, created_by: user }
+ let!(:user_team) { create :user_team, team: team, user: user, role: :admin }
+
+ describe 'POST #move_to' do
+ let!(:project_folder_1) do
+ create :project_folder, name: 'test folder A', team: team
+ end
+ let!(:project_folder_2) do
+ create :project_folder, name: 'test folder B', team: team
+ end
+ let!(:project_folder_3) do
+ create :project_folder, name: 'test folder C', team: team, parent_folder: project_folder_2
+ end
+ let!(:project_1) do
+ create :project, name: 'test project A', team: team, created_by: user
+ end
+ let!(:project_2) do
+ create :project, name: 'test project B', team: team, project_folder: project_folder_2, created_by: user
+ end
+ let!(:project_3) do
+ create :project, name: 'test project C', team: team, project_folder: project_folder_3, created_by: user
+ end
+
+ context 'in JSON format' do
+ let(:action) { post :move_to, params: params, format: :json }
+ let(:params) do
+ {
+ id: project_folder_1.id,
+ movables: [
+ { id: project_1.id, type: :project },
+ { id: project_folder_2.id, type: :project_folder }
+ ]
+ }
+ end
+
+ it 'returns success response' do
+ post :move_to, params: params, format: :json
+ expect(response).to have_http_status(:success)
+ expect(response.media_type).to eq 'application/json'
+ expect(project_1.reload.project_folder).to(be_eql(project_folder_1))
+ expect(project_folder_2.reload.parent_folder).to(be_eql(project_folder_1))
+ expect(project_folder_3.reload.parent_folders).to(include(project_folder_1))
+ expect(project_3.reload.project_folder.parent_folder.parent_folder).to(be_eql(project_folder_1))
+ end
+ end
+ end
+end