AlchemyCMS/alchemy_cms

View on GitHub
app/controllers/alchemy/admin/elements_controller.rb

Summary

Maintainability
A
35 mins
Test Coverage
A
100%
# frozen_string_literal: true

module Alchemy
  module Admin
    class ElementsController < Alchemy::Admin::BaseController
      before_action :load_element, only: [:update, :destroy, :collapse, :expand, :publish]
      authorize_resource class: Alchemy::Element

      def index
        @page_version = PageVersion.find(params[:page_version_id])
        @page = @page_version.page
        elements = @page_version.elements.order(:position).includes(*element_includes)
        @elements = elements.not_nested.unfixed
        @fixed_elements = elements.not_nested.fixed
        load_clipboard_items
      end

      def new
        @page_version = PageVersion.find(params[:page_version_id])
        @page = @page_version.page
        @parent_element = Element.find_by(id: params[:parent_element_id])
        @elements = @page.available_elements_within_current_scope(@parent_element)
        @element = @page_version.elements.build
        load_clipboard_items
      end

      # Creates a element as discribed in config/alchemy/elements.yml on page via AJAX.
      def create
        @page_version = PageVersion.find(params[:element][:page_version_id])
        @page = @page_version.page
        Element.transaction do
          @paste_from_clipboard = params[:paste_from_clipboard].present?
          @element = if @paste_from_clipboard
            paste_element_from_clipboard
          else
            Element.new(create_element_params)
          end
          if @page.definition["insert_elements_at"] == "top"
            @insert_at_top = true
            @element.position = 1
          end
        end
        if @element.save
          render :create
        else
          @element.page_version = @page_version
          @elements = @page.available_element_definitions
          load_clipboard_items
          render :new
        end
      end

      # Updates the element and all ingredients in the element.
      #
      def update
        if @element.update(element_params)
          render json: {
            notice: Alchemy.t(:element_saved),
            previewText: Rails::Html::SafeListSanitizer.new.sanitize(@element.preview_text),
            ingredientAnchors: @element.ingredients.select { |i| i.settings[:anchor] }.map do |ingredient|
              {
                ingredientId: ingredient.id,
                active: ingredient.dom_id.present?
              }
            end
          }
        else
          @warning = Alchemy.t("Validation failed")
          render json: {
            warning: @warning,
            errorMessage: Alchemy.t(:ingredient_validations_headline),
            ingredientsWithErrors: @element.ingredients_with_errors.map(&:id),
            errors: @element.ingredient_error_messages
          }
        end
      end

      def destroy
        @element.destroy

        render json: {
          message: Alchemy.t("Successfully deleted element") % {element: @element.display_name}
        }
      end

      def publish
        @element.public = !@element.public?
        @element.save(validate: false)
        render json: {
          public: @element.public?,
          label: @element.public? ? Alchemy.t(:hide_element) : Alchemy.t(:show_element)
        }
      end

      def order
        @element = Element.find(params[:element_id])
        @element.update(
          parent_element_id: params[:parent_element_id],
          position: params[:position]
        )
        if params[:parent_element_id].present?
          @parent_element = Element.find_by(id: params[:parent_element_id])
        end

        render json: {
          message: Alchemy.t(:successfully_saved_element_position),
          preview_text: @element.preview_text
        }
      end

      # Collapses the element, all nested elements and persists the state in the db
      #
      def collapse
        # We do not want to trigger the touch callback or any validations
        @element.update_columns(folded: true)
        # Collapse all nested elements
        nested_elements_ids = collapse_nested_elements_ids(@element)
        Alchemy::Element.where(id: nested_elements_ids).update_all(folded: true)

        render json: {
          nestedElementIds: nested_elements_ids,
          title: Alchemy.t(@element.folded? ? :show_element_content : :hide_element_content)
        }
      end

      # Expands the element, all parents and persists the state in the db
      #
      def expand
        # We do not want to trigger the touch callback or any validations
        @element.update_columns(folded: false)
        # We want to expand the upper most parent first in order to prevent
        # re-painting issues in the browser
        parent_element_ids = @element.parent_element_ids.reverse
        Alchemy::Element.where(id: parent_element_ids).update_all(folded: false)

        render json: {
          parentElementIds: parent_element_ids,
          title: Alchemy.t(@element.folded? ? :show_element_content : :hide_element_content)
        }
      end

      private

      def collapse_nested_elements_ids(element)
        ids = []
        element.all_nested_elements.includes(:all_nested_elements).reject(&:compact?).each do |nested_element|
          ids.push nested_element.id if nested_element.expanded?
          ids.concat collapse_nested_elements_ids(nested_element) if nested_element.all_nested_elements.reject(&:compact?).any?
        end
        ids
      end

      def element_includes
        [
          {
            ingredients: :related_object
          },
          :tags,
          {
            all_nested_elements: [
              {
                ingredients: :related_object
              },
              :tags
            ]
          }
        ]
      end

      def load_element
        @element = Element.find(params[:id])
      end

      def load_clipboard_items
        @clipboard = get_clipboard("elements")
        @clipboard_items = Element.all_from_clipboard_for_page(@clipboard, @page)
      end

      def element_from_clipboard
        @element_from_clipboard ||= begin
          @clipboard = get_clipboard("elements")
          @clipboard.detect { |item| item["id"].to_i == params[:paste_from_clipboard].to_i }
        end
      end

      def paste_element_from_clipboard
        @source_element = Element.find(element_from_clipboard["id"])
        element = Element.copy(
          @source_element,
          {
            parent_element_id: create_element_params[:parent_element_id],
            page_version_id: @page_version.id
          }
        )
        if element_from_clipboard["action"] == "cut"
          @cut_element_id = @source_element.id
          @clipboard.delete_if { |item| item["id"] == @source_element.id.to_s }
          @source_element.destroy
        end
        element
      end

      def element_params
        params.fetch(:element, {}).permit(:tag_list, ingredients_attributes: {})
      end

      def create_element_params
        params.require(:element).permit(:name, :page_version_id, :parent_element_id)
      end
    end
  end
end