alchemy_cms 6.0.0.pre.rc6 → 6.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +23 -8
  3. data/.github/workflows/stale.yml +21 -7
  4. data/.gitignore +0 -1
  5. data/.rspec +1 -0
  6. data/CHANGELOG.md +102 -0
  7. data/Gemfile +20 -5
  8. data/README.md +4 -3
  9. data/Rakefile +5 -1
  10. data/alchemy_cms.gemspec +3 -3
  11. data/app/assets/javascripts/alchemy/admin.js +0 -1
  12. data/app/assets/javascripts/alchemy/alchemy.link_dialog.js.coffee +2 -0
  13. data/app/assets/javascripts/alchemy/page_select.js +13 -8
  14. data/app/assets/javascripts/alchemy/templates/index.js +1 -0
  15. data/app/assets/javascripts/alchemy/templates/page.hbs +17 -7
  16. data/app/assets/javascripts/alchemy/templates/page_folder.hbs +3 -0
  17. data/app/assets/stylesheets/alchemy/archive.scss +9 -0
  18. data/app/assets/stylesheets/alchemy/elements.scss +4 -0
  19. data/app/assets/stylesheets/alchemy/page-select.scss +29 -4
  20. data/app/assets/stylesheets/alchemy/sitemap.scss +2 -6
  21. data/app/controllers/alchemy/admin/elements_controller.rb +20 -17
  22. data/app/controllers/alchemy/admin/pages_controller.rb +24 -19
  23. data/app/controllers/alchemy/api/base_controller.rb +4 -3
  24. data/app/controllers/alchemy/api/contents_controller.rb +1 -5
  25. data/app/controllers/alchemy/api/elements_controller.rb +2 -6
  26. data/app/controllers/alchemy/api/nodes_controller.rb +1 -0
  27. data/app/controllers/alchemy/api/pages_controller.rb +16 -10
  28. data/app/controllers/alchemy/base_controller.rb +7 -0
  29. data/app/controllers/alchemy/messages_controller.rb +0 -3
  30. data/app/controllers/alchemy/pages_controller.rb +0 -7
  31. data/app/helpers/alchemy/elements_helper.rb +17 -12
  32. data/app/models/alchemy/element.rb +13 -6
  33. data/app/models/alchemy/ingredient.rb +10 -1
  34. data/app/models/alchemy/ingredient_validator.rb +1 -1
  35. data/app/models/alchemy/language.rb +1 -1
  36. data/app/models/alchemy/page/page_elements.rb +2 -2
  37. data/app/models/alchemy/page/page_naming.rb +1 -1
  38. data/app/models/alchemy/page.rb +1 -1
  39. data/app/models/alchemy/picture/transformations.rb +2 -2
  40. data/app/models/alchemy/picture.rb +1 -1
  41. data/app/models/alchemy/picture_variant.rb +3 -1
  42. data/app/models/alchemy/site.rb +1 -1
  43. data/app/serializers/alchemy/page_serializer.rb +7 -1
  44. data/app/serializers/alchemy/page_tree_serializer.rb +3 -3
  45. data/app/views/alchemy/admin/clipboard/insert.js.erb +13 -0
  46. data/app/views/alchemy/admin/elements/_add_nested_element_form.html.erb +27 -0
  47. data/app/views/alchemy/admin/elements/_element.html.erb +1 -23
  48. data/app/views/alchemy/admin/elements/_form.html.erb +5 -1
  49. data/app/views/alchemy/admin/pages/_form.html.erb +19 -0
  50. data/app/views/alchemy/admin/pages/_new_page_form.html.erb +16 -5
  51. data/app/views/alchemy/admin/pages/_page.html.erb +111 -133
  52. data/app/views/alchemy/admin/pages/_sitemap.html.erb +2 -8
  53. data/app/views/alchemy/admin/pages/_toolbar.html.erb +0 -12
  54. data/app/views/alchemy/admin/pages/index.html.erb +1 -1
  55. data/app/views/alchemy/admin/pages/update.js.erb +7 -0
  56. data/app/views/alchemy/admin/partials/_routes.html.erb +12 -1
  57. data/app/views/alchemy/admin/resources/_form.html.erb +5 -0
  58. data/app/views/alchemy/essences/_essence_node_editor.html.erb +1 -1
  59. data/app/views/alchemy/essences/_essence_page_editor.html.erb +1 -1
  60. data/config/alchemy/config.yml +1 -0
  61. data/config/initializers/dragonfly.rb +2 -0
  62. data/config/locales/alchemy.en.yml +0 -3
  63. data/config/routes.rb +4 -2
  64. data/lib/alchemy/config.rb +5 -1
  65. data/lib/alchemy/controller_actions.rb +2 -1
  66. data/lib/alchemy/dragonfly/processors/thumbnail.rb +27 -0
  67. data/lib/alchemy/element_definition.rb +2 -3
  68. data/lib/alchemy/elements_finder.rb +1 -2
  69. data/lib/alchemy/engine.rb +12 -1
  70. data/lib/alchemy/essence.rb +1 -27
  71. data/lib/alchemy/page_layout.rb +5 -1
  72. data/lib/alchemy/permissions.rb +2 -3
  73. data/lib/alchemy/resource.rb +16 -1
  74. data/lib/alchemy/test_support/essence_shared_examples.rb +0 -12
  75. data/lib/alchemy/test_support/shared_ingredient_examples.rb +4 -2
  76. data/lib/alchemy/upgrader/tasks/ingredients_migrator.rb +1 -1
  77. data/lib/alchemy/version.rb +1 -1
  78. data/lib/alchemy.rb +2 -4
  79. data/lib/generators/alchemy/base.rb +7 -3
  80. data/lib/generators/alchemy/install/install_generator.rb +10 -2
  81. data/package/src/image_loader.js +4 -2
  82. data/package/src/node_tree.js +13 -6
  83. data/package/src/page_publication_fields.js +15 -14
  84. data/package/src/page_sorter.js +62 -0
  85. data/package/src/picture_editors.js +8 -8
  86. data/package/src/sitemap.js +51 -36
  87. data/package/src/utils/__tests__/ajax.spec.js +52 -16
  88. data/package/src/utils/ajax.js +12 -0
  89. data/package.json +1 -1
  90. metadata +43 -42
  91. data/app/assets/javascripts/alchemy/alchemy.page_sorter.js +0 -24
  92. data/app/views/alchemy/admin/pages/fold.js.erb +0 -2
  93. data/app/views/alchemy/admin/pages/sort.html.erb +0 -19
  94. data/vendor/assets/javascripts/jquery_plugins/jquery.ui.nestedSortable.js +0 -434
