alchemy_cms 4.4.2 → 4.6.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +23 -17
  3. data/.rubocop.yml +7 -15
  4. data/CHANGELOG.md +36 -0
  5. data/alchemy_cms.gemspec +1 -0
  6. data/app/assets/javascripts/alchemy/admin.js +3 -0
  7. data/app/assets/javascripts/alchemy/alchemy.link_dialog.js.coffee +5 -5
  8. data/app/assets/javascripts/alchemy/alchemy.node_tree.js +66 -0
  9. data/app/assets/javascripts/alchemy/alchemy.utils.js +45 -0
  10. data/app/assets/javascripts/alchemy/templates/index.js +1 -0
  11. data/app/assets/javascripts/alchemy/templates/node_folder.hbs +3 -0
  12. data/app/assets/javascripts/alchemy/templates/page.hbs +1 -1
  13. data/app/assets/stylesheets/alchemy/_mixins.scss +2 -3
  14. data/app/assets/stylesheets/alchemy/_variables.scss +2 -2
  15. data/app/assets/stylesheets/alchemy/lists.scss +0 -8
  16. data/app/assets/stylesheets/alchemy/nodes.scss +6 -1
  17. data/app/assets/stylesheets/alchemy/sitemap.scss +59 -21
  18. data/app/controllers/alchemy/admin/dashboard_controller.rb +1 -1
  19. data/app/controllers/alchemy/admin/nodes_controller.rb +2 -10
  20. data/app/controllers/alchemy/admin/pages_controller.rb +0 -1
  21. data/app/controllers/alchemy/api/nodes_controller.rb +29 -0
  22. data/app/controllers/alchemy/api/pages_controller.rb +2 -0
  23. data/app/decorators/alchemy/content_editor.rb +55 -0
  24. data/app/helpers/alchemy/admin/pages_helper.rb +16 -16
  25. data/app/helpers/alchemy/pages_helper.rb +2 -2
  26. data/app/models/alchemy/content.rb +8 -22
  27. data/app/models/alchemy/node.rb +29 -5
  28. data/app/models/alchemy/page.rb +15 -1
  29. data/app/models/alchemy/page/url_path.rb +66 -0
  30. data/app/serializers/alchemy/node_serializer.rb +12 -0
  31. data/app/serializers/alchemy/page_serializer.rb +2 -1
  32. data/app/serializers/alchemy/page_tree_serializer.rb +4 -3
  33. data/app/views/alchemy/admin/layoutpages/index.html.erb +5 -1
  34. data/app/views/alchemy/admin/nodes/_form.html.erb +14 -8
  35. data/app/views/alchemy/admin/nodes/_node.html.erb +10 -20
  36. data/app/views/alchemy/admin/nodes/index.html.erb +7 -17
  37. data/app/views/alchemy/admin/pages/_form.html.erb +1 -1
  38. data/app/views/alchemy/admin/pages/_menu_fields.html.erb +33 -29
  39. data/app/views/alchemy/admin/pages/_page.html.erb +3 -6
  40. data/app/views/alchemy/admin/pages/_sitemap.html.erb +6 -0
  41. data/app/views/alchemy/admin/pages/info.html.erb +1 -1
  42. data/app/views/alchemy/admin/partials/_routes.html.erb +8 -0
  43. data/config/alchemy/config.yml +0 -6
  44. data/config/locales/alchemy.en.yml +14 -6
  45. data/config/routes.rb +8 -5
  46. data/db/migrate/20200226081535_add_site_id_to_alchemy_nodes.rb +15 -0
  47. data/lib/alchemy/config.rb +30 -2
  48. data/lib/alchemy/ssl_protection.rb +3 -1
  49. data/lib/alchemy/test_support/factories/node_factory.rb +1 -0
  50. data/lib/alchemy/upgrader/four_point_six.rb +50 -0
  51. data/lib/alchemy/version.rb +1 -1
  52. data/lib/rails/generators/alchemy/install/install_generator.rb +1 -1
  53. data/lib/rails/generators/alchemy/install/templates/menus.yml.tt +8 -0
  54. data/lib/rails/generators/alchemy/menus/menus_generator.rb +3 -3
  55. data/lib/rails/generators/alchemy/menus/templates/wrapper.html.haml +1 -1
  56. data/lib/rails/generators/alchemy/menus/templates/wrapper.html.slim +1 -1
  57. data/lib/tasks/alchemy/convert.rake +2 -0
  58. data/lib/tasks/alchemy/upgrade.rake +67 -46
  59. data/vendor/assets/javascripts/sortable/Sortable.min.js +2 -0
  60. metadata +28 -2
