alchemy_cms 6.0.11 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) 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 +46 -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/elements_helper.rb +0 -8
  19. data/app/helpers/alchemy/url_helper.rb +0 -8
  20. data/app/models/alchemy/attachment/url.rb +1 -0
  21. data/app/models/alchemy/element/definitions.rb +16 -2
  22. data/app/models/alchemy/element/dom_id.rb +30 -0
  23. data/app/models/alchemy/element/presenters.rb +1 -1
  24. data/app/models/alchemy/element.rb +12 -3
  25. data/app/models/alchemy/ingredients/headline.rb +4 -1
  26. data/app/models/alchemy/ingredients/text.rb +3 -0
  27. data/app/models/alchemy/page/page_layouts.rb +128 -0
  28. data/app/models/alchemy/page.rb +4 -2
  29. data/app/models/alchemy/picture_thumb/create.rb +15 -3
  30. data/app/models/concerns/alchemy/dom_ids.rb +32 -0
  31. data/app/serializers/alchemy/ingredient_serializer.rb +11 -0
  32. data/app/views/alchemy/admin/elements/update.js.erb +3 -0
  33. data/app/views/alchemy/admin/ingredients/_dom_id_fields.html.erb +4 -0
  34. data/app/views/alchemy/admin/ingredients/_headline_fields.html.erb +3 -0
  35. data/app/views/alchemy/admin/ingredients/_text_fields.html.erb +3 -0
  36. data/app/views/alchemy/admin/ingredients/update.js.erb +7 -0
  37. data/app/views/alchemy/admin/languages/_form.html.erb +1 -1
  38. data/app/views/alchemy/admin/languages/_language.html.erb +1 -1
  39. data/app/views/alchemy/admin/pages/_page_layout_filter.html.erb +1 -1
  40. data/app/views/alchemy/admin/partials/_routes.html.erb +2 -1
  41. data/app/views/alchemy/ingredients/_headline_editor.html.erb +22 -20
  42. data/app/views/alchemy/ingredients/_headline_view.html.erb +1 -0
  43. data/app/views/alchemy/ingredients/_text_editor.html.erb +3 -0
  44. data/app/views/alchemy/ingredients/_text_view.html.erb +7 -3
  45. data/app/views/alchemy/ingredients/shared/_anchor.html.erb +9 -0
  46. data/app/views/alchemy/messages_mailer/contact_form_mail.de.text.erb +2 -0
  47. data/app/views/alchemy/messages_mailer/contact_form_mail.en.text.erb +2 -0
  48. data/app/views/alchemy/messages_mailer/contact_form_mail.es.text.erb +2 -0
  49. data/config/locales/alchemy.en.yml +5 -2
  50. data/config/routes.rb +1 -0
  51. data/lib/alchemy/error_tracking/error_logger.rb +13 -0
  52. data/lib/alchemy/error_tracking.rb +3 -1
  53. data/lib/alchemy/install/tasks.rb +1 -3
  54. data/lib/alchemy/modules.rb +2 -2
  55. data/lib/alchemy/page_layout.rb +0 -113
  56. data/lib/alchemy/test_support/shared_dom_ids_examples.rb +119 -0
  57. data/lib/alchemy/upgrader/tasks/add_page_versions.rb +2 -2
  58. data/lib/alchemy/upgrader/tasks/ingredients_migrator.rb +7 -5
  59. data/lib/alchemy/version.rb +1 -1
  60. data/lib/alchemy.rb +4 -0
  61. data/lib/generators/alchemy/elements/templates/view.html.erb +1 -1
  62. data/lib/generators/alchemy/elements/templates/view.html.haml +1 -1
  63. data/lib/generators/alchemy/elements/templates/view.html.slim +1 -1
  64. data/lib/generators/alchemy/install/install_generator.rb +27 -3
  65. data/lib/generators/alchemy/module/templates/ability.rb.tt +2 -3
  66. data/lib/generators/alchemy/module/templates/module_config.rb.tt +16 -14
  67. data/lib/tasks/alchemy/thumbnails.rake +1 -1
  68. data/package/admin.js +2 -0
  69. data/package/src/ingredient_anchor_link.js +17 -0
  70. data/package.json +2 -2
  71. data/vendor/assets/javascripts/tinymce/tinymce.min.js +2 -2
  72. metadata +18 -5
