alchemy_cms 6.0.12 → 6.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of alchemy_cms might be problematic. Click here for more details.

Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/brakeman-analysis.yml +26 -26
  3. data/.github/workflows/ci.yml +1 -1
  4. data/.github/workflows/stale.yml +2 -0
  5. data/CHANGELOG.md +45 -0
  6. data/Gemfile +2 -2
  7. data/alchemy_cms.gemspec +1 -1
  8. data/app/assets/javascripts/alchemy/alchemy.link_dialog.js.coffee +13 -11
  9. data/app/assets/stylesheets/alchemy/_variables.scss +9 -4
  10. data/app/assets/stylesheets/alchemy/elements.scss +96 -4
  11. data/app/assets/stylesheets/alchemy/forms.scss +59 -17
  12. data/app/assets/stylesheets/tinymce/skins/alchemy/content.min.css.scss +3 -3
  13. data/app/controllers/alchemy/admin/ingredients_controller.rb +1 -1
  14. data/app/controllers/alchemy/admin/pages_controller.rb +3 -3
  15. data/app/controllers/alchemy/api/ingredients_controller.rb +22 -0
  16. data/app/controllers/alchemy/messages_controller.rb +1 -1
  17. data/app/decorators/alchemy/ingredient_editor.rb +4 -0
  18. data/app/helpers/alchemy/url_helper.rb +0 -8
  19. data/app/models/alchemy/element/definitions.rb +16 -2
  20. data/app/models/alchemy/element/dom_id.rb +30 -0
  21. data/app/models/alchemy/element/element_contents.rb +39 -0
  22. data/app/models/alchemy/element/element_essences.rb +9 -1
  23. data/app/models/alchemy/element/element_ingredients.rb +7 -0
  24. data/app/models/alchemy/element/presenters.rb +1 -1
  25. data/app/models/alchemy/element.rb +14 -0
  26. data/app/models/alchemy/ingredients/headline.rb +4 -1
  27. data/app/models/alchemy/ingredients/text.rb +3 -0
  28. data/app/models/alchemy/page/page_layouts.rb +128 -0
  29. data/app/models/alchemy/page.rb +5 -2
  30. data/app/models/alchemy/picture_thumb/create.rb +15 -3
  31. data/app/models/concerns/alchemy/dom_ids.rb +32 -0
  32. data/app/serializers/alchemy/ingredient_serializer.rb +11 -0
  33. data/app/views/alchemy/admin/elements/update.js.erb +3 -0
  34. data/app/views/alchemy/admin/ingredients/_dom_id_fields.html.erb +4 -0
  35. data/app/views/alchemy/admin/ingredients/_headline_fields.html.erb +3 -0
  36. data/app/views/alchemy/admin/ingredients/_text_fields.html.erb +3 -0
  37. data/app/views/alchemy/admin/ingredients/update.js.erb +7 -0
  38. data/app/views/alchemy/admin/languages/_form.html.erb +1 -1
  39. data/app/views/alchemy/admin/languages/_language.html.erb +1 -1
  40. data/app/views/alchemy/admin/pages/_form.html.erb +9 -0
  41. data/app/views/alchemy/admin/pages/_page_layout_filter.html.erb +1 -1
  42. data/app/views/alchemy/admin/partials/_routes.html.erb +2 -1
  43. data/app/views/alchemy/ingredients/_headline_editor.html.erb +22 -20
  44. data/app/views/alchemy/ingredients/_headline_view.html.erb +1 -0
  45. data/app/views/alchemy/ingredients/_text_editor.html.erb +3 -0
  46. data/app/views/alchemy/ingredients/_text_view.html.erb +7 -3
  47. data/app/views/alchemy/ingredients/shared/_anchor.html.erb +9 -0
  48. data/app/views/alchemy/messages_mailer/contact_form_mail.de.text.erb +2 -0
  49. data/app/views/alchemy/messages_mailer/contact_form_mail.en.text.erb +2 -0
  50. data/app/views/alchemy/messages_mailer/contact_form_mail.es.text.erb +2 -0
  51. data/config/locales/alchemy.en.yml +7 -2
  52. data/config/routes.rb +1 -0
  53. data/db/migrate/20230123112425_add_searchable_to_alchemy_pages.rb +9 -0
  54. data/lib/alchemy/deprecation.rb +1 -1
  55. data/lib/alchemy/error_tracking/error_logger.rb +13 -0
  56. data/lib/alchemy/error_tracking.rb +3 -1
  57. data/lib/alchemy/page_layout.rb +0 -113
  58. data/lib/alchemy/test_support/shared_dom_ids_examples.rb +119 -0
  59. data/lib/alchemy/upgrader/tasks/ingredients_migrator.rb +7 -5
  60. data/lib/alchemy/version.rb +1 -1
  61. data/lib/alchemy.rb +17 -0
  62. data/package/admin.js +2 -0
  63. data/package/src/ingredient_anchor_link.js +17 -0
  64. data/package.json +2 -2
  65. data/vendor/assets/javascripts/tinymce/tinymce.min.js +2 -2
  66. metadata +19 -5
