alchemy_cms 4.4.3 → 4.6.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) 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 +35 -1
  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 +0 -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 +1 -1
  26. data/app/models/alchemy/content.rb +8 -22
  27. data/app/models/alchemy/node.rb +29 -6
  28. data/app/models/alchemy/page.rb +11 -0
  29. data/app/models/alchemy/page/page_naming.rb +1 -1
  30. data/app/models/alchemy/page/url_path.rb +66 -0
  31. data/app/serializers/alchemy/node_serializer.rb +12 -0
  32. data/app/serializers/alchemy/page_serializer.rb +2 -1
  33. data/app/serializers/alchemy/page_tree_serializer.rb +4 -3
  34. data/app/views/alchemy/admin/layoutpages/index.html.erb +5 -1
  35. data/app/views/alchemy/admin/nodes/_form.html.erb +13 -8
  36. data/app/views/alchemy/admin/nodes/_node.html.erb +10 -20
  37. data/app/views/alchemy/admin/nodes/index.html.erb +7 -17
  38. data/app/views/alchemy/admin/pages/_form.html.erb +1 -1
  39. data/app/views/alchemy/admin/pages/_menu_fields.html.erb +33 -29
  40. data/app/views/alchemy/admin/pages/_page.html.erb +3 -6
  41. data/app/views/alchemy/admin/pages/_sitemap.html.erb +6 -0
  42. data/app/views/alchemy/admin/pages/info.html.erb +1 -1
  43. data/app/views/alchemy/admin/partials/_routes.html.erb +8 -0
  44. data/config/alchemy/config.yml +0 -6
  45. data/config/locales/alchemy.en.yml +14 -6
  46. data/config/routes.rb +8 -5
  47. data/lib/alchemy/config.rb +30 -2
  48. data/lib/alchemy/ssl_protection.rb +3 -1
  49. data/lib/alchemy/upgrader/four_point_six.rb +50 -0
  50. data/lib/alchemy/version.rb +1 -1
  51. data/lib/rails/generators/alchemy/install/install_generator.rb +1 -1
  52. data/lib/rails/generators/alchemy/install/templates/menus.yml.tt +8 -0
  53. data/lib/rails/generators/alchemy/menus/menus_generator.rb +3 -3
  54. data/lib/rails/generators/alchemy/menus/templates/wrapper.html.haml +1 -1
  55. data/lib/rails/generators/alchemy/menus/templates/wrapper.html.slim +1 -1
  56. data/lib/tasks/alchemy/convert.rake +2 -0
  57. data/lib/tasks/alchemy/upgrade.rake +67 -46
  58. data/vendor/assets/javascripts/sortable/Sortable.min.js +2 -0
  59. metadata +29 -4
@@ -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
@@ -15,16 +15,6 @@ module Alchemy
15
15
  )
16
16
  end
17
17
 
18
- def toggle
19
- node = Node.find(params[:id])
20
- node.update(folded: !node.folded)
21
- if node.folded?
22
- head :ok
23
- else
24
- render partial: 'node', collection: node.children.includes(:page, :children)
25
- end
26
- end
27
-
28
18
  private
29
19
 
30
20
  def resource_params
@@ -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
@@ -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,12 +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 :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
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
13
13
 
14
14
  validates :name, presence: true, if: -> { page.nil? }
15
15
  validates :url, format: { with: VALID_URL_REGEX }, unless: -> { url.nil? }
@@ -25,9 +25,32 @@ module Alchemy
25
25
  class << self
26
26
  # Returns all root nodes for current language
27
27
  def language_root_nodes
28
- raise 'No language found' if Language.current.nil?
28
+ raise "No language found" if Language.current.nil?
29
+
29
30
  roots.where(language_id: Language.current.id)
30
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
31
54
  end
32
55
 
33
56
  # Returns the url
@@ -35,7 +58,7 @@ module Alchemy
35
58
  # Either the value is stored in the database, aka. an external url.
36
59
  # Or, if attached, the values comes from a page.
37
60
  def url
38
- page && "/#{page.urlname}" || read_attribute(:url).presence
61
+ page&.url_path || read_attribute(:url).presence
39
62
  end
40
63
 
41
64
  def to_partial_path