@@ -9,6 +9,8 @@ module Alchemy
9
9
  end
10
10
 
11
11
  mattr_accessor :notification_handler
12
- @@notification_handler = BaseHandler
13
12
  end
14
13
  end
14
+
15
+ require "alchemy/error_tracking/error_logger"
16
+ Alchemy::ErrorTracking.notification_handler = Alchemy::ErrorTracking::ErrorLogger
@@ -18,12 +18,10 @@ module Alchemy
18
18
  inject_into_file "./config/routes.rb", "\n mount Alchemy::Engine => '#{mountpoint}'\n", { after: sentinel, verbose: true }
19
19
  end
20
20
 
21
- def set_primary_language(auto_accept = false)
22
- code = "en"
21
+ def set_primary_language(code: "en", name: "English", auto_accept: false)
23
22
  unless auto_accept
24
23
  code = ask("- What is the language code of your site's primary language?", default: code)
25
24
  end
26
- name = "English"
27
25
  unless auto_accept
28
26
  name = ask("- What is the name of your site's primary language?", default: name)
29
27
  end
@@ -32,13 +32,13 @@ module Alchemy
32
32
  defined_controllers = [definition_hash["navigation"]["controller"]]
33
33
 
34
34
  if definition_hash["navigation"]["sub_navigation"].is_a?(Array)
35
- defined_controllers.concat(definition_hash["navigation"]["sub_navigation"].map{ |x| x["controller"] })
35
+ defined_controllers.concat(definition_hash["navigation"]["sub_navigation"].map { |x| x["controller"] })
36
36
  end
37
37
 
38
38
  validate_controllers_existence(defined_controllers)
39
39
  end
40
40
 
41
- @@alchemy_modules << definition_hash
41
+ @@alchemy_modules |= [definition_hash]
42
42
  end
43
43
 
44
44
  private
@@ -41,112 +41,8 @@ module Alchemy
41
41
  all.detect { |a| a["name"].casecmp(name).zero? }
42
42
  end
43
43
 
44
- def get_all_by_attributes(attributes)
45
- return [] if attributes.blank?
46
-
47
- if attributes.is_a? Hash
48
- layouts = []
49
- attributes.stringify_keys.each do |key, value|
50
- result = all.select { |l| l.key?(key) && l[key].to_s.casecmp(value.to_s).zero? }
51
- layouts += result unless result.empty?
52
- end
53
- layouts
54
- else
55
- []
56
- end
57
- end
58
-
59
- # Returns page layouts ready for Rails' select form helper.
60
- #
61
- def layouts_for_select(language_id, only_layoutpages = false)
62
- @map_array = []
63
- mapped_layouts_for_select(selectable_layouts(language_id, only_layoutpages))
64
- end
65
-
66
- # Returns all layouts that can be used for creating a new page.
67
- #
68
- # It removes all layouts from available layouts that are unique and already taken and that are marked as hide.
69
- #
70
- # @param [Fixnum]
71
- # language_id of current used Language.
72
- # @param [Boolean] (false)
73
- # Pass true to only select layouts for global/layout pages.
74
- #
75
- def selectable_layouts(language_id, only_layoutpages = false)
76
- @language_id = language_id
77
- all.select do |layout|
78
- if only_layoutpages
79
- layout["layoutpage"] && layout_available?(layout)
80
- else
81
- !layout["layoutpage"] && layout_available?(layout)
82
- end
83
- end
84
- end
85
-
86
- # Returns all names of elements defined in given page layout.
87
- #
88
- def element_names_for(page_layout)
89
- if definition = get(page_layout)
90
- definition.fetch("elements", [])
91
- else
92
- Rails.logger.warn "\n+++ Warning: No layout definition for #{page_layout} found! in page_layouts.yml\n"
93
- []
94
- end
95
- end
96
-
97
- # Translates name for given layout
98
- #
99
- # === Translation example
100
- #
101
- # en:
102
- # alchemy:
103
- # page_layout_names:
104
- # products_overview: Products Overview
105
- #
106
- # @param [String]
107
- # The layout name
108
- #
109
- def human_layout_name(layout)
110
- Alchemy.t(layout, scope: "page_layout_names", default: layout.to_s.humanize)
111
- end
112
-
113
44
  private