@@ -8,19 +8,33 @@ module Alchemy
8
8
  extend ActiveSupport::Concern
9
9
 
10
10
  module ClassMethods
11
+ # Register a custom element definitions repository
12
+ #
13
+ # The default repository is Alchemy::ElementDefinition
14
+ #
15
+ def definitions_repository=(klass)
16
+ @_definitions_repository = klass
17
+ end
18
+
11
19
  # Returns the definitions from elements.yml file.
12
20
  #
13
21
  # Place a +elements.yml+ file inside your apps +config/alchemy+ folder to define
14
22
  # your own set of elements
15
23
  #
16
24
  def definitions
17
- ElementDefinition.all
25
+ definitions_repository.all
18
26
  end
19
27
 
20
28
  # Returns one element definition by given name.
21
29
  #
22
30
  def definition_by_name(name)
23
- ElementDefinition.get(name)
31
+ definitions_repository.get(name)
32
+ end
33
+
34
+ private
35
+
36
+ def definitions_repository
37
+ @_definitions_repository ||= ElementDefinition
24
38
  end
25
39
  end
26
40
 
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ # Returns a dom id used for elements html id tag.
5
+ #
6
+ # Uses the elements name and its position on the page.
7
+ # If the element is nested in a parent element it prefixes
8
+ # the id with the parent elements dom_id.
9
+ #
10
+ # Register your own dom id class with
11
+ #
12
+ # Alchemy::Element.dom_id_class = MyDomIdClass
13
+ #
14
+ class Element < BaseRecord
15
+ class DomId
16
+ def initialize(element)
17
+ @element = element
18
+ @parent_element = element.parent_element
19
+ end
20
+
21
+ def call
22
+ [parent_element&.dom_id, element.name, element.position].compact.join("-")
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :element, :parent_element
28
+ end
29
+ end
30
+ end
@@ -6,29 +6,41 @@ module Alchemy
6
6
  #
7
7
  module ElementContents
8
8
  # Find first content from element by given name.
9
+ # @deprecated
9
10
  def content_by_name(name)
10
11
  contents_by_name(name).first
11
12
  end
12
13
 
14
+ deprecate content_by_name: :ingredient_by_role, deprecator: Alchemy::Deprecation
15
+
13
16
  # Find first content from element by given essence type.
17
+ # @deprecated
14
18
  def content_by_type(essence_type)
15
19
  contents_by_type(essence_type).first
16
20
  end
17
21
 
22
+ deprecate content_by_type: :ingredient_by_type, deprecator: Alchemy::Deprecation
23
+
18
24
  # All contents from element by given name.
25
+ # @deprecated
19
26
  def contents_by_name(name)
20
27
  contents.select { |content| content.name == name.to_s }
21
28
  end
22
29
 
30
+ deprecate contents_by_name: :ingredients_by_role, deprecator: Alchemy::Deprecation
31
+
23
32
  alias_method :all_contents_by_name, :contents_by_name
24
33
 
25
34
  # All contents from element by given essence type.
35
+ # @deprecated
26
36
  def contents_by_type(essence_type)
27
37
  contents.select do |content|
28
38
  content.essence_type == Content.normalize_essence_type(essence_type)
29
39
  end
30
40
  end
31
41
 
42
+ deprecate contents_by_type: :ingredients_by_type, deprecator: Alchemy::Deprecation
43
+
32
44
  alias_method :all_contents_by_type, :contents_by_type
33
45
 
34
46
  # Updates all related contents by calling +update_essence+ on each of them.
