alchemy_cms 8.0.0.a → 8.0.0.c
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.
- checksums.yaml +4 -4
- data/README.md +3 -0
- data/app/assets/builds/alchemy/admin/page-select.css +1 -1
- data/app/assets/builds/alchemy/admin.css +1 -1
- data/app/assets/builds/alchemy/dark-theme.css +1 -0
- data/app/assets/builds/alchemy/light-theme.css +1 -0
- data/app/assets/builds/alchemy/theme.css +1 -0
- data/app/assets/builds/alchemy/welcome.css +1 -1
- data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
- data/app/assets/builds/tinymce/skins/content/alchemy-dark/content.min.css +1 -0
- data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css +1 -1
- data/app/assets/builds/tinymce/skins/ui/alchemy-dark/content.min.css +1 -0
- data/app/assets/builds/tinymce/skins/ui/alchemy-dark/skin.min.css +1 -0
- data/app/assets/images/alchemy/element_icons/layout-bottom-2-line.svg +1 -0
- data/app/assets/images/alchemy/icons-sprite.svg +1 -1
- data/app/components/alchemy/admin/element_select.rb +39 -0
- data/app/components/alchemy/admin/link_dialog/tabs.rb +1 -1
- data/app/components/alchemy/admin/locale_select.rb +38 -0
- data/app/components/alchemy/ingredients/datetime_view.rb +4 -2
- data/app/controllers/alchemy/admin/attachments_controller.rb +2 -0
- data/app/controllers/alchemy/admin/elements_controller.rb +2 -0
- data/app/controllers/alchemy/admin/pages_controller.rb +3 -1
- data/app/controllers/alchemy/admin/pictures_controller.rb +26 -34
- data/app/controllers/alchemy/admin/resources_controller.rb +1 -1
- data/app/controllers/alchemy/pages_controller.rb +19 -2
- data/app/controllers/concerns/alchemy/admin/resource_filter.rb +1 -0
- data/app/decorators/alchemy/ingredient_editor.rb +9 -1
- data/app/helpers/alchemy/admin/attachments_helper.rb +5 -5
- data/app/helpers/alchemy/admin/base_helper.rb +0 -7
- data/app/helpers/alchemy/admin/form_helper.rb +2 -1
- data/app/helpers/alchemy/pages_helper.rb +1 -1
- data/app/javascript/alchemy_admin/components/auto_submit.js +20 -0
- data/app/javascript/alchemy_admin/components/datepicker.js +8 -5
- data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +3 -2
- data/app/javascript/alchemy_admin/components/element_editor.js +25 -15
- data/app/javascript/alchemy_admin/components/element_select.js +43 -0
- data/app/javascript/alchemy_admin/components/index.js +5 -0
- data/app/javascript/alchemy_admin/components/link_buttons.js +6 -2
- data/app/javascript/alchemy_admin/components/remote_select.js +5 -1
- data/app/javascript/alchemy_admin/components/tinymce.js +93 -16
- data/app/javascript/alchemy_admin/dialog.js +1 -1
- data/app/javascript/alchemy_admin/file_editors.js +1 -1
- data/app/javascript/alchemy_admin/image_loader.js +4 -2
- data/app/javascript/alchemy_admin/picture_editors.js +7 -4
- data/app/javascript/alchemy_admin/picture_selector.js +4 -4
- data/app/jobs/alchemy/delete_picture_job.rb +12 -0
- data/app/models/alchemy/attachment.rb +2 -9
- data/app/models/alchemy/element.rb +1 -0
- data/app/models/alchemy/element_definition.rb +31 -0
- data/app/models/alchemy/ingredient.rb +1 -1
- data/app/models/alchemy/ingredients/boolean.rb +2 -1
- data/app/models/alchemy/language.rb +2 -7
- data/app/models/alchemy/page/page_naming.rb +4 -11
- data/app/models/alchemy/page/page_natures.rb +16 -11
- data/app/models/alchemy/page/publisher.rb +1 -1
- data/app/models/alchemy/page.rb +1 -6
- data/app/models/alchemy/page_definition.rb +1 -1
- data/app/models/alchemy/picture.rb +6 -17
- data/app/models/alchemy/resource.rb +15 -2
- data/app/models/alchemy/site/layout.rb +1 -0
- data/app/models/alchemy/site.rb +1 -6
- data/app/models/alchemy/storage_adapter/dragonfly/picture_url.rb +7 -2
- data/app/models/alchemy/storage_adapter/dragonfly.rb +24 -2
- data/app/models/concerns/alchemy/relatable_resource.rb +28 -0
- data/app/stylesheets/alchemy/_custom-properties.scss +162 -0
- data/app/stylesheets/alchemy/_mixins.scss +12 -24
- data/app/stylesheets/alchemy/_themes.scss +540 -0
- data/app/stylesheets/alchemy/admin/archive.scss +28 -8
- data/app/stylesheets/alchemy/admin/attachments.scss +10 -33
- data/app/stylesheets/alchemy/admin/base.scss +4 -1
- data/app/stylesheets/alchemy/admin/buttons.scss +7 -32
- data/app/stylesheets/alchemy/admin/dashboard.scss +13 -0
- data/app/stylesheets/alchemy/admin/dialogs.scss +17 -7
- data/app/stylesheets/alchemy/admin/element-select.scss +11 -0
- data/app/stylesheets/alchemy/admin/elements.scss +95 -34
- data/app/stylesheets/alchemy/admin/filters.scss +8 -9
- data/app/stylesheets/alchemy/admin/flatpickr.scss +12 -27
- data/app/stylesheets/alchemy/admin/form_fields.scss +0 -15
- data/app/stylesheets/alchemy/admin/forms.scss +3 -8
- data/app/stylesheets/alchemy/admin/frame.scss +5 -7
- data/app/stylesheets/alchemy/admin/icons.scss +0 -9
- data/app/stylesheets/alchemy/admin/image_library.scss +13 -55
- data/app/stylesheets/alchemy/admin/navigation.scss +1 -11
- data/app/stylesheets/alchemy/admin/node-select.scss +1 -10
- data/app/stylesheets/alchemy/admin/nodes.scss +6 -2
- data/app/stylesheets/alchemy/admin/notices.scss +5 -4
- data/app/stylesheets/alchemy/admin/page-select.scss +16 -0
- data/app/stylesheets/alchemy/admin/pagination.scss +1 -8
- data/app/stylesheets/alchemy/admin/preview_window.scss +12 -1
- data/app/stylesheets/alchemy/admin/resource_info.scss +106 -3
- data/app/stylesheets/alchemy/admin/search.scss +1 -1
- data/app/stylesheets/alchemy/admin/selects.scss +58 -31
- data/app/stylesheets/alchemy/admin/shoelace.scss +32 -62
- data/app/stylesheets/alchemy/admin/sitemap.scss +7 -18
- data/app/stylesheets/alchemy/admin/tables.scss +3 -3
- data/app/stylesheets/alchemy/admin/tags.scss +18 -35
- data/app/stylesheets/alchemy/admin/toolbar.scss +0 -6
- data/app/stylesheets/alchemy/admin/typography.scss +2 -5
- data/app/stylesheets/alchemy/admin.scss +1 -1
- data/app/stylesheets/alchemy/dark-theme.scss +5 -0
- data/app/stylesheets/alchemy/light-theme.scss +6 -0
- data/app/stylesheets/alchemy/theme.scss +13 -0
- data/app/stylesheets/tinymce/skins/content/alchemy/content.scss +8 -8
- data/app/stylesheets/tinymce/skins/content/alchemy-dark/content.scss +70 -0
- data/app/stylesheets/tinymce/skins/ui/alchemy/skin.scss +28 -43
- data/app/stylesheets/tinymce/skins/ui/alchemy-dark/content.scss +1 -0
- data/app/stylesheets/tinymce/skins/ui/alchemy-dark/skin.scss +3784 -0
- data/app/views/alchemy/admin/attachments/_files_list.html.erb +20 -10
- data/app/views/alchemy/admin/attachments/assign.js.erb +4 -3
- data/app/views/alchemy/admin/attachments/show.html.erb +55 -43
- data/app/views/alchemy/admin/crop.html.erb +1 -1
- data/app/views/alchemy/admin/dashboard/index.html.erb +1 -1
- data/app/views/alchemy/admin/dashboard/info.html.erb +36 -6
- data/app/views/alchemy/admin/elements/_form.html.erb +9 -9
- data/app/views/alchemy/admin/elements/_header.html.erb +12 -10
- data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +1 -1
- data/app/views/alchemy/admin/nodes/_form.html.erb +5 -1
- data/app/views/alchemy/admin/pages/info.html.erb +1 -1
- data/app/views/alchemy/admin/partials/_search_form.html.erb +1 -0
- data/app/views/alchemy/admin/pictures/_archive.html.erb +13 -23
- data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +1 -6
- data/app/views/alchemy/admin/pictures/_form.html.erb +10 -5
- data/app/views/alchemy/admin/pictures/_infos.html.erb +21 -52
- data/app/views/alchemy/admin/pictures/_library_sidebar.html.erb +7 -0
- data/app/views/alchemy/admin/pictures/_picture.html.erb +15 -16
- data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +20 -16
- data/app/views/alchemy/admin/pictures/_sorting_select.html.erb +13 -0
- data/app/views/alchemy/admin/pictures/_tag_list.html.erb +1 -1
- data/app/views/alchemy/admin/pictures/edit_multiple.html.erb +1 -6
- data/app/views/alchemy/admin/pictures/index.html.erb +3 -12
- data/app/views/alchemy/admin/pictures/show.html.erb +17 -14
- data/app/views/alchemy/admin/pictures/update.turbo_stream.erb +1 -1
- data/app/views/alchemy/admin/resources/_filter_bar.html.erb +5 -15
- data/app/views/alchemy/admin/resources/_resource_usage_info.html.erb +36 -0
- data/app/views/alchemy/admin/styleguide/index.html.erb +118 -66
- data/app/views/alchemy/admin/uploader/_button.html.erb +1 -1
- data/app/views/alchemy/base/error_notice.html.erb +1 -1
- data/app/views/alchemy/ingredients/_page_editor.html.erb +0 -1
- data/app/views/alchemy/ingredients/_richtext_editor.html.erb +0 -1
- data/app/views/alchemy/ingredients/_select_editor.html.erb +1 -2
- data/app/views/layouts/alchemy/admin.html.erb +25 -23
- data/config/locales/alchemy.en.yml +26 -8
- data/db/migrate/20250905140323_add_created_at_index_to_pictures_and_attachments.rb +14 -0
- data/lib/alchemy/configuration/base_option.rb +18 -5
- data/lib/alchemy/configuration/boolean_option.rb +2 -5
- data/lib/alchemy/configuration/collection_option.rb +69 -0
- data/lib/alchemy/configuration/configuration_option.rb +35 -0
- data/lib/alchemy/configuration/pathname_option.rb +12 -0
- data/lib/alchemy/configuration.rb +44 -6
- data/lib/alchemy/configurations/format_matchers.rb +1 -1
- data/lib/alchemy/configurations/importmap.rb +11 -0
- data/lib/alchemy/configurations/mailer.rb +2 -2
- data/lib/alchemy/configurations/main.rb +148 -3
- data/lib/alchemy/configurations/page_cache.rb +19 -0
- data/lib/alchemy/configurations/uploader.rb +2 -2
- data/lib/alchemy/deprecation.rb +1 -1
- data/lib/alchemy/engine.rb +43 -21
- data/lib/alchemy/install/tasks.rb +0 -12
- data/lib/alchemy/name_conversions.rb +6 -0
- data/lib/alchemy/tasks/tidy.rb +18 -0
- data/lib/alchemy/test_support/config_stubbing.rb +13 -4
- data/lib/alchemy/test_support/factories/language_factory.rb +8 -4
- data/lib/alchemy/test_support/factories/page_factory.rb +1 -0
- data/lib/alchemy/test_support/factories/picture_factory.rb +1 -0
- data/lib/alchemy/test_support/relatable_resource_examples.rb +58 -0
- data/lib/alchemy/tinymce.rb +0 -1
- data/lib/alchemy/version.rb +1 -1
- data/lib/alchemy.rb +18 -171
- data/lib/generators/alchemy/install/install_generator.rb +21 -10
- data/lib/generators/alchemy/install/templates/alchemy.rb.tt +88 -13
- data/lib/tasks/alchemy/assets.rake +1 -1
- data/lib/tasks/alchemy/tidy.rake +6 -0
- data/lib/tasks/alchemy/usage.rake +2 -0
- data/vendor/assets/stylesheets/tinymce/skins/content/dark/content.min.css +1 -0
- data/vendor/assets/stylesheets/tinymce/skins/content/default/content.min.css +1 -0
- data/vendor/assets/stylesheets/tinymce/skins/ui/oxide/skin.min.css +1 -0
- data/vendor/assets/stylesheets/tinymce/skins/ui/oxide-dark/content.min.css +1 -0
- data/vendor/assets/stylesheets/tinymce/skins/ui/oxide-dark/skin.min.css +1 -0
- data/vendor/javascript/clipboard.min.js +1 -1
- data/vendor/javascript/cropperjs.min.js +1 -1
- data/vendor/javascript/handlebars.min.js +3 -3
- data/vendor/javascript/jquery.min.js +1 -1
- data/vendor/javascript/select2.min.js +3 -3
- data/vendor/javascript/shoelace.min.js +92 -76
- data/vendor/javascript/sortable.min.js +2 -2
- data/vendor/javascript/tinymce.min.js +1 -1
- data/vendor/javascript/ungap-custom-elements.min.js +2 -2
- metadata +51 -36
- data/CHANGELOG.md +0 -2100
- data/CODE_OF_CONDUCT.md +0 -13
- data/CONTRIBUTING.md +0 -73
- data/Gemfile +0 -78
- data/Rakefile +0 -102
- data/SECURITY.md +0 -13
- data/alchemy_cms.gemspec +0 -97
- data/app/assets/builds/alchemy/custom-properties.css +0 -1
- data/app/helpers/alchemy/admin/elements_helper.rb +0 -25
- data/app/stylesheets/alchemy/custom-properties.css +0 -244
- data/bin/importmap +0 -4
- data/bin/rails +0 -9
- data/bin/rspec +0 -3
- data/bin/setup +0 -30
- data/bin/start +0 -17
- data/bun.lockb +0 -0
- data/bundles/remixicon.mjs +0 -153
- data/bundles/shoelace.js +0 -12
- data/bundles/tinymce.js +0 -22
- data/eslint.config.js +0 -18
- data/lib/alchemy/configuration/class_set_option.rb +0 -46
- data/lib/alchemy/configuration/integer_list_option.rb +0 -13
- data/lib/alchemy/configuration/list_option.rb +0 -22
- data/lib/alchemy/configuration/string_list_option.rb +0 -13
- data/lib/alchemy/upgrader/.keep +0 -0
- data/lib/alchemy/upgrader/tasks/.keep +0 -0
- data/rollup.config.mjs +0 -108
- data/vitest.config.js +0 -21
|
@@ -9,15 +9,28 @@ module Alchemy
|
|
|
9
9
|
|
|
10
10
|
def initialize(value:, name:, **args)
|
|
11
11
|
@name = name
|
|
12
|
-
|
|
12
|
+
validate(value) unless value.nil?
|
|
13
|
+
@value = value
|
|
13
14
|
end
|
|
14
15
|
attr_reader :name, :value
|
|
15
16
|
|
|
16
|
-
private
|
|
17
|
-
|
|
18
17
|
def validate(value)
|
|
19
|
-
raise
|
|
20
|
-
|
|
18
|
+
raise ConfigurationError.new(name, value, allowed_classes) unless allowed_classes.any? { value.is_a?(_1) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def allowed_classes
|
|
22
|
+
[self.class.value_class]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def raw_value = @value
|
|
26
|
+
|
|
27
|
+
def ==(other)
|
|
28
|
+
self.class == other.class && raw_value == other.raw_value
|
|
29
|
+
end
|
|
30
|
+
alias_method :eql?, :==
|
|
31
|
+
|
|
32
|
+
def hash
|
|
33
|
+
[self.class, raw_value].hash
|
|
21
34
|
end
|
|
22
35
|
end
|
|
23
36
|
end
|
|
@@ -5,11 +5,8 @@ require "alchemy/configuration/base_option"
|
|
|
5
5
|
module Alchemy
|
|
6
6
|
class Configuration
|
|
7
7
|
class BooleanOption < BaseOption
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def validate(value)
|
|
11
|
-
raise TypeError, "#{name} must be a Boolean, given #{value.inspect}" unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
12
|
-
value
|
|
8
|
+
def allowed_classes
|
|
9
|
+
[TrueClass, FalseClass]
|
|
13
10
|
end
|
|
14
11
|
end
|
|
15
12
|
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "alchemy/configuration/base_option"
|
|
4
|
+
|
|
5
|
+
module Alchemy
|
|
6
|
+
class Configuration
|
|
7
|
+
class CollectionOption < BaseOption
|
|
8
|
+
include Enumerable
|
|
9
|
+
|
|
10
|
+
def self.value_class
|
|
11
|
+
Enumerable
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
attr_reader :collection_class, :item_class, :item_args
|
|
15
|
+
|
|
16
|
+
def initialize(value:, name:, item_type:, collection_class: Array, **args)
|
|
17
|
+
@collection_class = collection_class
|
|
18
|
+
@item_class = get_item_class(item_type)
|
|
19
|
+
@item_args = args
|
|
20
|
+
value = [] if value.nil?
|
|
21
|
+
collection = @collection_class.new(value.map { |value| to_item(value) })
|
|
22
|
+
super(value: collection, name: name)
|
|
23
|
+
rescue ConfigurationError => configuration_error
|
|
24
|
+
raise ConfigurationError.new(name, configuration_error.value, configuration_error.allowed_classes)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def value
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def <<(value)
|
|
32
|
+
@value << to_item(value)
|
|
33
|
+
end
|
|
34
|
+
alias_method(:add, :<<)
|
|
35
|
+
|
|
36
|
+
def concat(values)
|
|
37
|
+
values.each do |value|
|
|
38
|
+
add(value)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
delegate :join, :[], to: :to_a
|
|
43
|
+
|
|
44
|
+
delegate :clear, :empty?, to: :@value
|
|
45
|
+
|
|
46
|
+
def each(&block)
|
|
47
|
+
@value.each do |option|
|
|
48
|
+
yield option.value
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def to_serializable_array
|
|
53
|
+
to_a.map do |item|
|
|
54
|
+
item.respond_to?(:to_h) ? item.to_h : item
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def to_item(value)
|
|
61
|
+
@item_class.new(value: value, name: "#{name}_item", **item_args)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def get_item_class(item_type)
|
|
65
|
+
"Alchemy::Configuration::#{item_type.to_s.classify}Option".constantize
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "alchemy/configuration/base_option"
|
|
4
|
+
|
|
5
|
+
module Alchemy
|
|
6
|
+
class Configuration
|
|
7
|
+
class ConfigurationOption < BaseOption
|
|
8
|
+
def self.value_class
|
|
9
|
+
Hash
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
attr_reader :config_class
|
|
13
|
+
|
|
14
|
+
def initialize(value:, name:, config_class:, **args)
|
|
15
|
+
@name = name
|
|
16
|
+
@config_class = config_class
|
|
17
|
+
validate(value)
|
|
18
|
+
@value = if value.is_a?(config_class)
|
|
19
|
+
value
|
|
20
|
+
else
|
|
21
|
+
config_class.new(value)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def validate(value)
|
|
26
|
+
return true if value.is_a?(config_class)
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def allowed_classes
|
|
31
|
+
super + [config_class]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -4,16 +4,28 @@ require "active_support"
|
|
|
4
4
|
require "active_support/core_ext/string"
|
|
5
5
|
|
|
6
6
|
require "alchemy/configuration/boolean_option"
|
|
7
|
+
require "alchemy/configuration/collection_option"
|
|
8
|
+
require "alchemy/configuration/configuration_option"
|
|
7
9
|
require "alchemy/configuration/class_option"
|
|
8
|
-
require "alchemy/configuration/class_set_option"
|
|
9
10
|
require "alchemy/configuration/integer_option"
|
|
10
|
-
require "alchemy/configuration/
|
|
11
|
+
require "alchemy/configuration/pathname_option"
|
|
11
12
|
require "alchemy/configuration/regexp_option"
|
|
12
|
-
require "alchemy/configuration/string_list_option"
|
|
13
13
|
require "alchemy/configuration/string_option"
|
|
14
14
|
|
|
15
15
|
module Alchemy
|
|
16
16
|
class Configuration
|
|
17
|
+
class ConfigurationError < StandardError
|
|
18
|
+
attr_reader :name, :value, :allowed_classes
|
|
19
|
+
|
|
20
|
+
def initialize(name, value, allowed_classes)
|
|
21
|
+
@name = name
|
|
22
|
+
@value = value
|
|
23
|
+
@allowed_classes = allowed_classes
|
|
24
|
+
expected_classes_message = allowed_classes.map(&:name).to_sentence(two_words_connector: " or ", last_word_connector: ", or ")
|
|
25
|
+
super("Invalid configuration value for #{name}: #{value.inspect} (expected #{expected_classes_message})")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
17
29
|
def initialize(configuration_hash = {})
|
|
18
30
|
set(configuration_hash)
|
|
19
31
|
end
|
|
@@ -45,7 +57,8 @@ module Alchemy
|
|
|
45
57
|
|
|
46
58
|
def to_h
|
|
47
59
|
self.class.defined_options.map do |option|
|
|
48
|
-
|
|
60
|
+
value = send(option)
|
|
61
|
+
[option, value.respond_to?(:to_serializable_array) ? value.to_serializable_array : value]
|
|
49
62
|
end.concat(
|
|
50
63
|
self.class.defined_configurations.map do |configuration|
|
|
51
64
|
[configuration, send(configuration).to_h]
|
|
@@ -58,6 +71,10 @@ module Alchemy
|
|
|
58
71
|
|
|
59
72
|
def defined_options = []
|
|
60
73
|
|
|
74
|
+
def defined_values
|
|
75
|
+
defined_options + defined_configurations
|
|
76
|
+
end
|
|
77
|
+
|
|
61
78
|
def configuration(name, configuration_class)
|
|
62
79
|
# The defined configurations on a class are all those defined directly on
|
|
63
80
|
# that class as well as those defined on ancestors.
|
|
@@ -99,11 +116,19 @@ module Alchemy
|
|
|
99
116
|
super() + singleton_options
|
|
100
117
|
end
|
|
101
118
|
|
|
102
|
-
define_method(name) do
|
|
119
|
+
define_method("#{name}_option") do
|
|
103
120
|
unless instance_variable_defined?(:"@#{name}")
|
|
104
121
|
send(:"#{name}=", default)
|
|
105
122
|
end
|
|
106
|
-
instance_variable_get(:"@#{name}")
|
|
123
|
+
instance_variable_get(:"@#{name}")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
define_method(name) do
|
|
127
|
+
send("#{name}_option").value
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
define_method("raw_#{name}") do
|
|
131
|
+
send("#{name}_option").raw_value
|
|
107
132
|
end
|
|
108
133
|
|
|
109
134
|
define_method(:"#{name}=") do |value|
|
|
@@ -111,5 +136,18 @@ module Alchemy
|
|
|
111
136
|
end
|
|
112
137
|
end
|
|
113
138
|
end
|
|
139
|
+
|
|
140
|
+
def hash
|
|
141
|
+
self.class.defined_values.map do |ivar|
|
|
142
|
+
[ivar, send(ivar).hash]
|
|
143
|
+
end.hash
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def ==(other)
|
|
147
|
+
equal?(other) || self.class == other.class && self.class.defined_values.all? do |var|
|
|
148
|
+
send(var) == other.send(var)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
alias_method :eql?, :==
|
|
114
152
|
end
|
|
115
153
|
end
|
|
@@ -4,7 +4,7 @@ module Alchemy
|
|
|
4
4
|
module Configurations
|
|
5
5
|
class FormatMatchers < Alchemy::Configuration
|
|
6
6
|
option :email, :regexp, default: /\A[^@\s]+@([^@\s]+\.)+[^@\s]+\z/
|
|
7
|
-
option :url, :regexp, default: /\A[a-z0-9]+([
|
|
7
|
+
option :url, :regexp, default: /\A[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?\z/ix
|
|
8
8
|
option :link_url, :regexp, default: /^(tel:|mailto:|\/|[a-z]+:\/\/)/
|
|
9
9
|
end
|
|
10
10
|
end
|
|
@@ -9,8 +9,8 @@ module Alchemy
|
|
|
9
9
|
option :mail_from, :string, default: "your.mail@your-domain.com"
|
|
10
10
|
option :mail_to, :string, default: "your.mail@your-domain.com"
|
|
11
11
|
option :subject, :string, default: "A new contact form message"
|
|
12
|
-
option :fields, :
|
|
13
|
-
option :validate_fields, :
|
|
12
|
+
option :fields, :collection, item_type: :string, default: %w[salutation firstname lastname address zip city phone email message]
|
|
13
|
+
option :validate_fields, :collection, item_type: :string, default: %w[lastname email]
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
16
|
end
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
require "alchemy/configuration"
|
|
4
4
|
require "alchemy/configurations/default_language"
|
|
5
5
|
require "alchemy/configurations/default_site"
|
|
6
|
+
require "alchemy/configurations/importmap"
|
|
6
7
|
require "alchemy/configurations/format_matchers"
|
|
7
8
|
require "alchemy/configurations/mailer"
|
|
9
|
+
require "alchemy/configurations/page_cache"
|
|
8
10
|
require "alchemy/configurations/preview"
|
|
9
11
|
require "alchemy/configurations/sitemap"
|
|
10
12
|
require "alchemy/configurations/uploader"
|
|
@@ -31,6 +33,12 @@ module Alchemy
|
|
|
31
33
|
#
|
|
32
34
|
option :cache_pages, :boolean, default: true
|
|
33
35
|
|
|
36
|
+
# === Page caching max age
|
|
37
|
+
#
|
|
38
|
+
# max-age [Integer] # The duration in seconds for which the page is cached before revalidation.
|
|
39
|
+
# stale-while-revalidate [Boolean] # If true, enables the stale-while-revalidate caching strategy.
|
|
40
|
+
configuration :page_cache, PageCache
|
|
41
|
+
|
|
34
42
|
# === Sitemap
|
|
35
43
|
#
|
|
36
44
|
# Alchemy creates a XML, Google compatible, sitemap for you.
|
|
@@ -155,7 +163,7 @@ module Alchemy
|
|
|
155
163
|
# user_roles:
|
|
156
164
|
# rolename: Name of the role
|
|
157
165
|
#
|
|
158
|
-
option :user_roles, :
|
|
166
|
+
option :user_roles, :collection, item_type: :string, default: %w[member author editor admin]
|
|
159
167
|
|
|
160
168
|
# === Uploader Settings
|
|
161
169
|
#
|
|
@@ -177,7 +185,7 @@ module Alchemy
|
|
|
177
185
|
#
|
|
178
186
|
# jQuery(a[data-link-target="overlay"]).dialog();
|
|
179
187
|
#
|
|
180
|
-
option :link_target_options, :
|
|
188
|
+
option :link_target_options, :collection, item_type: :string, default: %w[blank]
|
|
181
189
|
|
|
182
190
|
# === Format matchers
|
|
183
191
|
#
|
|
@@ -194,7 +202,7 @@ module Alchemy
|
|
|
194
202
|
option :admin_page_preview_layout, :string, default: "application"
|
|
195
203
|
|
|
196
204
|
# The sizes for the preview size select in the page editor.
|
|
197
|
-
option :page_preview_sizes, :
|
|
205
|
+
option :page_preview_sizes, :collection, item_type: :integer, default: [360, 640, 768, 1024, 1280, 1440]
|
|
198
206
|
|
|
199
207
|
# Enable full text search configuration
|
|
200
208
|
#
|
|
@@ -211,6 +219,143 @@ module Alchemy
|
|
|
211
219
|
# The storage adapter for Pictures and Attachments
|
|
212
220
|
#
|
|
213
221
|
option :storage_adapter, :string, default: "dragonfly"
|
|
222
|
+
|
|
223
|
+
# Define page preview sources
|
|
224
|
+
#
|
|
225
|
+
# A preview source is a Ruby class returning an URL
|
|
226
|
+
# that is used as source for the preview frame in the
|
|
227
|
+
# admin UI.
|
|
228
|
+
#
|
|
229
|
+
# == Example
|
|
230
|
+
#
|
|
231
|
+
# # lib/acme/preview_source.rb
|
|
232
|
+
# class Acme::PreviewSource < Alchemy::Admin::PreviewUrl
|
|
233
|
+
# def url_for(page)
|
|
234
|
+
# if page.site.name == "Next"
|
|
235
|
+
# "https://user:#{ENV['PREVIEW_HTTP_PASS']}@next.acme.com"
|
|
236
|
+
# else
|
|
237
|
+
# "https://www.acme.com"
|
|
238
|
+
# end
|
|
239
|
+
# end
|
|
240
|
+
# end
|
|
241
|
+
#
|
|
242
|
+
# # config/initializers/alchemy.rb
|
|
243
|
+
# require "acme/preview_source"
|
|
244
|
+
# Alchemy.config.preview_sources << "Acme::PreviewSource"
|
|
245
|
+
#
|
|
246
|
+
# # config/locales/de.yml
|
|
247
|
+
# de:
|
|
248
|
+
# activemodel:
|
|
249
|
+
# models:
|
|
250
|
+
# acme/preview_source: Acme Vorschau
|
|
251
|
+
#
|
|
252
|
+
option :preview_sources, :collection, item_type: :class, collection_class: Set, default: ["Alchemy::Admin::PreviewUrl"]
|
|
253
|
+
|
|
254
|
+
# Additional JS modules to be imported in the Alchemy admin UI
|
|
255
|
+
#
|
|
256
|
+
# Be sure to also pin the modules with +Alchemy.importmap+.
|
|
257
|
+
#
|
|
258
|
+
# == Example
|
|
259
|
+
#
|
|
260
|
+
# Alchemy.importmap.pin "flatpickr/de",
|
|
261
|
+
# to: "https://ga.jspm.io/npm:flatpickr@4.6.13/dist/l10n/de.js"
|
|
262
|
+
#
|
|
263
|
+
# Alchemy.config.admin_js_imports << "flatpickr/de"
|
|
264
|
+
#
|
|
265
|
+
option :admin_js_imports, :collection, item_type: :string, collection_class: Set, default: []
|
|
266
|
+
|
|
267
|
+
# Additional importmaps to be included in the Alchemy admin UI
|
|
268
|
+
#
|
|
269
|
+
# Be sure to also pin modules with +Alchemy.importmap+.
|
|
270
|
+
#
|
|
271
|
+
# == Example
|
|
272
|
+
#
|
|
273
|
+
# # config/alchemy/importmap.rb
|
|
274
|
+
# Alchemy.importmap.pin "alchemy_solidus", to: "alchemy_solidus.js", preload: true
|
|
275
|
+
# Alchemy.importmap.pin_all_from Alchemy::Solidus::Engine.root.join("app/javascript/alchemy_solidus"),
|
|
276
|
+
# under: "alchemy_solidus",
|
|
277
|
+
# preload: true
|
|
278
|
+
#
|
|
279
|
+
# # lib/alchemy/solidus/engine.rb
|
|
280
|
+
# initializer "alchemy_solidus.assets", before: "alchemy.importmap" do |app|
|
|
281
|
+
# Alchemy.admin_importmaps.add({
|
|
282
|
+
# importmap_path: root.join("config/importmap.rb"),
|
|
283
|
+
# source_paths: [
|
|
284
|
+
# root.join("app/javascript")
|
|
285
|
+
# ],
|
|
286
|
+
# name: "alchemy_solidus"
|
|
287
|
+
# })
|
|
288
|
+
# app.config.assets.precompile << "alchemy_solidus/manifest.js"
|
|
289
|
+
# end
|
|
290
|
+
#
|
|
291
|
+
option :admin_importmaps, :collection, collection_class: Set, item_type: :configuration, config_class: Alchemy::Configurations::Importmap, default: []
|
|
292
|
+
|
|
293
|
+
# Additional stylesheets to be included in the Alchemy admin UI
|
|
294
|
+
#
|
|
295
|
+
# == Example
|
|
296
|
+
#
|
|
297
|
+
# # lib/alchemy/devise/engine.rb
|
|
298
|
+
# initializer "alchemy.devise.stylesheets", before: "alchemy.admin_stylesheets" do
|
|
299
|
+
# Alchemy.config.admin_stylesheets << "alchemy/devise/admin.css"
|
|
300
|
+
# end
|
|
301
|
+
#
|
|
302
|
+
option :admin_stylesheets, :collection, collection_class: Set, item_type: :string, default: ["alchemy/admin/custom.css"]
|
|
303
|
+
|
|
304
|
+
# Define page publish targets
|
|
305
|
+
#
|
|
306
|
+
# A publish target is a ActiveJob that gets performed
|
|
307
|
+
# whenever a user clicks the publish page button.
|
|
308
|
+
#
|
|
309
|
+
# Use this to trigger deployment hooks of external
|
|
310
|
+
# services in an asychronous way.
|
|
311
|
+
#
|
|
312
|
+
# == Example
|
|
313
|
+
#
|
|
314
|
+
# # app/jobs/publish_job.rb
|
|
315
|
+
# class PublishJob < ApplicationJob
|
|
316
|
+
# def perform(page)
|
|
317
|
+
# RestClient.post(ENV['BUILD_HOOK_URL'])
|
|
318
|
+
# end
|
|
319
|
+
# end
|
|
320
|
+
#
|
|
321
|
+
# # config/initializers/alchemy.rb
|
|
322
|
+
# Alchemy.config.publish_targets << PublishJob
|
|
323
|
+
#
|
|
324
|
+
option :publish_targets, :collection, collection_class: Set, item_type: :class, default: []
|
|
325
|
+
|
|
326
|
+
# Configure tabs in the link dialog
|
|
327
|
+
#
|
|
328
|
+
# With this configuration that tabs in the link dialog can be extended
|
|
329
|
+
# without overwriting or defacing the Admin Interface.
|
|
330
|
+
#
|
|
331
|
+
# == Example
|
|
332
|
+
#
|
|
333
|
+
# # components/acme/link_tab.rb
|
|
334
|
+
# module Acme
|
|
335
|
+
# class LinkTab < ::Alchemy::Admin::LinkDialog::BaseTab
|
|
336
|
+
# def title
|
|
337
|
+
# "Awesome Tab Title"
|
|
338
|
+
# end
|
|
339
|
+
#
|
|
340
|
+
# def name
|
|
341
|
+
# :unique_name
|
|
342
|
+
# end
|
|
343
|
+
#
|
|
344
|
+
# def fields
|
|
345
|
+
# [ title_input, target_select ]
|
|
346
|
+
# end
|
|
347
|
+
# end
|
|
348
|
+
# end
|
|
349
|
+
#
|
|
350
|
+
# # config/initializers/alchemy.rb
|
|
351
|
+
# Alchemy.config.link_dialog_tabs << "Acme::LinkTab"
|
|
352
|
+
#
|
|
353
|
+
option :link_dialog_tabs, :collection, collection_class: Set, item_type: :class, default: [
|
|
354
|
+
"Alchemy::Admin::LinkDialog::InternalTab",
|
|
355
|
+
"Alchemy::Admin::LinkDialog::AnchorTab",
|
|
356
|
+
"Alchemy::Admin::LinkDialog::ExternalTab",
|
|
357
|
+
"Alchemy::Admin::LinkDialog::FileTab"
|
|
358
|
+
]
|
|
214
359
|
end
|
|
215
360
|
end
|
|
216
361
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
module Configurations
|
|
5
|
+
class PageCache < Alchemy::Configuration
|
|
6
|
+
# === Page caching max age
|
|
7
|
+
#
|
|
8
|
+
# Control the max-age duration in seconds in the cache-control header.
|
|
9
|
+
#
|
|
10
|
+
option :max_age, :integer, default: 600
|
|
11
|
+
|
|
12
|
+
# === Page caching stale-while-revalidate
|
|
13
|
+
#
|
|
14
|
+
# Set stale-while-revalidate cache-control header.
|
|
15
|
+
#
|
|
16
|
+
option :stale_while_revalidate, :integer
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -4,8 +4,8 @@ module Alchemy
|
|
|
4
4
|
module Configurations
|
|
5
5
|
class Uploader < Alchemy::Configuration
|
|
6
6
|
class AllowedFileTypes < Alchemy::Configuration
|
|
7
|
-
option :alchemy_attachments, :
|
|
8
|
-
option :alchemy_pictures, :
|
|
7
|
+
option :alchemy_attachments, :collection, item_type: :string, default: ["*"]
|
|
8
|
+
option :alchemy_pictures, :collection, item_type: :string, default: %w[jpg jpeg gif png svg webp]
|
|
9
9
|
|
|
10
10
|
def set(configuration_hash)
|
|
11
11
|
super(configuration_hash.transform_keys { transform_key(_1) })
|
data/lib/alchemy/deprecation.rb
CHANGED
data/lib/alchemy/engine.rb
CHANGED
|
@@ -26,7 +26,7 @@ module Alchemy
|
|
|
26
26
|
|
|
27
27
|
initializer "alchemy.admin_stylesheets" do |app|
|
|
28
28
|
if defined?(Sprockets)
|
|
29
|
-
Alchemy.admin_stylesheets.each do |stylesheet|
|
|
29
|
+
Alchemy.config.admin_stylesheets.each do |stylesheet|
|
|
30
30
|
app.config.assets.precompile << stylesheet
|
|
31
31
|
end
|
|
32
32
|
end
|
|
@@ -42,33 +42,48 @@ module Alchemy
|
|
|
42
42
|
end
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
initializer "alchemy.admin_importmap" do
|
|
46
|
+
Alchemy.config.admin_importmaps.add(
|
|
47
|
+
importmap_path: root.join("config/importmap.rb"),
|
|
48
|
+
source_paths: [
|
|
49
|
+
root.join("app/javascript"),
|
|
50
|
+
root.join("vendor/javascript")
|
|
51
|
+
],
|
|
52
|
+
name: "alchemy_admin"
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
45
56
|
initializer "alchemy.importmap" do |app|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
Alchemy.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
app.config.to_prepare do
|
|
58
|
+
watch_paths = []
|
|
59
|
+
|
|
60
|
+
Alchemy.config.admin_importmaps.each do |admin_import|
|
|
61
|
+
Alchemy.importmap.draw admin_import.importmap_path
|
|
62
|
+
watch_paths += admin_import.source_paths.to_a
|
|
63
|
+
app.config.assets.paths += admin_import.source_paths.to_a
|
|
64
|
+
if admin_import[:name] != "alchemy_admin"
|
|
65
|
+
Alchemy.config.admin_js_imports.add(admin_import.name)
|
|
66
|
+
end
|
|
54
67
|
end
|
|
55
|
-
end
|
|
56
68
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
if app.config.importmap.sweep_cache
|
|
70
|
+
Alchemy.importmap.cache_sweeper(watches: watch_paths)
|
|
71
|
+
ActiveSupport.on_load(:action_controller_base) do
|
|
72
|
+
before_action { Alchemy.importmap.cache_sweeper.execute_if_updated }
|
|
73
|
+
end
|
|
61
74
|
end
|
|
62
75
|
end
|
|
63
76
|
end
|
|
64
77
|
|
|
78
|
+
# All the initialization that needs to be re-triggered during reloads
|
|
65
79
|
config.to_prepare do
|
|
80
|
+
# Definition files
|
|
66
81
|
elements_reloader = Rails.application.config.file_watcher.new([ElementDefinition.definitions_file_path]) do
|
|
67
|
-
Rails.logger.info "[
|
|
82
|
+
Rails.logger.info "[alchemy] Reloading Element Definitions."
|
|
68
83
|
ElementDefinition.reset!
|
|
69
84
|
end
|
|
70
85
|
page_layouts_reloader = Rails.application.config.file_watcher.new([PageDefinition.layouts_file_path]) do
|
|
71
|
-
Rails.logger.info "[
|
|
86
|
+
Rails.logger.info "[alchemy] Reloading Page Layouts."
|
|
72
87
|
PageDefinition.reset!
|
|
73
88
|
end
|
|
74
89
|
[elements_reloader, page_layouts_reloader].each do |reloader|
|
|
@@ -77,12 +92,19 @@ module Alchemy
|
|
|
77
92
|
reloader.execute_if_updated
|
|
78
93
|
end
|
|
79
94
|
end
|
|
80
|
-
end
|
|
81
95
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
96
|
+
# The storage adapter for Pictures and Attachments
|
|
97
|
+
#
|
|
98
|
+
# Chose between 'active_storage' (default) or 'dragonfly' (legacy)
|
|
99
|
+
#
|
|
100
|
+
# Can be set via 'ALCHEMY_STORAGE_ADAPTER' env var.
|
|
101
|
+
Alchemy.storage_adapter = Alchemy::StorageAdapter.new(
|
|
102
|
+
ENV.fetch("ALCHEMY_STORAGE_ADAPTER", Alchemy.config.storage_adapter)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Gutentag downcases all tags before save
|
|
106
|
+
# and Gutentag validations are not case sensitive.
|
|
107
|
+
# But we support having tags with uppercase characters.
|
|
86
108
|
Gutentag.normaliser = ->(value) { value.to_s }
|
|
87
109
|
Gutentag.tag_validations = Alchemy::TagValidations
|
|
88
110
|
end
|
|
@@ -22,18 +22,6 @@ module Alchemy
|
|
|
22
22
|
{after: SENTINEL, verbose: true}
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
def set_primary_language(code: "en", name: "English", auto_accept: false)
|
|
26
|
-
unless auto_accept
|
|
27
|
-
code = ask("- What is the language code of your site's primary language?", default: code)
|
|
28
|
-
end
|
|
29
|
-
unless auto_accept
|
|
30
|
-
name = ask("- What is the name of your site's primary language?", default: name)
|
|
31
|
-
end
|
|
32
|
-
gsub_file "./config/alchemy/config.yml", /default_language:\n\s\scode:\sen\n\s\sname:\sEnglish/m do
|
|
33
|
-
"default_language:\n code: #{code}\n name: #{name}"
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
25
|
def inject_seeder
|
|
38
26
|
seed_file = Rails.root.join("db", "seeds.rb")
|
|
39
27
|
args = [seed_file, "Alchemy::Seeder.seed!\n"]
|
|
@@ -22,5 +22,11 @@ module Alchemy
|
|
|
22
22
|
def convert_to_humanized_name(name, suffix)
|
|
23
23
|
name.gsub(/\.#{::Regexp.quote(suffix)}$/i, "").tr("_", " ").strip
|
|
24
24
|
end
|
|
25
|
+
|
|
26
|
+
# Sanitizes a given filename by removing directory traversal attempts and HTML entities.
|
|
27
|
+
def sanitized_filename(file_name)
|
|
28
|
+
file_name = File.basename(file_name)
|
|
29
|
+
CGI.escapeHTML(file_name)
|
|
30
|
+
end
|
|
25
31
|
end
|
|
26
32
|
end
|