114
45
 
115
- # Returns true if the given layout is unique and not already taken or it should be hidden.
116
- #
117
- def layout_available?(layout)
118
- !layout["hide"] && !already_taken?(layout) && available_on_site?(layout)
119
- end
120
-
121
- # Returns true if this layout is unique and already taken by another page.
122
- #
123
- def already_taken?(layout)
124
- layout["unique"] && page_with_layout_existing?(layout["name"])
125
- end
126
-
127
- # Returns true if one page already has the given layout
128
- #
129
- def page_with_layout_existing?(layout)
130
- Alchemy::Page.where(page_layout: layout, language_id: @language_id).pluck(:id).any?
131
- end
132
-
133
- # Returns true if given layout is available for current site.
134
- #
135
- # If no site layouts are defined it always returns true.
136
- #
137
- # == Example
138
- #
139
- # # config/alchemy/site_layouts.yml
140
- # - name: default_site
141
- # page_layouts: [default_intro]
142
- #
143
- def available_on_site?(layout)
144
- return false unless Alchemy::Site.current
145
-
146
- Alchemy::Site.current.definition.blank? ||
147
- Alchemy::Site.current.definition.fetch("page_layouts", []).include?(layout["name"])
148
- end
149
-
150
46
  # Reads the layout definitions from +config/alchemy/page_layouts.yml+.
151
47
  #
152
48
  def read_definitions_file
@@ -168,15 +64,6 @@ module Alchemy
168
64
  def layouts_file_path
169
65
  Rails.root.join "config/alchemy/page_layouts.yml"
170
66
  end
171
-
172
- # Maps given layouts for Rails select form helper.
173
- #
174
- def mapped_layouts_for_select(layouts)
175
- layouts.each do |layout|
176
- @map_array << [human_layout_name(layout["name"]), layout["name"]]
177
- end
178
- @map_array
179
- end
180
67
  end
181
68
  end
182
69
  end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples_for "having dom ids" do