@@ -7,7 +7,7 @@ module Alchemy
7
7
 
8
8
  helper "alchemy/pages"
9
9
 
10
- before_action :load_resource, except: [:index, :flush, :new, :order, :create, :copy_language_tree, :link, :sort]
10
+ before_action :load_resource, except: [:index, :flush, :new, :order, :create, :copy_language_tree, :link]
11
11
 
12
12
  authorize_resource class: Alchemy::Page, except: [:index, :tree]
13
13
 
@@ -21,11 +21,9 @@ module Alchemy
21
21
  except: [:show]
22
22
 
23
23
  before_action :set_root_page,
24
- only: [:index, :show, :sort, :order]
24
+ only: [:index, :show, :order]
25
25
 
26
- before_action :run_on_page_layout_callbacks,
27
- if: :run_on_page_layout_callbacks?,
28
- only: [:show]
26
+ before_action :set_preview_mode, only: [:show]
29
27
 
30
28
  before_action :load_languages_and_layouts,
31
29
  unless: -> { @page_root },
@@ -35,6 +33,10 @@ module Alchemy
35
33
 
36
34
  before_action :set_page_version, only: [:show, :edit]
37
35
 
36
+ before_action :run_on_page_layout_callbacks,
37
+ if: :run_on_page_layout_callbacks?,
38
+ only: [:show]
39
+
38
40
  def index
39
41
  @query = @current_language.pages.contentpages.ransack(search_filter_params[:q])
40
42
 
@@ -64,7 +66,6 @@ module Alchemy
64
66
  # Used by page preview iframe in Page#edit view.
65
67
  #
66
68
  def show
67
- @preview_mode = true
68
69
  Page.current_preview = @page
69
70
  # Setting the locale to pages language, so the page content has it's correct translations.