@@ -48,6 +60,7 @@ module Alchemy
48
60
  # "2" => {link: "https://google.com"}
49
61
  # )
50
62
  #
63
+ # @deprecated
51
64
  def update_contents(contents_attributes)
52
65
  return true if contents_attributes.nil?
53
66
 
@@ -58,13 +71,18 @@ module Alchemy
58
71
  errors.blank?
59
72
  end
60
73
 
74
+ deprecate :update_contents, deprecator: Alchemy::Deprecation
75
+
61
76
  # Copy current content's contents to given target element
77
+ # @deprecated
62
78
  def copy_contents_to(element)
63
79
  contents.map do |content|
64
80
  Content.copy(content, element_id: element.id)
65
81
  end
66
82
  end
67
83
 
84
+ deprecate :copy_contents_to, deprecator: Alchemy::Deprecation
85
+
68
86
  # Returns the content that is marked as rss title.
69
87
  #
70
88
  # Mark a content as rss title in your +elements.yml+ file:
@@ -75,10 +93,13 @@ module Alchemy
75
93
  # type: EssenceText
76
94
  # rss_title: true
77
95
  #
96
+ # @deprecated
78
97
  def content_for_rss_title
79
98
  content_for_rss_meta("title")
80
99
  end
81
100
 
101
+ deprecate :content_for_rss_title, deprecator: Alchemy::Deprecation
102
+
82
103
  # Returns the content that is marked as rss description.
83
104
  #
84
105
  # Mark a content as rss description in your +elements.yml+ file:
@@ -89,18 +110,25 @@ module Alchemy
89
110
  # type: EssenceRichtext
90
111
  # rss_description: true
91
112
  #
113
+ # @deprecated
92
114
  def content_for_rss_description
93
115
  content_for_rss_meta("description")
94
116
  end
95
117
 
118
+ deprecate :content_for_rss_description, deprecator: Alchemy::Deprecation
119
+
96
120
  # Returns the array with the hashes for all element contents in the elements.yml file
121
+ # @deprecated
97
122
  def content_definitions
98
123
  return nil if definition.blank?
99
124
 
100
125
  definition["contents"]
101
126
  end
102
127
 
128
+ deprecate content_definitions: :ingredient_definitions, deprecator: Alchemy::Deprecation
129
+
103
130
  # Returns the definition for given content_name
131
+ # @deprecated
104
132
  def content_definition_for(content_name)
105
133
  if content_definitions.blank?
106
134
  log_warning "Element #{name} is missing the content definition for #{content_name}"
@@ -110,10 +138,13 @@ module Alchemy
110
138
  end
111
139
  end
112
140
 
141
+ deprecate content_definition_for: :ingredient_definition_for, deprecator: Alchemy::Deprecation
142
+
113
143
  # Returns an array of all EssenceRichtext contents ids from elements
114
144
  #
115
145
  # This is used to re-initialize the TinyMCE editor in the element editor.
116
146
  #
147
+ # @deprecated
117
148
  def richtext_contents_ids
118
149
  # This is not very efficient SQL wise I know, but we need to iterate
119
150
  # recursivly through all descendent elements and I don't know how to do this
@@ -126,16 +157,24 @@ module Alchemy
126
157
  ids.flatten
127
158
  end
128
159
 
160
+ deprecate richtext_contents_ids: :richtext_ingredients_ids, deprecator: Alchemy::Deprecation
161
+
129
162
  # True, if any of the element's contents has essence validations defined.
163
+ # @deprecated
130
164
  def has_validations?
131
165
  !contents.detect(&:has_validations?).blank?
132
166
  end
133
167
 
168
+ deprecate :has_validations?, deprecator: Alchemy::Deprecation
169
+
134
170
  # All element contents where the essence validation has failed.
171
+ # @deprecated
135
172
  def contents_with_errors
136
173
  contents.select(&:essence_validation_failed?)
137
174
  end
138
175
 
176
+ deprecate contents_with_errors: :ingredients_with_errors, deprecator: Alchemy::Deprecation
177
+
139
178
  private
140
179
 
141
180
  def content_for_rss_meta(type)
@@ -7,7 +7,7 @@ module Alchemy
7
7
  def ingredient(name)
8
8
  ing = ingredient_by_role(name)
9
9
  if ing