4
+ let(:element) { build(:alchemy_element, name: "element_with_ingredients") }
5
+
6
+ let(:ingredient) do
7
+ described_class.new(
8
+ element: element,
9
+ role: "headline",
10
+ )
11
+ end
12
+
13
+ describe "setting dom id from value" do
14
+ subject do
15
+ ingredient.valid? && ingredient.dom_id
16
+ end
17
+
18
+ before do
19
+ expect_any_instance_of(described_class).to receive(:settings).at_least(:once) { settings }
20
+ end
21
+
22
+ context "without anchor settings" do
23
+ let(:settings) do
24
+ {}
25
+ end
26
+
27
+ it "does not set a dom_id" do
28
+ is_expected.to be_nil
29
+ end
30
+ end
31
+
32
+ context "with anchor setting set to true" do
33
+ let(:settings) do
34
+ { anchor: true }
35
+ end
36
+
37
+ it "parameterizes dom_id" do
38
+ ingredient.dom_id = "SE Headline"
39
+ is_expected.to eq "se-headline"
40
+ end
41
+ end
42
+
43
+ context "with anchor setting set to from_value" do
44
+ let(:settings) do
45
+ { anchor: "from_value" }
46
+ end
47
+
48
+ context "with a value present" do
49
+ let(:ingredient) do
50
+ described_class.new(
51
+ element: element,
52
+ role: "headline",
53
+ value: "Hello World",
54
+ )
55
+ end
56
+
57
+ it "sets a dom_id from value" do
58
+ is_expected.to eq "hello-world"
59
+ end
60
+ end
61
+
62
+ context "with no value present" do
63
+ let(:ingredient) do
64
+ described_class.new(
65
+ element: element,
66
+ role: "headline",
67
+ value: "",
68
+ )
69
+ end
70
+
71
+ it "sets no dom_id" do
72
+ is_expected.to eq ""
73
+ end
74
+ end
75
+ end
76
+
77
+ context "with anchor setting set to fixed value" do
78
+ context "that is false" do
79
+ let(:settings) do
80
+ { anchor: false }
81
+ end
82
+
83
+ it "sets no dom_id" do
84
+ is_expected.to be_nil
85
+ end
86
+ end
87
+
88
+ context "that is true" do
89
+ let(:settings) do
90
+ { anchor: true }
91
+ end
92
+
93
+ it "sets no dom_id" do
94
+ is_expected.to be_nil
95
+ end
96
+ end
97
+
98
+ context "that is from_value" do
99
+ let(:settings) do
100
+ { anchor: true }
101
+ end
102
+
103
+ it "sets no dom_id" do
104
+ is_expected.to be_nil
105
+ end
106
+ end
107
+
108
+ context "that is a non reserved value" do
109
+ let(:settings) do
110
+ { anchor: "FixED VALUE" }
111
+ end
112
+
113
+ it "sets the dom_id to fixed value" do
114
+ is_expected.to eq "fixed-value"
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -15,10 +15,10 @@ module Alchemy::Upgrader::Tasks
15
15
  Alchemy::Page.transaction do
16
16
  page.versions.create!(
17
17
  public_on: page.legacy_public_on,
18
- public_until: page.legacy_public_until
18
+ public_until: page.legacy_public_until,
19
19
  ).tap do |version|
20
20
  # We must not use .find_each here to not mess up the order of elements
21
- page.draft_version.elements.not_nested.available.each do |element|
21
+ page.draft_version.elements.not_nested.published.each do |element|
22
22
  Alchemy::Element.copy(element, page_version_id: version.id)
23
23
  end
24
24
  end
@@ -7,7 +7,7 @@ module Alchemy::Upgrader::Tasks
7
7
  include Thor::Actions
8
8
 
9
9
  no_tasks do
10
- def create_ingredients
10
+ def create_ingredients(verbose: !Rails.env.test?)
11
11
  Alchemy::Deprecation.silence do
12
12
  elements_with_ingredients = Alchemy::ElementDefinition.all.select { |d| d.key?(:ingredients) }
13
13
  if ENV["ONLY"]
@@ -22,13 +22,13 @@ module Alchemy::Upgrader::Tasks
22
22
  elements_with_ingredients.map do |element_definition|
23
23
  elements = all_elements.select { |e| e.name == element_definition[:name] }
24
24
  if elements.any?
25
- puts "-- Creating ingredients for #{elements.count} #{element_definition[:name]}(s)"
25
+ puts "-- Creating ingredients for #{elements.count} #{element_definition[:name]}(s)" if verbose
26
26
  elements.each do |element|
27
27
  MigrateElementIngredients.call(element)
28
- print "."
28
+ print "." if verbose
29
29
  end
30
- puts "\n"
31
- else
30
+ puts "\n" if verbose
31
+ elsif verbose
32
32
  puts "-- No #{element_definition[:name]} elements found for migration."
33
33
  end
34
34
  end
@@ -56,6 +56,8 @@ module Alchemy::Upgrader::Tasks
56
56
  ingredient.value = content.ingredient
57
57
  end
58
58
  data = ingredient.class.stored_attributes.fetch(:data, []).each_with_object({}) do |attr, d|
59
+ next unless essence.respond_to?(attr)
60
+
59
61
  d[attr] = essence.public_send(attr)
60
62
  end
61
63
  ingredient.data = data
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Alchemy
4
- VERSION = "6.0.11"
4
+ VERSION = "6.1.0"
5
5
 
6
6
  def self.version