@@ -1,3 +1,6 @@
1
+ $sitemap-url-large-width: 250px;
2
+ $sitemap-url-xlarge-width: 350px;
3
+
1
4
  #sort_panel {
2
5
  background: $light-gray;
3
6
  padding: 47px 0 8px 0;
@@ -20,7 +23,7 @@
20
23
 
21
24
  #sitemap-wrapper {
22
25
  position: relative;
23
- min-height: 100%;
26
+ min-height: calc(100vh - 96px);
24
27
  }
25
28
 
26
29
  .sitemap_pagename_link {
@@ -28,23 +31,35 @@
28
31
  padding: 0 10px;
29
32
  margin: 2px;
30
33
  text-decoration: none;
34
+ white-space: nowrap;
35
+ text-overflow: ellipsis;
36
+ overflow: hidden;
31
37
 
32
38
  &.inactive {
33
39
  color: #656565;
34
40
  }
35
41
  }
36
42
 
37
- .redirect_url {
43
+ .sitemap_url {
44
+ display: none;
38
45
  float: right;
39
- text-align: right;
40
46
  background-color: $sitemap-info-background-color;
41
- line-height: $sitemap-line-height;
47
+ line-height: $sitemap-line-height - 2px;
42
48
  font-size: $small-font-size;
43
- padding: 0 2*$default-padding;
44
- max-width: 45%;
49
+ padding: 0 2 * $default-padding;
45
50
  white-space: nowrap;
46
51
  overflow: hidden;
47
52
  text-overflow: ellipsis;
53
+ border: 1px solid $sitemap-page-background-color;
54
+
55
+ @media screen and (min-width: $large-screen-break-point) {
56
+ display: block;
57
+ width: $sitemap-url-large-width;
58
+ }
59
+
60
+ @media screen and (min-width: 1440px) {
61
+ width: $sitemap-url-xlarge-width;
62
+ }
48
63
  }
49
64
 
50
65
  .sitemap_line_spacer {
@@ -55,7 +70,7 @@
55
70
 
56
71
  .sitemap_page {
57
72
  height: $sitemap-line-height;
58
- margin: 3*$default-margin 0;
73
+ margin: 3 * $default-margin 0;
59
74
  position: relative;
60
75
  transition: background-color $transition-duration;
61
76
 
@@ -89,13 +104,13 @@
89
104
  width: 32px;
90
105
  line-height: $sitemap-line-height;
91
106
  float: left;
92
- padding: 0 2*$default-padding;
107
+ padding: 0 2 * $default-padding;
93
108
  text-align: center;
94
109
  }
95
110
 
96
111
  .sitemap_right_tools {
97
112
  height: $sitemap-line-height;
98
- padding: 0 2*$default-padding;
113
+ padding: 0 2 * $default-padding;
99
114
  float: right;
100
115
 
101
116
  .sitemap_tool {
@@ -130,7 +145,9 @@
130
145
  &.sorting {
131
146
  padding-top: 100px;
132
147
 
133
- .page_icon { cursor: move }
148
+ .page_icon {
149
+ cursor: move;
150
+ }
134
151
  }
135
152
 
136
153
  .page_folder {
@@ -183,25 +200,44 @@
183
200
  }
184
201
 
185
202
  #sitemap_heading {
203
+ display: flex;
186
204
  padding: 0;
187
-
188
- .page_infos {
189
- margin-right: 210px;
190
- text-align: left;
191
- float: right;
192
- line-height: 28px;
193
- background: transparent;
194
- }
205
+ line-height: 28px;
195
206
 
196
207
  .page_name {
197
- line-height: 28px;
198
208
  margin-left: 43px;
199
209
  }
210
+
211
+ .page_urlname {
212
+ display: none;
213
+ margin-left: auto;
214
+ padding-left: 2 * $default-padding;
215
+ padding-right: 2 * $default-padding;
216
+
217
+ @media screen and (min-width: $large-screen-break-point) {
218
+ display: block;
219
+ width: $sitemap-url-large-width;
220
+ }
221
+
222
+ @media screen and (min-width: 1440px) {
223
+ width: $sitemap-url-xlarge-width;
224
+ }
225
+ }
226
+
227
+ .page_status {
228
+ padding-left: 2 * $default-padding;
229
+ margin-right: 214px;
230
+ margin-left: auto;
231
+
232
+ @media screen and (min-width: $large-screen-break-point) {
233
+ margin-left: initial;
234
+ }
235
+ }
200
236
  }
201
237
 
202
238
  #page_filter_result {
203
239
  display: none;
204
- margin-left: 2*$default-margin;
240
+ margin-left: 2 * $default-margin;
205
241
  }
206
242
 
207
243
  .alchemy-dialog {
@@ -213,6 +249,8 @@
213
249
  margin: 0;
214
250
  padding: 0 24px 8px 8px;
215
251
 
216
- .page_icon { cursor: default }
252
+ .page_icon {
253
+ cursor: default;
254
+ }
217
255
  }
218
256
  }
@@ -60,7 +60,7 @@ module Alchemy
60
60
  response = query_github
61
61
  if response.code == "200"
62
62
  alchemy_tags = JSON.parse(response.body)
63
- alchemy_tags.collect { |h| h['name'] }.sort
63
+ alchemy_tags.collect { |h| h['name'].tr('v', '') }.sort
64
64
  else
65
65
  # no luck at all?
66
66
  raise UpdateServiceUnavailable
@@ -9,25 +9,17 @@ module Alchemy
9
9
 
10
10
  def new
11
11
  @node = Node.new(
12
+ site: Alchemy::Site.current,
12
13
  parent_id: params[:parent_id],
13
14
  language: Language.current
14
15
  )
15
16
  end
16
17
 
17
- def toggle
18
- node = Node.find(params[:id])
19
- node.update(folded: !node.folded)
20
- if node.folded?
21
- head :ok
22
- else
23
- render partial: 'node', collection: node.children.includes(:page, :children)
24
- end
25
- end
26
-
27
18
  private
28
19
 
29
20
  def resource_params
30
21
  params.require(:node).permit(
22
+ :site_id,
31
23
  :parent_id,
32
24
  :language_id,
33
25
  :page_id,
@@ -142,7 +142,6 @@ module Alchemy
142
142
  @attachments = Attachment.all.collect { |f|
143
143
  [f.name, download_attachment_path(id: f.id, name: f.urlname)]
144
144
  }
145
- @url_prefix = prefix_locale? ? "#{Language.current.code}/" : ""
146
145
  end
147
146
 
148
147
  def fold
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ class Api::NodesController < Api::BaseController
5
+ before_action :load_node
6
+ before_action :authorize_access, only: [:move, :toggle_folded]
7
+
8
+ def move
9
+ target_parent_node = Node.find(params[:target_parent_id])
10
+ @node.move_to_child_with_index(target_parent_node, params[:new_position])
11
+ render json: @node, serializer: NodeSerializer
12
+ end
13
+
14
+ def toggle_folded
15
+ @node.update(folded: !@node.folded)
16
+ render json: @node, serializer: NodeSerializer
17
+ end
18
+
19
+ private
20
+
21
+ def load_node
22
+ @node = Node.find(params[:id])
23
+ end
24
+
25
+ def authorize_access
26
+ authorize! :update, @node
27
+ end
28
+ end
29
+ end
@@ -13,6 +13,7 @@ module Alchemy
13
13
  else
14
14
  @pages = Page.accessible_by(current_ability, :index)
15
15
  end
16
+ @pages = @pages.where.not(parent_id: nil)
16
17
  @pages = @pages.includes(*page_includes)
17
18
  if params[:page_layout].present?
18
19
  Alchemy::Deprecation.warn <<~WARN
@@ -104,6 +105,7 @@ module Alchemy
104
105
  [
105
106
  :tags,
106
107
  {
108
+ language: :site,
107
109
  elements: [
108
110
  {
109
111
  nested_elements: [
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Alchemy
4
+ class ContentEditor < SimpleDelegator
5
+ alias_method :content, :__getobj__
6
+
7
+ def to_partial_path
8
+ "alchemy/essences/#{essence_partial_name}_editor"
9
+ end
10
+
11
+ def css_classes
12
+ [
13
+ "content_editor",
14
+ essence_partial_name,
15
+ ].compact
16
+ end
17
+
18
+ def data_attributes
19
+ {
20
+ content_id: id,
21
+ content_name: name,
22
+ }
23
+ end
24
+
25
+ # Returns a string to be passed to Rails form field tags to ensure we have same params layout everywhere.
26
+ #
27
+ # === Example:
28
+ #
29
+ # <%= text_field_tag content_editor.form_field_name, content_editor.ingredient %>
30
+ #
31
+ # === Options:
32
+ #
33
+ # You can pass an Essence column_name. Default is 'ingredient'
34
+ #
35
+ # ==== Example:
36
+ #
37
+ # <%= text_field_tag content_editor.form_field_name(:link), content_editor.ingredient %>
38
+ #
39
+ def form_field_name(essence_column = "ingredient")
40
+ "contents[#{id}][#{essence_column}]"
41
+ end
42
+
43
+ def form_field_id(essence_column = "ingredient")
44
+ "contents_#{id}_#{essence_column}"
45
+ end
46
+
47
+ # Fixes Rails partial renderer calling to_model on the object
48
+ # which reveals the delegated content instead of this decorator.
49
+ def respond_to?(method_name)
50
+ return false if method_name == :to_model
51
+
52
+ super
53
+ end
54
+ end
55
+ end
@@ -9,13 +9,13 @@ module Alchemy
9
9
  #
10
10
  def preview_sizes_for_select
11
11
  options_for_select([
12
- 'auto',
13
- [Alchemy.t('240', scope: 'preview_sizes'), 240],
14
- [Alchemy.t('320', scope: 'preview_sizes'), 320],
15
- [Alchemy.t('480', scope: 'preview_sizes'), 480],
16
- [Alchemy.t('768', scope: 'preview_sizes'), 768],
17
- [Alchemy.t('1024', scope: 'preview_sizes'), 1024],
18
- [Alchemy.t('1280', scope: 'preview_sizes'), 1280]
12
+ "auto",
13
+ [Alchemy.t("240", scope: "preview_sizes"), 240],
14
+ [Alchemy.t("320", scope: "preview_sizes"), 320],
15
+ [Alchemy.t("480", scope: "preview_sizes"), 480],
16
+ [Alchemy.t("768", scope: "preview_sizes"), 768],
17
+ [Alchemy.t("1024", scope: "preview_sizes"), 1024],
18
+ [Alchemy.t("1280", scope: "preview_sizes"), 1280],
19
19
  ])
20
20
  end
21
21
 
@@ -27,30 +27,30 @@ module Alchemy
27
27
  if page.persisted? && page.definition.blank?
28
28
  [
29
29
  page_layout_missing_warning,
30
- Alchemy.t(:page_type)
31
- ].join('&nbsp;').html_safe
30
+ Alchemy.t(:page_type),
31
+ ].join("&nbsp;").html_safe
32
32
  else
33
33
  Alchemy.t(:page_type)
34
34
  end
35
35
  end
36
36
 
37
- def page_status_checkbox(page, attribute)
38
- label = page.class.human_attribute_name(attribute)
37
+ def page_status_checkbox(page, attribute, label: nil)
38
+ label_text = label || page.class.human_attribute_name(attribute)
39
39
 
40
40
  if page.attribute_fixed?(attribute)
41
41
  checkbox = check_box(:page, attribute, disabled: true)
42
- hint = content_tag(:span, class: 'hint-bubble') do
42
+ hint = content_tag(:span, class: "hint-bubble") do
43
43
  Alchemy.t(:attribute_fixed, attribute: attribute)
44
44
  end
45
- content = content_tag(:span, class: 'with-hint') do
46
- "#{checkbox}\n#{label}\n#{hint}".html_safe
45
+ content = content_tag(:span, class: "with-hint") do
46
+ "#{checkbox}\n#{label_text}\n#{hint}".html_safe
47
47
  end
48
48
  else
49
49
  checkbox = check_box(:page, attribute)
50
- content = "#{checkbox}\n#{label}".html_safe
50
+ content = "#{checkbox}\n#{label_text}".html_safe
51
51
  end
52
52
 
53
- content_tag(:label, class: 'checkbox') { content }
53
+ content_tag(:label, class: "checkbox") { content }
54
54
  end
55
55
  end
56
56
  end
@@ -188,7 +188,7 @@ module Alchemy
188
188
  # @param [String] - Name of the menu
189
189
  # @param [Hash] - A set of options available in your menu partials
190
190
  def render_menu(name, options = {})
191
- root_node = Alchemy::Node.roots.find_by(name: name)
191
+ root_node = Alchemy::Node.roots.find_by(name: name, site: Alchemy::Site.current)
192
192
  if root_node.nil?
193
193
  warning("Menu with name #{name} not found!")
194
194
  return
@@ -198,7 +198,7 @@ module Alchemy
198
198
  node_partial_name: "#{root_node.view_folder_name}/node"
199
199
  }.merge(options)
200
200
 
201
- render(root_node, menu: root_node, node: root_node, options: options)
201
+ render(root_node.to_partial_path, menu: root_node, node: root_node, options: options)
202
202
  rescue ActionView::MissingTemplate => e
203
203
  warning <<~WARN
204
204
  Menu partial not found for #{name}.
@@ -172,28 +172,6 @@ module Alchemy
172
172
  definition['validate'].present?
173
173
  end
174
174
 
175
- # Returns a string to be passed to Rails form field tags to ensure we have same params layout everywhere.
176
- #
177
- # === Example:
178
- #
179
- # <%= text_field_tag content.form_field_name, content.ingredient %>
180
- #
181
- # === Options:
182
- #
183
- # You can pass an Essence column_name. Default is 'ingredient'
184
- #
185
- # ==== Example:
186
- #
187
- # <%= text_field_tag content.form_field_name(:link), content.ingredient %>
188
- #
189
- def form_field_name(essence_column = 'ingredient')
190
- "contents[#{id}][#{essence_column}]"
191
- end
192
-
193
- def form_field_id(essence_column = 'ingredient')
194
- "contents_#{id}_#{essence_column}"
195
- end
196
-
197
175
  # Returns a string used as dom id on html elements.
198
176
  def dom_id
199
177
  return '' if essence.nil?
@@ -245,6 +223,14 @@ module Alchemy
245
223
  "has_tinymce" + (has_custom_tinymce_config? ? " #{element.name}_#{name}" : "")
246
224
  end
247
225
 
226
+ def editor
227
+ @_editor ||= ContentEditor.new(self)
228
+ end
229
+ delegate :form_field_name, to: :editor
230
+ deprecate form_field_name: "use Alchemy::ContentEditor#form_field_name instead", deprecator: Alchemy::Deprecation
231
+ delegate :form_field_id, to: :editor
232
+ deprecate form_field_id: "use Alchemy::ContentEditor#form_field_id instead", deprecator: Alchemy::Deprecation
233
+
248
234
  # Returns the default value from content definition
249
235
  #
250
236
  # If the value is a symbol it gets passed through i18n
@@ -4,11 +4,12 @@ module Alchemy
4
4
  class Node < BaseRecord
5
5
  VALID_URL_REGEX = /\A(\/|\D[a-z\+\d\.\-]+:)/
6
6
 
7
- acts_as_nested_set scope: 'language_id', touch: true
7
+ acts_as_nested_set scope: "language_id", touch: true
8
8
  stampable stamper_class_name: Alchemy.user_class_name
9
9
 
10
- belongs_to :language, class_name: 'Alchemy::Language'
11
- belongs_to :page, class_name: 'Alchemy::Page', optional: true, inverse_of: :nodes
10
+ belongs_to :site, class_name: "Alchemy::Site"
11
+ belongs_to :language, class_name: "Alchemy::Language"
12
+ belongs_to :page, class_name: "Alchemy::Page", optional: true, inverse_of: :nodes
12
13
 
13
14
  validates :name, presence: true, if: -> { page.nil? }
14
15
  validates :url, format: { with: VALID_URL_REGEX }, unless: -> { url.nil? }
@@ -24,9 +25,32 @@ module Alchemy
24
25
  class << self
25
26
  # Returns all root nodes for current language
26
27
  def language_root_nodes
27
- raise 'No language found' if Language.current.nil?
28
+ raise "No language found" if Language.current.nil?
29
+
28
30
  roots.where(language_id: Language.current.id)
29
31
  end
32
+
33
+ def available_menu_names
34
+ read_definitions_file
35
+ end
36
+
37
+ private
38
+
39
+ # Reads the element definitions file named +menus.yml+ from +config/alchemy/+ folder.
40
+ #
41
+ def read_definitions_file
42
+ if ::File.exist?(definitions_file_path)
43
+ ::YAML.safe_load(File.read(definitions_file_path)) || []
44
+ else
45
+ raise LoadError, "Could not find menus.yml file! Please run `rails generate alchemy:install`"
46
+ end
47
+ end
48
+
49
+ # Returns the +menus.yml+ file path
50
+ #
51
+ def definitions_file_path
52
+ Rails.root.join "config/alchemy/menus.yml"
53
+ end
30
54
  end
31
55
 
32
56
  # Returns the url
@@ -34,7 +58,7 @@ module Alchemy
34
58
  # Either the value is stored in the database, aka. an external url.
35
59
  # Or, if attached, the values comes from a page.
36
60
  def url
37
- page && "/#{page.urlname}" || read_attribute(:url).presence
61
+ page&.url_path || read_attribute(:url).presence
38
62
  end
39
63
 
40
64
  def to_partial_path