10
- Alchemy::Deprecation.warn <<~WARN
10
+ Alchemy::Deprecation.warn(<<~WARN)
11
11
  Using `element.ingredient` to get the value of an ingredient is deprecated and will change in Alchemy 6.1
12
12
  If you want to read the value of an elements ingredient please use `element.value_for(:ingredient_role)` instead.
13
13
  The next version of Alchemy will return a `Alchemy::Ingredient` record instead.
@@ -23,9 +23,11 @@ module Alchemy
23
23
 
24
24
  # True if the element has a content for given name,
25
25
  # that has an essence value (aka. ingredient) that is not blank.
26
+ # @deprecated
26
27
  def has_ingredient?(name)
27
28
  ingredient(name).present?
28
29
  end
30
+
29
31
  deprecate has_ingredient?: :has_value_for?, deprecator: Alchemy::Deprecation
30
32
 
31
33
  # Returns all essence errors in the format of:
@@ -39,6 +41,7 @@ module Alchemy
39
41
  #
40
42
  # Get translated error messages with +Element#essence_error_messages+
41
43
  #
44
+ # @deprecated
42
45
  def essence_errors
43
46
  essence_errors = {}
44
47
  contents.each do |content|
@@ -49,6 +52,8 @@ module Alchemy
49
52
  essence_errors
50
53
  end
51
54
 
55
+ deprecate :essence_errors, deprecator: Alchemy::Deprecation
56
+
52
57
  # Essence validation errors
53
58
  #
54
59
  # == Error messages are translated via I18n
@@ -103,6 +108,7 @@ module Alchemy
103
108
  # invalid: %{field} has wrong format
104
109
  # blank: %{field} can't be blank
105
110
  #
111
+ # @deprecated
106
112
  def essence_error_messages
107
113
  messages = []
108
114
  essence_errors.each do |content_name, errors|
@@ -120,6 +126,8 @@ module Alchemy
120
126
  end
121
127
  messages
122
128
  end
129
+
130
+ deprecate essence_error_messages: :ingredient_error_messages, deprecator: Alchemy::Deprecation
123
131
  end
124
132
  end
125
133
  end
@@ -37,6 +37,13 @@ module Alchemy
37
37
  ingredients_by_type(type).first
38
38
  end
39
39
 
40
+ # All ingredients from element by given role.
41
+ def ingredients_by_role(role)
42
+ ingredients.select do |ingredient|
43
+ ingredient.role == Ingredient.normalize_type(role)
44
+ end
45
+ end
46
+
40
47
  # All ingredients from element by given type.
41
48
  def ingredients_by_type(type)
42
49
  ingredients.select do |ingredient|
@@ -82,7 +82,7 @@ module Alchemy
82
82
  # Returns a dom id used for elements html id tag.
83
83
  #
84
84
  def dom_id
85
- [parent_element&.dom_id, name, position].compact.join("-")
85
+ self.class.dom_id_class.new(self).call
86
86
  end
87
87
 
88
88
  # The content that's used for element's preview text.
@@ -61,6 +61,8 @@ module Alchemy
61
61
 
62
62
  has_many :contents, dependent: :destroy, inverse_of: :element
63
63
 
64
+ deprecate contents: :ingredients, deprecator: Alchemy::Deprecation
65
+
64
66
  before_destroy :delete_all_nested_elements
65
67
 
66
68
  has_many :all_nested_elements,
@@ -143,6 +145,18 @@ module Alchemy
143
145
  super(element_definition.merge(element_attributes).except(*FORBIDDEN_DEFINITION_ATTRIBUTES))
144
146
  end
145
147
 
148
+ # The class responsible for the +dom_id+ of elements.
149
+ # Defaults to +Alchemy::Element::DomId+.
150
+ def dom_id_class
151
+ @_dom_id_class || DomId
152
+ end
153
+
154
+ # Register a custom +DomId+ class responsible for the +dom_id+ of elements.
155
+ # Defaults to +Alchemy::Element::DomId+.
156
+ def dom_id_class=(klass)
157
+ @_dom_id_class = klass
158
+ end
159
+
146
160
  # This methods does a copy of source and all depending contents and all of their depending essences.
147
161
  #
148
162
  # == Options
@@ -5,7 +5,10 @@ module Alchemy
5
5
  # A text headline
6
6
  #
7
7
  class Headline < Alchemy::Ingredient