7
7
  VERSION
data/lib/alchemy.rb CHANGED
@@ -57,6 +57,10 @@ module Alchemy
57
57
  @_preview_sources ||= Set.new << Alchemy::Admin::PreviewUrl
58
58
  end
59
59
 
60
+ def self.preview_sources=(sources)
61
+ @_preview_sources = Array(sources)
62
+ end
63
+
60
64
  # Define page publish targets
61
65
  #
62
66
  # A publish target is a ActiveJob that gets performed
@@ -19,7 +19,7 @@
19
19
  <%- end -%>
20
20
  <%- end -%>
21
21
  <%- if @element['nestable_elements'].present? -%>
22
- <%%= render <%= @element_name %>.nested_elements.available %>
22
+ <%%= render <%= @element_name %>.nested_elements.published %>
23
23
  <%- end -%>
24
24
  <%%- end -%>
25
25
  <%%- end -%>
@@ -18,5 +18,5 @@
18
18
  <%- end -%>
19
19
  <%- end -%>
20
20
  <%- if @element['nestable_elements'].present? -%>
21
- = render <%= @element_name -%>.nested_elements.available
21
+ = render <%= @element_name -%>.nested_elements.published
22
22
  <%- end -%>
@@ -18,5 +18,5 @@
18
18
  <%- end -%>
19
19
  <%- end -%>
20
20
  <%- if @element['nestable_elements'].present? -%>
21
- = render <%= @element_name -%>.nested_elements.available
21
+ = render <%= @element_name -%>.nested_elements.published
22
22
  <%- end -%>
@@ -26,7 +26,22 @@ module Alchemy
26
26
  class_option :skip_db_create,
27
27
  type: :boolean,
28
28
  default: false,
29
- desc: "Skip creting the database during install."
29
+ desc: "Skip creating the database during install."
30
+
31
+ class_option :skip_mount,
32
+ type: :boolean,
33
+ default: false,
34
+ desc: "Skip mounting into routes.rb during install."
35
+
36
+ class_option :default_language_code,
37
+ type: :string,
38
+ default: "en",
39
+ desc: "The default language code of your site."
40
+
41
+ class_option :default_language_name,
42
+ type: :string,
43
+ default: "English",
44
+ desc: "The default language name of your site."
30
45
 
31
46
  source_root File.expand_path("files", __dir__)
32
47
 
@@ -34,6 +49,11 @@ module Alchemy
34
49
  header
35
50
  say "Welcome to AlchemyCMS!"
36
51
  say "Let's begin with some questions.\n\n"
52
+ end
53
+
54
+ def mount
55
+ return if options[:skip_mount]
56
+
37
57
  install_tasks.inject_routes(options[:auto_accept])
38
58
  end
39
59
 
@@ -108,13 +128,17 @@ module Alchemy
108
128
 
109
129
  def set_primary_language
110
130
  header
111
- install_tasks.set_primary_language(options[:auto_accept])
131
+ install_tasks.set_primary_language(
132
+ code: options[:default_language_code],
133
+ name: options[:default_language_name],
134
+ auto_accept: options[:auto_accept]
135
+ )
112
136
  end
113
137
 
114
138
  def setup_database
115
139
  rake("db:create", abort_on_failure: true) unless options[:skip_db_create]
116
140
  # We can't invoke this rake task, because Rails will use wrong engine names otherwise
117
- rake("railties:install:migrations", abort_on_failure: true)
141
+ rake("alchemy:install:migrations", abort_on_failure: true)
118
142
  rake("db:migrate", abort_on_failure: true)
119
143
  install_tasks.inject_seeder
120
144
  rake("db:seed", abort_on_failure: true)
@@ -1,4 +1,4 @@
1
- class <%= @class_name %>Ability
1
+ class <%= @controller_class %>Ability
2
2
  include CanCan::Ability
3
3
 
4
4
  def initialize(user)
@@ -7,5 +7,4 @@ class <%= @class_name %>Ability
7
7
  can :manage, :admin_<%= @controller_name %>