70
71
  ::I18n.locale = @page.language.locale
@@ -123,6 +124,7 @@ module Alchemy
123
124
  # * fetches page via before filter
124
125
  #
125
126
  def update
127
+ @old_parent_id = @page.parent_id
126
128
  if @page.update(page_params)
127
129
  @notice = Alchemy.t("Page saved", name: @page.name)
128
130
  @while_page_edit = request.referer.include?("edit")
@@ -167,9 +169,7 @@ module Alchemy
167
169
  def fold
168
170
  # @page is fetched via before filter
169
171
  @page.fold!(current_alchemy_user.id, !@page.folded?(current_alchemy_user.id))
170
- respond_to do |format|
171
- format.js
172
- end
172
+ render json: serialized_page_tree
173
173
  end
174
174
 
175
175
  # Leaves the page editing mode and unlocks the page for other users
@@ -180,9 +180,12 @@ module Alchemy
180
180
  @pages_locked_by_user = Page.from_current_site.locked_by(current_alchemy_user)
181
181
  respond_to do |format|
182
182
  format.js
183
- format.html {
184
- redirect_to params[:redirect_to].blank? ? admin_pages_path : params[:redirect_to]
185
- }
183
+ format.html do
184
+ redirect_to(
185
+ params[:redirect_to].presence || admin_pages_path,
186
+ allow_other_host: true,
187
+ )
188
+ end
186
189
  end
187
190
  end
188
191
 
@@ -202,10 +205,6 @@ module Alchemy
202
205
  redirect_to admin_pages_path
203
206
  end
204
207
 
205
- def sort
206
- @sorting = true
207
- end
208
-
209
208
  # Receives a JSON object representing a language tree to be ordered
210
209
  # and updates all pages in that language structure to their correct indexes
211
210
  def order
@@ -387,9 +386,11 @@ module Alchemy
387
386
  end
388
387
 
389
388
  def serialized_page_tree
390
- PageTreeSerializer.new(@page, ability: current_ability,
391
- user: current_alchemy_user,
392
- full: params[:full] == "true")
389
+ PageTreeSerializer.new(
390
+ @page,
391
+ ability: current_ability,
392
+ user: current_alchemy_user,
393
+ )
393
394
  end
394
395
 
395
396
  def load_languages_and_layouts
@@ -397,6 +398,10 @@ module Alchemy
397
398
  @languages_with_page_tree = Language.on_current_site.with_root_page
398
399
  @page_layouts = PageLayout.layouts_for_select(@language.id)
399
400
  end
401
+
402
+ def set_preview_mode
403
+ @preview_mode = true
404
+ end
400
405
  end
401
406
  end
402
407
  end
@@ -5,17 +5,18 @@ module Alchemy
5
5
  layout false
6
6
  respond_to :json
7
7
 
8
- rescue_from CanCan::AccessDenied, with: :render_not_authorized
8
+ rescue_from CanCan::AccessDenied, with: :render_not_authorized
9
9
  rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
10
+ rescue_from ActionController::RoutingError, with: :render_not_found
10
11
 
11
12
  private
12
13
 
13
14
  def render_not_authorized
14
- render json: {error: "Not authorized"}, status: 403
15
+ render json: { error: "Not authorized" }, status: 403
15
16
  end
16
17
 
17
18
  def render_not_found
18
- render json: {error: "Record not found"}, status: 404
19
+ render json: { error: "Record not found" }, status: 404
19
20
  end
20
21
  end
21
22
  end
@@ -46,11 +46,7 @@ module Alchemy
46
46
  private
47
47
 
48
48
  def content_includes
49
- [
50
- {
51
- essence: :ingredient_association,
52
- },
53
- ]
49
+ %i[essence]
54
50
  end
55
51
  end
56
52
  end
@@ -47,18 +47,14 @@ module Alchemy
47
47
  {
48
48
  nested_elements: [
49
49
  {
50
- contents: {
51
- essence: :ingredient_association,
52
- },
50
+ contents: :essence,
53
51
  ingredients: :related_object,
54
52
  },
55
53
  :tags,
56
54
  ],
57
55
  },
58
56
  {
59
- contents: {
60
- essence: :ingredient_association,
61
- },
57
+ contents: :essence,
62
58
  ingredients: :related_object,
63
59
  },