8
+ include DomIds
9
+
8
10
  store_accessor :data,
11
+ :dom_id,
9
12
  :level,
10
13
  :size
11
14
 
@@ -20,7 +23,7 @@ module Alchemy
20
23
  end
21
24
 
22
25
  def size_options
23
- sizes.map { |size| ["H#{size}", size] }
26
+ sizes.map { |size| [".h#{size}", size] }
24
27
  end
25
28
 
26
29
  private
@@ -7,7 +7,10 @@ module Alchemy
7
7
  # Optionally it can have a link
8
8
  #
9
9
  class Text < Alchemy::Ingredient
10
+ include DomIds
11
+
10
12
  store_accessor :data,
13
+ :dom_id,
11
14
  :link,
12
15
  :link_target,
13
16
  :link_title,
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ class Page < BaseRecord
5
+ # Module concerning page layouts
6
+ #
7
+ module PageLayouts
8
+ extend ActiveSupport::Concern
9
+
10
+ module ClassMethods
11
+ # Register a custom page layouts repository
12
+ #
13
+ # The default repository is Alchemy::PageLayout
14
+ #
15
+ def layouts_repository=(klass)
16
+ @_layouts_repository = klass
17
+ end
18
+
19
+ # Returns page layouts ready for Rails' select form helper.
20
+ #
21
+ def layouts_for_select(language_id, layoutpages: false)
22
+ @map_array = []
23
+ mapped_layouts_for_select(selectable_layouts(language_id, layoutpages: layoutpages))
24
+ end
25
+
26
+ # Returns page layouts including given layout ready for Rails' select form helper.
27
+ #
28
+ def layouts_with_own_for_select(page_layout_name, language_id, layoutpages: false)
29
+ layouts = selectable_layouts(language_id, layoutpages: layoutpages)
30
+ if layouts.detect { |l| l["name"] == page_layout_name }.nil?
31
+ @map_array = [[human_layout_name(page_layout_name), page_layout_name]]
32
+ else
33
+ @map_array = []
34
+ end
35
+ mapped_layouts_for_select(layouts)
36
+ end
37
+
38
+ deprecate :layouts_with_own_for_select, deprecator: Alchemy::Deprecation
39
+
40
+ # Returns all layouts that can be used for creating a new page.
41
+ #
42
+ # It removes all layouts from available layouts that are unique and already taken and that are marked as hide.
43
+ #
44
+ # @param [Fixnum]
45
+ # language_id of current used Language.
46
+ # @param [Boolean] (false)
47
+ # Pass true to only select layouts for global/layout pages.
48
+ #
49
+ def selectable_layouts(language_id, layoutpages: false)
50
+ @language_id = language_id
51
+ layouts_repository.all.select do |layout|
52
+ if layoutpages
53
+ layout["layoutpage"] && layout_available?(layout)
54
+ else
55
+ !layout["layoutpage"] && layout_available?(layout)
56
+ end
57
+ end
58
+ end
59
+
60
+ # Translates name for given layout
61
+ #
62
+ # === Translation example
63
+ #
64
+ # en:
65
+ # alchemy:
66
+ # page_layout_names:
67
+ # products_overview: Products Overview
68
+ #
69
+ # @param [String]
70
+ # The layout name
71
+ #
72
+ def human_layout_name(layout)
73
+ Alchemy.t(layout, scope: "page_layout_names", default: layout.to_s.humanize)
74
+ end
75
+
76
+ private
77
+
78
+ def layouts_repository
79
+ @_layouts_repository ||= PageLayout
80
+ end
81
+
82
+ # Maps given layouts for Rails select form helper.
83
+ #
84
+ def mapped_layouts_for_select(layouts)
85
+ layouts.each do |layout|
86
+ @map_array << [human_layout_name(layout["name"]), layout["name"]]
87
+ end
88
+ @map_array
89
+ end
90
+
91
+ # Returns true if the given layout is unique and not already taken or it should be hidden.
92
+ #
93
+ def layout_available?(layout)
94
+ !layout["hide"] && !already_taken?(layout) && available_on_site?(layout)
95
+ end
96
+
97
+ # Returns true if this layout is unique and already taken by another page.
98
+ #
99
+ def already_taken?(layout)
100
+ layout["unique"] && page_with_layout_existing?(layout["name"])
101
+ end
102
+
103
+ # Returns true if one page already has the given layout
104
+ #
105
+ def page_with_layout_existing?(layout)
106
+ Alchemy::Page.where(page_layout: layout, language_id: @language_id).pluck(:id).any?
107
+ end
108
+
109
+ # Returns true if given layout is available for current site.
110
+ #
111
+ # If no site layouts are defined it always returns true.
112
+ #
113
+ # == Example
114
+ #
115
+ # # config/alchemy/site_layouts.yml
116
+ # - name: default_site
117
+ # page_layouts: [default_intro]
118
+ #
119
+ def available_on_site?(layout)
120
+ return false unless Alchemy::Site.current
121
+
122
+ Alchemy::Site.current.definition.blank? ||
123
+ Alchemy::Site.current.definition.fetch("page_layouts", []).include?(layout["name"])
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -36,6 +36,7 @@
36
36
  #