8
8
  end
9
9
  end
10
-
11
- end
10
+ end
@@ -1,17 +1,19 @@
1
- Alchemy::Modules.register_module({
2
- name: '<%= @module_name %>',
3
- order: 1,
4
- navigation: {
5
- name: 'modules.<%= @module_name %>',
6
- controller: '/admin/<%= @module_name %>',
7
- action: 'index',
8
- image: 'alchemy/<%= @module_name %>_module.png',
9
- sub_navigation: [{
1
+ Rails.application.config.to_prepare do
2
+ Alchemy::Modules.register_module({
3
+ name: '<%= @module_name %>',
4
+ order: 1,
5
+ navigation: {
10
6
  name: 'modules.<%= @module_name %>',
11
7
  controller: '/admin/<%= @module_name %>',
12
- action: 'index'
13
- }]
14
- }
15
- })
8
+ action: 'index',
9
+ image: 'alchemy/<%= @module_name %>_module.png',
10
+ sub_navigation: [{
11
+ name: 'modules.<%= @module_name %>',
12
+ controller: '/admin/<%= @module_name %>',
13
+ action: 'index'
14
+ }]
15
+ }
16
+ })
16
17
 
17
- Alchemy.register_ability(<%= @class_name %>Ability)
18
+ Alchemy.register_ability(<%= @controller_class %>Ability)
19
+ end
@@ -46,7 +46,7 @@ namespace :alchemy do
46
46
  ingredient_pictures = Alchemy::Ingredients::Picture.
47
47
  joins(:element).
48
48
  preload({ related_object: :thumbs }).
49
- merge(Alchemy::Element.available)
49
+ merge(Alchemy::Element.published)
50
50
 
51
51
  if ENV["ELEMENTS"].present?
52
52
  ingredient_pictures = ingredient_pictures.merge(
data/package/admin.js CHANGED
@@ -2,6 +2,7 @@ import translate from "./src/i18n"
2
2
  import translationData from "./src/translations"
3
3
  import NodeTree from "./src/node_tree"
4
4
  import fileEditors from "./src/file_editors"
5
+ import IngredientAnchorLink from "./src/ingredient_anchor_link"
5
6
  import pictureEditors from "./src/picture_editors"
6
7
  import ImageLoader from "./src/image_loader"
7
8
  import ImageCropper from "./src/image_cropper"
@@ -24,6 +25,7 @@ Object.assign(Alchemy, {
24
25
  pictureEditors,
25
26
  ImageLoader: ImageLoader.init,
26
27
  ImageCropper,
28
+ IngredientAnchorLink,
27
29
  Datepicker,
28
30
  Sitemap,
29
31
  PagePublicationFields
@@ -0,0 +1,17 @@
1
+ export default class IngredientAnchorLink {
2
+ static updateIcon(ingredientId, active = false) {
3
+ const ingredientEditor = document.querySelector(
4
+ `[data-ingredient-id="${ingredientId}"]`
5
+ )
6
+ if (ingredientEditor) {
7
+ const icon = ingredientEditor.querySelector(
8
+ ".edit-ingredient-anchor-link > a > .icon"
9
+ )
10
+ if (icon) {
11
+ active
12
+ ? icon.classList.replace("far", "fas")
13
+ : icon.classList.replace("fas", "far")
14
+ }
15
+ }
16
+ }
17
+ }
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alchemy_cms/admin",
3
- "version": "6.0.11",
3
+ "version": "6.1.0",
4
4
  "description": "AlchemyCMS",
5
5
  "browser": "package/admin.js",
6
6
  "files": [
@@ -31,7 +31,7 @@
31
31
  "devDependencies": {
32
32
  "@babel/core": "^7.9.6",
33
33
  "@babel/preset-env": "^7.9.6",
34
- "babel-jest": "^27.0.1",
34
+ "babel-jest": "^29.0.1",
35
35
  "jest": "^25.2.7",
36
36
  "prettier": "^2.0.2",
37
37
  "xhr-mock": "^2.5.1"