64
60
  :tags,
@@ -8,6 +8,7 @@ module Alchemy
8
8
  def index
9
9
  @nodes = Node.all
10
10
  @nodes = @nodes.includes(:parent)
11
+ @nodes = @nodes.where(language_id: params[:language_id]) if params[:language_id]
11
12
  @nodes = @nodes.ransack(params[:filter]).result
12
13
 
13
14
  if params[:page]
@@ -2,17 +2,19 @@
2
2
 
3
3
  module Alchemy
4
4
  class Api::PagesController < Api::BaseController
5
+ serialization_scope :current_ability
5
6
  before_action :load_page, only: [:show]
6
7
 
7
8
  # Returns all pages as json object
8
9
  #
9
10
  def index
10
11
  # Fix for cancancan not able to merge multiple AR scopes for logged in users
11
- if cannot? :edit_content, Alchemy::Page
12
- @pages = Alchemy::Page.accessible_by(current_ability, :index)
13
- @pages = @pages.where(language: Language.current)
12
+ if can? :edit_content, Alchemy::Page
13
+ @pages = Alchemy::Page.all
14
14
  else
15
- @pages = Language.current&.pages.presence || Alchemy::Page.none
15
+ language = Alchemy::Language.find_by(id: params[:language_id]) || Alchemy::Language.current
16
+ @pages = Alchemy::Page.accessible_by(current_ability, :index)
17
+ @pages = @pages.where(language: language)
16
18
  end
17
19
  @pages = @pages.includes(*page_includes)
18
20
  @pages = @pages.ransack(params[:q]).result
@@ -49,6 +51,14 @@ module Alchemy
49
51
  respond_with @page
50
52
  end
51
53
 
54
+ def move
55
+ @page = Page.find(params[:id])
56
+ authorize! :update, @page
57
+ target_parent_page = Page.find(params[:target_parent_id])
58
+ @page.move_to_child_with_index(target_parent_page, params[:new_position])
59
+ render json: @page, serializer: PageSerializer
60
+ end
61
+
52
62
  private
53
63
 
54
64
  def load_page
@@ -102,17 +112,13 @@ module Alchemy
102
112
  {
103
113
  nested_elements: [
104
114
  {
105
- contents: {
106
- essence: :ingredient_association,
107
- },
115
+ contents: :essence,
108
116
  },
109
117
  :tags,
110
118
  ],
111
119
  },
112
120
  {
113
- contents: {
114
- essence: :ingredient_association,
115
- },
121
+ contents: :essence,
116
122
  },
117
123
  :tags,
118
124
  ],
@@ -9,6 +9,13 @@ module Alchemy
9
9
  include Alchemy::ControllerActions
10
10
  include Alchemy::Modules
11
11
 
12
+ # Include Turbolinks explicitly in case Alchemy is embedded into a
13
+ # larger application that doesn't work with Turbolinks. The app
14
+ # can then set config.turbolinks.auto_include = false so that
15
+ # Turbolinks is not included in the app controllers.
16
+ include Turbolinks::Controller
17
+ ::ActionDispatch::Assertions.include ::Turbolinks::Assertions
18
+
12
19
  protect_from_forgery
13
20
 
14
21
  before_action :mailer_set_url_options
@@ -62,7 +62,6 @@ module Alchemy
62
62
  end
63
63
 
64
64
  @page = @element.page
65
- @root_page = @page.get_language_root
66
65
  if @message.valid?
67
66
  MessagesMailer.contact_form_mail(@message, mail_to, mail_from, subject).deliver
68
67
  redirect_to_success_page
@@ -122,8 +121,6 @@ module Alchemy
122
121
  if @page.blank?
123
122
  raise "Page for page_layout #{mailer_config["page_layout_name"]} not found"
124
123
  end
125
-
126
- @root_page = @page.get_language_root
127
124
  end
128
125
 
129
126
  def message_params
@@ -32,9 +32,6 @@ module Alchemy
32
32
  if: :locale_prefix_missing?,
33
33
  only: [:index, :show]
34
34
 
35
- # We only need to set the +@root_page+ if we are sure that no more redirects happen.
36
- before_action :set_root_page, only: [:index, :show]
37
-
38
35
  # Page layout callbacks need to run after all other callbacks