37
37
 
38
38
  require_dependency "alchemy/page/fixed_attributes"
39
+ require_dependency "alchemy/page/page_layouts"
39
40
  require_dependency "alchemy/page/page_scopes"
40
41
  require_dependency "alchemy/page/page_natures"
41
42
  require_dependency "alchemy/page/page_naming"
@@ -78,6 +79,7 @@ module Alchemy
78
79
  :restricted,
79
80
  :robot_index,
80
81
  :robot_follow,
82
+ :searchable,
81
83
  :sitemap,
82
84
  :tag_list,
83
85
  :title,
@@ -152,6 +154,7 @@ module Alchemy
152
154
  after_update :touch_nodes
153
155
 
154
156
  # Concerns
157
+ include PageLayouts
155
158
  include PageScopes
156
159
  include PageNatures
157
160
  include PageNaming
@@ -267,11 +270,11 @@ module Alchemy
267
270
  where(id: clipboard.collect { |p| p["id"] })
268
271
  end
269
272
 
270
- def all_from_clipboard_for_select(clipboard, language_id, layoutpage = false)
273
+ def all_from_clipboard_for_select(clipboard, language_id, layoutpages: false)
271
274
  return [] if clipboard.blank?
272
275
 
273
276
  clipboard_pages = all_from_clipboard(clipboard)
274
- allowed_page_layouts = Alchemy::PageLayout.selectable_layouts(language_id, layoutpage)
277
+ allowed_page_layouts = Alchemy::Page.selectable_layouts(language_id, layoutpages: layoutpages)
275
278
  allowed_page_layout_names = allowed_page_layouts.collect { |p| p["name"] }
276
279
  clipboard_pages.select { |cp| allowed_page_layout_names.include?(cp.page_layout) }
277
280
  end
@@ -15,13 +15,25 @@ module Alchemy
15
15
  # @return [Alchemy::PictureThumb] The persisted thumbnail record
16
16
  #
17
17
  def call(variant, signature, uid)
