diff --git a/app/controllers/storage_locations_controller.rb b/app/controllers/storage_locations_controller.rb index 849b16348..fb30f964a 100644 --- a/app/controllers/storage_locations_controller.rb +++ b/app/controllers/storage_locations_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class StorageLocationsController < ApplicationController - before_action :load_storage_location, only: %i(update destroy) + before_action :load_storage_location, only: %i(update destroy move) before_action :check_read_permissions, only: :index before_action :check_manage_permissions, except: :index before_action :set_breadcrumbs_items, only: :index @@ -51,6 +51,30 @@ class StorageLocationsController < ApplicationController end end + def move + storage_location_destination = + if move_params[:destination_storage_location_id] == 'root_storage_location' + nil + else + current_team.storage_locations.find(move_params[:destination_storage_location_id]) + end + + @storage_location.update!(parent: storage_location_destination) + + render json: { message: I18n.t('storage_locations.index.move_modal.success_flash') } + rescue StandardError => e + Rails.logger.error e.message + Rails.logger.error e.backtrace.join("\n") + render json: { error: I18n.t('storage_locations.index.move_modal.error_flash') }, status: :bad_request + end + + def tree + records = StorageLocation.inner_storage_locations(current_team) + .order(:name) + .select(:id, :name, :parent_id, :container) + render json: storage_locations_recursive_builder(nil, records) + end + def actions_toolbar render json: { actions: @@ -68,8 +92,12 @@ class StorageLocationsController < ApplicationController metadata: [:display_type, dimensions: [], parent_coordinations: []]) end + def move_params + params.permit(:id, :destination_storage_location_id) + end + def load_storage_location - @storage_location = StorageLocation.where(team: current_team).find(storage_location_params[:id]) + @storage_location = current_team.storage_locations.find_by(id: storage_location_params[:id]) render_404 unless @storage_location end @@ -95,7 +123,7 @@ class StorageLocationsController < ApplicationController storage_locations = [] if params[:parent_id] - location = StorageLocation.where(team: current_team).find_by(id: params[:parent_id]) + location = current_team.storage_locations.find_by(id: params[:parent_id]) if location storage_locations.unshift(breadcrumbs_item(location)) while location.parent @@ -113,4 +141,19 @@ class StorageLocationsController < ApplicationController url: storage_locations_path(parent_id: location.id) } end + + def storage_locations_recursive_builder(storage_location, records) + children = records.select do |i| + defined?(i.parent_id) && i.parent_id == storage_location&.id + end + + children.filter_map do |i| + next if i.container + + { + storage_location: i, + children: storage_locations_recursive_builder(i, records) + } + end + end end diff --git a/app/javascript/vue/storage_locations/modals/move.vue b/app/javascript/vue/storage_locations/modals/move.vue new file mode 100644 index 000000000..052b8e268 --- /dev/null +++ b/app/javascript/vue/storage_locations/modals/move.vue @@ -0,0 +1,114 @@ + + + + + + + + + + + {{ i18n.t('storage_locations.index.move_modal.title', { name: this.selectedObject.name }) }} + + + + {{ i18n.t('storage_locations.index.move_modal.description', { name: this.selectedObject.name }) }} + + + + + + + + + + {{ i18n.t('storage_locations.index.move_modal.search_header') }} + + + + + + + + + + + + diff --git a/app/javascript/vue/storage_locations/modals/move_tree.vue b/app/javascript/vue/storage_locations/modals/move_tree.vue new file mode 100644 index 000000000..720642392 --- /dev/null +++ b/app/javascript/vue/storage_locations/modals/move_tree.vue @@ -0,0 +1,45 @@ + + + + + + + + + {{ storageLocationTree.storage_location.name }} + + + + + + + + diff --git a/app/javascript/vue/storage_locations/table.vue b/app/javascript/vue/storage_locations/table.vue index 6a176db8f..d0efc50ad 100644 --- a/app/javascript/vue/storage_locations/table.vue +++ b/app/javascript/vue/storage_locations/table.vue @@ -10,6 +10,7 @@ @create_box="openCreateBoxModal" @edit="edit" @tableReloaded="reloadingTable = false" + @move="move" /> + @@ -29,12 +33,14 @@ import DataTable from '../shared/datatable/table.vue'; import EditModal from './modals/new_edit.vue'; +import MoveModal from './modals/move.vue'; export default { name: 'RepositoriesTable', components: { DataTable, - EditModal + EditModal, + MoveModal }, props: { dataSource: { @@ -50,6 +56,9 @@ export default { }, directUploadUrl: { type: String + }, + storageLocationTreeUrl: { + type: String } }, data() { @@ -57,7 +66,9 @@ export default { reloadingTable: false, openEditModal: false, editModalMode: null, - editStorageLocation: null + editStorageLocation: null, + objectToMove: null, + moveToUrl: null }; }, computed: { @@ -169,6 +180,14 @@ export default { ${boxIcon} ${name} `; + }, + updateTable() { + this.reloadingTable = true; + this.objectToMove = null; + }, + move(event, rows) { + [this.objectToMove] = rows; + this.moveToUrl = event.path; } } }; diff --git a/app/models/storage_location.rb b/app/models/storage_location.rb index 40e3d767d..d3b5051a6 100644 --- a/app/models/storage_location.rb +++ b/app/models/storage_location.rb @@ -17,9 +17,41 @@ class StorageLocation < ApplicationRecord has_many :repository_rows, through: :storage_location_repository_row validates :name, length: { maximum: Constants::NAME_MAX_LENGTH } + validate :parent_validation, if: -> { parent.present? } after_discard do StorageLocation.where(parent_id: id).find_each(&:discard) storage_location_repository_rows.each(&:discard) end + + def self.inner_storage_locations(team, storage_location = nil) + entry_point_condition = storage_location ? 'parent_id = ?' : 'parent_id IS NULL' + + inner_storage_locations_sql = + "WITH RECURSIVE inner_storage_locations(id, selected_storage_locations_ids) AS ( + SELECT id, ARRAY[id] + FROM storage_locations + WHERE team_id = ? AND #{entry_point_condition} + UNION ALL + SELECT storage_locations.id, selected_storage_locations_ids || storage_locations.id + FROM inner_storage_locations + JOIN storage_locations ON storage_locations.parent_id = inner_storage_locations.id + WHERE NOT storage_locations.id = ANY(selected_storage_locations_ids) + ) + SELECT id FROM inner_storage_locations ORDER BY selected_storage_locations_ids".gsub(/\n|\t/, ' ').squeeze(' ') + + if storage_location.present? + where("storage_locations.id IN (#{inner_storage_locations_sql})", team.id, storage_location.id) + else + where("storage_locations.id IN (#{inner_storage_locations_sql})", team.id) + end + end + + def parent_validation + if parent.id == id + errors.add(:parent, I18n.t('activerecord.errors.models.storage_location.attributes.parent_storage_location')) + elsif StorageLocation.inner_storage_locations(team, self).exists?(id: parent_id) + errors.add(:parent, I18n.t('activerecord.errors.models.project_folder.attributes.parent_storage_location_child')) + end + end end diff --git a/app/models/team.rb b/app/models/team.rb index 4b3df88ae..311fc27bb 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -72,6 +72,7 @@ class Team < ApplicationRecord source_type: 'RepositoryBase', dependent: :destroy has_many :shareable_links, inverse_of: :team, dependent: :destroy + has_many :storage_locations, dependent: :destroy attr_accessor :without_templates diff --git a/app/services/toolbars/storage_locations_service.rb b/app/services/toolbars/storage_locations_service.rb index fe33eeeff..4fdea2743 100644 --- a/app/services/toolbars/storage_locations_service.rb +++ b/app/services/toolbars/storage_locations_service.rb @@ -47,7 +47,7 @@ module Toolbars return unless can_manage_storage_locations?(current_user.current_team) { - name: 'set_as_default', + name: 'move', label: I18n.t("storage_locations.index.toolbar.move"), icon: 'sn-icon sn-icon-move', path: move_storage_location_path(@storage_locations.first), diff --git a/app/views/storage_locations/index.html.erb b/app/views/storage_locations/index.html.erb index 564b05a72..4229ceb35 100644 --- a/app/views/storage_locations/index.html.erb +++ b/app/views/storage_locations/index.html.erb @@ -15,6 +15,7 @@ data-source="<%= storage_locations_path(format: :json, parent_id: params[:parent_id]) %>" direct-upload-url="<%= rails_direct_uploads_url %>" create-url="<%= storage_locations_path(parent_id: params[:parent_id]) if can_create_storage_locations?(current_team) %>" + storage-location-tree-url="<%= tree_storage_locations_path %>" /> diff --git a/config/locales/en.yml b/config/locales/en.yml index 7605be074..dd9cc8902 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -264,6 +264,9 @@ en: storage_location: missing_position: 'Missing position metadata' not_uniq_position: 'Position already taken' + attributes: + parent_storage_location: "Storage location cannot be parent to itself" + parent_storage_location_child: "Storage location cannot be moved to it's child" storage: limit_reached: "Storage limit has been reached." helpers: @@ -2713,6 +2716,14 @@ en: edit_box: "Box %{name} was successfully updated." errors: max_length: "is too long (maximum is %{max_length} characters)" + move_modal: + title: 'Move %{name}' + description: 'Select where you want to move %{name}.' + search_header: 'Locations' + success_flash: "You have successfully moved the selected location/box to another location." + error_flash: "An error occurred. The selected location/box has not been moved." + placeholder: + find_storage_locations: 'Find location' libraries: manange_modal_column_index: diff --git a/config/routes.rb b/config/routes.rb index 41a447ed5..8dd0e7692 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -810,6 +810,7 @@ Rails.application.routes.draw do resources :storage_locations, only: %i(index create destroy update) do collection do get :actions_toolbar + get :tree end member do post :move