39
36
  before_action :run_on_page_layout_callbacks,
40
37
  if: :run_on_page_layout_callbacks?,
@@ -199,10 +196,6 @@ module Alchemy
199
196
  end
200
197
  end
201
198
 
202
- def set_root_page
203
- @root_page ||= Language.current_root_page
204
- end
205
-
206
199
  def signup_required?
207
200
  if Alchemy.user_class.respond_to?(:admins)
208
201
  Alchemy.user_class.admins.empty? && @page.nil?
@@ -70,7 +70,7 @@ module Alchemy
70
70
  # A class instance that will return elements that get rendered.
71
71
  # Use this for your custom element loading logic in views.
72
72
  #
73
- def render_elements(options = {})
73
+ def render_elements(options = {}, &blk)
74
74
  options = {
75
75
  from_page: @page,
76
76
  render_format: "html",
@@ -86,11 +86,12 @@ module Alchemy
86
86
 
87
87
  elements = finder.elements(page_version: page_version)
88
88
 
89
- buff = []
90
- elements.each_with_index do |element, i|
91
- buff << render_element(element, options, i + 1)
92
- end
93
- buff.join(options[:separator]).html_safe
89
+ default_rendering = ->(element, i) { render_element(element, options, i + 1) }
90
+ if block_given?
91
+ elements.map.with_index(&blk)
92
+ else
93
+ elements.map.with_index(&default_rendering)
94
+ end.join(options[:separator]).html_safe
94
95
  end
95
96
 
96
97
  # This helper renders a {Alchemy::Element} view partial.
@@ -125,7 +126,7 @@ module Alchemy
125
126
  #
126
127
  # == Usage
127
128
  #
128
- # <%= render_element(Alchemy::Element.available.named(:headline).first) %>
129
+ # <%= render_element(Alchemy::Element.published.named(:headline).first) %>
129
130
  #
130
131
  # @param [Alchemy::Element] element
131
132
  # The element you want to render the view for
@@ -146,11 +147,15 @@ module Alchemy
146
147
 
147
148
  element.store_page(@page)
148
149
 
149
- render element, {
150
- element: element,
151
- counter: counter,
152
- options: options,
153
- }.merge(options.delete(:locals) || {})
150
+ render(
151
+ partial: options[:partial] || element.to_partial_path,
152
+ object: element,
153
+ locals: {
154
+ element: element,
155
+ counter: counter,
156
+ options: options.except(:locals, :partial),
157
+ }.merge(options[:locals] || {}),
158
+ )
154
159
  rescue ActionView::MissingTemplate => e
155
160
  warning(%(
156
161
  Element view partial not found for #{element.name}.\n
@@ -70,7 +70,7 @@ module Alchemy
70
70
  dependent: :destroy
71
71
 
72
72
  has_many :nested_elements,
73
- -> { order(:position).available },
73
+ -> { order(:position).published },
74
74
  class_name: "Alchemy::Element",
75
75
  foreign_key: :parent_element_id,
76
76
  dependent: :destroy,
@@ -159,7 +159,7 @@ module Alchemy
159
159
  end
160
160
 
161
161
  def all_from_clipboard(clipboard)
162
- return [] if clipboard.nil?
162
+ return none if clipboard.nil?
163
163
 
164
164
  where(id: clipboard.collect { |e| e["id"] })
165
165
  end
@@ -167,12 +167,19 @@ module Alchemy
167
167
  # All elements in clipboard that could be placed on page
168
168
  #
169
169
  def all_from_clipboard_for_page(clipboard, page)
170
- return [] if clipboard.nil? || page.nil?
170
+ return none if clipboard.nil? || page.nil?
171
171
 
172
- all_from_clipboard(clipboard).select { |ce|
173
- page.available_element_names.include?(ce.name)
174
- }
172
+ all_from_clipboard(clipboard).where(name: page.available_element_names)
175
173
  end
174
+
175
+ # All elements in clipboard that could be placed as a child of `parent_element`
176
+ def all_from_clipboard_for_parent_element(clipboard, parent_element)
177
+ return none if clipboard.nil? || parent_element.nil?
178
+
179
+ all_from_clipboard(clipboard).where(name: parent_element.definition["nestable_elements"])
180
+ end
181
+
182
+ deprecate available: :published, deprecator: Alchemy::Deprecation
176
183
  end
177
184
 
178
185
  # Returns next public element from same page.
@@ -6,12 +6,17 @@ module Alchemy
6
6
 
7
7
  include Hints
8
8
 
9
+ # MariaDB needs to be told explicitly to use `data` as a JSON store. All other databases
10
+ # can do this natively.
11
+ store :data, coder: JSON
12
+
9
13
  self.table_name = "alchemy_ingredients"
10
14
 
11
15
  belongs_to :element, touch: true, class_name: "Alchemy::Element", inverse_of: :ingredients
12
16
  belongs_to :related_object, polymorphic: true, optional: true
13
17
 
14
- before_validation(on: :create) { self.value ||= default_value }
18
+ after_initialize :set_default_value,
19
+ if: -> { definition.key?(:default) && value.nil? }
15
20
 
16
21
  validates :type, presence: true
17
22
  validates :role, presence: true
@@ -161,6 +166,10 @@ module Alchemy
161
166
  role
162
167
  end
163
168
 
169
+ def set_default_value
170
+ self.value = default_value
171
+ end
172
+
164
173
  # Returns the default value from ingredient definition
165
174
  #
166
175
  # If the value is a symbol it gets passed through i18n
@@ -88,7 +88,7 @@ module Alchemy
88
88
 
89
89
  def duplicates
90
90
  ingredient.class
91
- .joins(:element).merge(Alchemy::Element.available)
91
+ .joins(:element).merge(Alchemy::Element.published)
92
92
  .where(Alchemy::Element.table_name => { name: ingredient.element.name })
93
93
  .where(value: ingredient.value)
94
94
  .where.not(id: ingredient.id)
@@ -38,7 +38,7 @@ module Alchemy
38
38
 
39
39
  validates :language_code,
40
40
  presence: true,
41
- uniqueness: { scope: [:site_id, :country_code] },
41
+ uniqueness: { scope: [:site_id, :country_code], case_sensitive: false },
42
42
  format: { with: /\A[a-z]{2}\z/, if: -> { language_code.present? } }
43
43
 
44
44
  validates :country_code,
@@ -15,8 +15,8 @@ module Alchemy
15
15
  source: :elements,
16
16
  ) do
17
17
  has_many :all_elements
18
- has_many :elements, -> { not_nested.unfixed.available }
19
- has_many :fixed_elements, -> { fixed.available }
18
+ has_many :elements, -> { not_nested.unfixed.published }
19
+ has_many :fixed_elements, -> { fixed.published }
20
20
  end
21
21
 
22
22
  has_many :contents, through: :elements
@@ -15,7 +15,7 @@ module Alchemy
15
15
  validates :name,
16
16
  presence: true
17
17
  validates :urlname,
18
- uniqueness: { scope: [:language_id, :layoutpage], if: -> { urlname.present? } },
18
+ uniqueness: { scope: [:language_id, :layoutpage], if: -> { urlname.present? }, case_sensitive: false },
19
19
  exclusion: { in: RESERVED_URLNAMES },
20
20
  length: { minimum: 3, if: -> { urlname.present? } }
21
21
 
@@ -117,7 +117,7 @@ module Alchemy
117
117
  has_many :nodes, class_name: "Alchemy::Node", inverse_of: :page
118
118
  has_many :versions, class_name: "Alchemy::PageVersion", inverse_of: :page, dependent: :destroy
119
119
  has_one :draft_version, -> { drafts }, class_name: "Alchemy::PageVersion"
120
- has_one :public_version, -> { published }, class_name: "Alchemy::PageVersion"
120
+ has_one :public_version, -> { published }, class_name: "Alchemy::PageVersion", autosave: -> { persisted? }
121
121
 
122
122
  before_validation :set_language,
123
123
  if: -> { language.nil? }
@@ -30,7 +30,7 @@ module Alchemy
30
30
  # Returns the rendered resized image using imagemagick directly.
31
31
  #
32
32
  def resize(size, upsample = false)
33
- image_file.thumb(upsample ? size : "#{size}>")
33
+ image_file.thumbnail(upsample ? size : "#{size}>")
34
34
  end
35
35
 
36
36
  # Returns true if picture's width is greater than it's height
@@ -119,7 +119,7 @@ module Alchemy
119
119
  if is_smaller_than?(dimensions) && upsample == false
120
120
  dimensions = reduce_to_image(dimensions)
121
121
  end
122
- image_file.thumb("#{dimensions_to_string(dimensions)}#")
122
+ image_file.thumbnail("#{dimensions_to_string(dimensions)}#")
123
123
  end
124
124
 
125
125
  # Use imagemagick to custom crop an image. Uses -thumbnail for better performance when resizing.
@@ -28,7 +28,7 @@ module Alchemy
28
28
  large: "240x180",
29
29
  }.with_indifferent_access.freeze
30
30
 
31
- CONVERTIBLE_FILE_FORMATS = %w(gif jpg jpeg png).freeze
31
+ CONVERTIBLE_FILE_FORMATS = %w[gif jpg jpeg png webp].freeze
32
32
 
33
33
  TRANSFORMATION_OPTIONS = [
34
34
  :crop,
@@ -13,6 +13,8 @@ module Alchemy
13
13
  include Alchemy::Logger
14
14
  include Alchemy::Picture::Transformations
15
15
 
16
+ ANIMATED_IMAGE_FORMATS = %w[gif webp]
17
+
16
18
  attr_reader :picture, :render_format
17
19
 
18
20
  def_delegators :@picture,
@@ -86,7 +88,7 @@ module Alchemy
86
88
  end
87
89
 
88
90
  options = {
89
- flatten: render_format != "gif" && picture.image_file_format == "gif",
91
+ flatten: !render_format.in?(ANIMATED_IMAGE_FORMATS) && picture.image_file_format == "gif",
90
92
  }.with_indifferent_access.merge(options)
91
93
 
92
94
  encoding_options = []
@@ -18,7 +18,7 @@ module Alchemy
18
18
  class Site < BaseRecord
19
19
  # validations
20
20
  validates_presence_of :host
21
- validates_uniqueness_of :host
21
+ validates_uniqueness_of :host, case_sensitive: false
22
22
 
23
23
  # associations
24
24
  has_many :languages
@@ -14,8 +14,14 @@ module Alchemy
14
14
  :created_at,
15
15
  :updated_at,
16
16
  :status,
17
- :url_path
17
+ :url_path,
18
+ :parent_id
18
19
 
19
20
  has_many :elements
21
+
22
+ with_options if: -> { scope.can?(:edit_content, object) } do
23
+ belongs_to :site
24
+ belongs_to :language
25
+ end
20
26
  end
21
27
  end
@@ -3,13 +3,13 @@
3
3
  module Alchemy
4
4
  class PageTreeSerializer < BaseSerializer
5
5
  def attributes
6
- {"pages" => nil}
6
+ { "pages" => nil }
7
7
  end
8
8
 
9
9
  def pages
10
10
  tree = []
11
- path = [{id: object.parent_id, children: tree}]
12
- page_list = object.self_and_descendants
11
+ path = [{ id: object.parent_id, children: tree }]
12
+ page_list = object.self_and_descendants.includes(:public_version, { language: :site })
13
13
  base_level = object.level - 1
14
14
  # Load folded pages in advance
15
15
  folded_user_pages = FoldedPage.folded_for_user(opts[:user]).pluck(:page_id)
@@ -14,3 +14,16 @@
14
14
  Alchemy.growl('<%= j Alchemy.t("item copied to clipboard", name: @item.class.name == "Alchemy::Element" ? @item.display_name_with_preview_text : @item.name) %>')
15
15
  <% end -%>
16
16
  $('#clipboard_button .icon').removeClass('fa-clipboard').addClass('fa-paste');
17
+
18
+ <%# Update add nested element forms for any elements that accept ONLY this as a nested element %>
19
+ <% if @item.class == Alchemy::Element %>
20
+ if (window.location.pathname == "<%= edit_admin_page_path(@item.page.id) %>") {
21
+ <%
22
+ @item.page.draft_version.elements.expanded.select do |element|
23
+ element.definition["nestable_elements"] == [@item.name]
24
+ end.each do |element|
25
+ %>
26
+ $(".add-nested-element[data-element-id='<%= element.id %>']").replaceWith('<%= j render "alchemy/admin/elements/add_nested_element_form", element: element %>')
27
+ <% end %>
28
+ }
29
+ <% end %>
@@ -0,0 +1,27 @@
1
+ <%= content_tag :div, class: 'add-nested-element', data: { element_id: element.id } do %>
2
+ <% if element.expanded? || element.fixed? %>
3
+ <% if element.nestable_elements.length == 1 &&
4
+ (nestable_element = element.nestable_elements.first) &&
5
+ Alchemy::Element.all_from_clipboard_for_parent_element(get_clipboard("elements"), element).none?
6
+ %>
7
+ <%= form_for [:admin, Alchemy::Element.new(name: nestable_element)],
8
+ remote: true, html: { class: 'add-nested-element-form', id: nil } do |f| %>
9
+ <%= f.hidden_field :name %>
10
+ <%= f.hidden_field :page_version_id, value: element.page_version_id %>
11
+ <%= f.hidden_field :parent_element_id, value: element.id %>
12
+ <button class="button add-nestable-element-button" data-alchemy-button>
13
+ <%= Alchemy.t(:add_nested_element, name: Alchemy.t(nestable_element, scope: 'element_names')) %>
14
+ </button>
15
+ <% end %>
16
+ <% else %>
17
+ <%= link_to_dialog (nestable_element ? Alchemy.t(:add_nested_element, name: Alchemy.t(nestable_element, scope: 'element_names')) : Alchemy.t("New Element")),
18
+ alchemy.new_admin_element_path(
19
+ parent_element_id: element.id,
20
+ page_version_id: element.page_version_id
21
+ ), {
22
+ size: "320x125",
23
+ title: Alchemy.t("New Element")
24
+ }, class: "button add-nestable-element-button" %>
25
+ <% end %>
26
+ <% end %>
27
+ <% end %>
@@ -55,29 +55,7 @@
55
55
  } %>
56
56
  <% end %>
57
57
 
58
- <% if element.expanded? || element.fixed? %>
59
- <% if element.nestable_elements.length == 1 %>
60
- <% nestable_element = element.nestable_elements.first %>
61
- <%= form_for [:admin, Alchemy::Element.new(name: nestable_element)],
62
- remote: true, html: { class: 'add-nested-element-form', id: nil } do |f| %>
63
- <%= f.hidden_field :name %>
64
- <%= f.hidden_field :page_version_id, value: element.page_version_id %>
65
- <%= f.hidden_field :parent_element_id, value: element.id %>
66
- <button class="button add-nestable-element-button" data-alchemy-button>
67
- <%= Alchemy.t(:add_nested_element) % { name: Alchemy.t(nestable_element, scope: 'element_names') } %>
68
- </button>
69
- <% end %>
70
- <% else %>
71
- <%= link_to_dialog Alchemy.t("New Element"),
72
- alchemy.new_admin_element_path(
73
- parent_element_id: element.id,
74
- page_version_id: element.page_version_id
75
- ), {
76
- size: "320x125",
77
- title: Alchemy.t("New Element")
78
- }, class: "button add-nestable-element-button" %>
79
- <% end %>
80
- <% end %>
58
+ <%= render "alchemy/admin/elements/add_nested_element_form", element: element %>
81
59
  </div>
82
60
  <% end %>
83
61
  <% end %>
@@ -9,7 +9,11 @@
9
9
  label: Alchemy.t(:element_of_type),
10
10
  collection: elements_for_select(@elements),
11
11
  prompt: Alchemy.t(:select_element),
12
- input_html: {class: 'alchemy_selectbox', autofocus: true} %>
12
+ selected: (@elements.first if @elements.count == 1),
13
+ input_html: {class: 'alchemy_selectbox', autofocus: true, disabled: @elements.count == 1} %>
14
+ <% if @elements.count == 1 %>
15
+ <%= form.hidden_field :name, value: @elements.first[:name] %>
16
+ <% end %>
13
17
  <%= form.hidden_field :parent_element_id, value: @parent_element.try(:id) %>
14
18
  <%= form.submit Alchemy.t(:add) %>
15
19
  <%- end -%>