18
- image = variant.image
19
- image.to_file(server_path(uid)).close
20
- variant.picture.thumbs.create!(
18
+ return if !variant.picture.valid?
19
+
20
+ # create the thumb before storing
21
+ # to prevent db race conditions
22
+ thumb = Alchemy::PictureThumb.create!(
21
23
  picture: variant.picture,
22
24
  signature: signature,
23
25
  uid: uid,
24
26
  )
27
+ begin
28
+ # process the image
29
+ image = variant.image
30
+ # store the processed image
31
+ image.to_file(server_path(uid)).close
32
+ rescue RuntimeError => e
33
+ ErrorTracking.notification_handler.call(e)
34
+ # destroy the thumb if processing or storing fails
35
+ thumb&.destroy
36
+ end
25
37
  end
26
38
 
27
39
  private
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ module DomIds
5
+ extend ActiveSupport::Concern
6
+
7
+ RESERVED_ANCHOR_SETTING_VALUES = %w[false from_value true]
8
+
9
+ included do
10
+ before_validation :parameterize_dom_id,
11
+ if: -> { settings[:anchor].to_s == "true" }
12
+ before_validation :set_dom_id_from_value,
13
+ if: -> { settings[:anchor].to_s == "from_value" }
14
+ before_validation :set_dom_id_to_fixed_value,
15
+ if: -> { !RESERVED_ANCHOR_SETTING_VALUES.include? settings[:anchor].to_s }
16
+ end
17
+
18
+ private
19
+
20
+ def parameterize_dom_id
21
+ self.dom_id = dom_id&.parameterize
22
+ end
23
+
24
+ def set_dom_id_from_value
25
+ self.dom_id = value&.parameterize
26
+ end
27
+
28
+ def set_dom_id_to_fixed_value
29
+ self.dom_id = settings[:anchor]&.parameterize
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ class IngredientSerializer < ActiveModel::Serializer
5
+ attributes :id,
6
+ :role,
7
+ :value,
8
+ :element_id,
9
+ :data
10
+ end
11
+ end
@@ -7,6 +7,9 @@
7
7
 
8
8
  $errors.hide();
9
9
  $el.trigger('SaveElement.Alchemy', {previewText: '<%= j sanitize(@element.preview_text) %>'});
10
+ <% @element.ingredients.select { |i| i.settings[:anchor] }.each do |ingredient| %>
11
+ Alchemy.IngredientAnchorLink.updateIcon(<%= ingredient.id %>, <%= ingredient.dom_id.present? %>);
12
+ <% end %>
10
13
  Alchemy.growl('<%= Alchemy.t(:element_saved) %>');
11
14
  Alchemy.PreviewWindow.refresh(function() {
12
15
  Alchemy.ElementEditors.focusElementPreview(<%= @element.id %>);
@@ -0,0 +1,4 @@
1
+ <% disabled = ingredient.settings[:anchor].to_s != "true" %>
2
+ <%= f.input :dom_id,
3
+ disabled: disabled,
4
+ hint: disabled && Alchemy.t(:automatic_anchor_notice) %>
@@ -0,0 +1,3 @@
1
+ <% if ingredient.settings[:anchor] %>
2
+ <%= render "dom_id_fields", f: f, ingredient: ingredient %>
3
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <% if ingredient.settings[:anchor] %>
2
+ <%= render "dom_id_fields", f: f, ingredient: ingredient %>
3
+ <% end %>
@@ -0,0 +1,7 @@
1
+ Alchemy.closeCurrentDialog(
2
+ <% if @ingredient.settings[:anchor] %>
3
+ function() {
4
+ Alchemy.IngredientAnchorLink.updateIcon(<%= @ingredient.id %>, <%= @ingredient.dom_id.present? %>);
5
+ }
6
+ <% end %>
7
+ );
@@ -14,7 +14,7 @@
14
14
  <%= f.input :frontpage_name %>
15
15
  <%= f.input :page_layout,
16
16
  collection: Alchemy::PageLayout.all,
17
- label_method: ->(p) { Alchemy::PageLayout.human_layout_name(p['name']) },
17
+ label_method: ->(p) { Alchemy::Page.human_layout_name(p['name']) },
18
18
  value_method: ->(p) { p['name'] },
19
19
  input_html: {class: 'alchemy_selectbox'} %>
20
20
  <%= f.input :public %>
@@ -18,7 +18,7 @@
18
18
  <%= language.frontpage_name %>
19
19
  </td>
20
20
  <td>
21
- <%= Alchemy::PageLayout.human_layout_name(language.page_layout) %>
21
+ <%= Alchemy::Page.human_layout_name(language.page_layout) %>
22
22
  </td>
23
23
  <td class="center">
24
24
  <%= language.public? ? render_icon(:check) : nil %>
@@ -19,6 +19,15 @@
19
19
  <%= f.input :title,
20
20
  input_html: {'data-alchemy-char-counter' => 60} %>
21
21
 
22
+ <% if Alchemy.enable_searchable %>
23
+ <div class="input check_boxes">
24
+ <label class="control-label"><%= Alchemy.t(:fulltext_search) %></label>
25
+ <div class="control_group">
26
+ <%= page_status_checkbox(@page, :searchable) %>
27
+ </div>
28
+ </div>
29
+ <% end %>
30
+
22
31
  <div class="input check_boxes">
23
32
  <label class="control-label"><%= Alchemy.t(:search_engines) %></label>
24
33
  <div class="control_group">
@@ -6,7 +6,7 @@
6
6
  options_for_select(
7
7
  @current_language.site.page_layout_names.map do |layout|
8
8
  [
9
- Alchemy::PageLayout.human_layout_name(layout),
9
+ Alchemy::Page.human_layout_name(layout),
10
10
  layout
11
11
  ]
12
12
  end,