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.
Files changed (216) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -0
  3. data/app/assets/builds/alchemy/admin/page-select.css +1 -1
  4. data/app/assets/builds/alchemy/admin.css +1 -1
  5. data/app/assets/builds/alchemy/dark-theme.css +1 -0
  6. data/app/assets/builds/alchemy/light-theme.css +1 -0
  7. data/app/assets/builds/alchemy/theme.css +1 -0
  8. data/app/assets/builds/alchemy/welcome.css +1 -1
  9. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
  10. data/app/assets/builds/tinymce/skins/content/alchemy-dark/content.min.css +1 -0
  11. data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css +1 -1
  12. data/app/assets/builds/tinymce/skins/ui/alchemy-dark/content.min.css +1 -0
  13. data/app/assets/builds/tinymce/skins/ui/alchemy-dark/skin.min.css +1 -0
  14. data/app/assets/images/alchemy/element_icons/layout-bottom-2-line.svg +1 -0
  15. data/app/assets/images/alchemy/icons-sprite.svg +1 -1
  16. data/app/components/alchemy/admin/element_select.rb +39 -0
  17. data/app/components/alchemy/admin/link_dialog/tabs.rb +1 -1
  18. data/app/components/alchemy/admin/locale_select.rb +38 -0
  19. data/app/components/alchemy/ingredients/datetime_view.rb +4 -2
  20. data/app/controllers/alchemy/admin/attachments_controller.rb +2 -0
  21. data/app/controllers/alchemy/admin/elements_controller.rb +2 -0
  22. data/app/controllers/alchemy/admin/pages_controller.rb +3 -1
  23. data/app/controllers/alchemy/admin/pictures_controller.rb +26 -34
  24. data/app/controllers/alchemy/admin/resources_controller.rb +1 -1
  25. data/app/controllers/alchemy/pages_controller.rb +19 -2
  26. data/app/controllers/concerns/alchemy/admin/resource_filter.rb +1 -0
  27. data/app/decorators/alchemy/ingredient_editor.rb +9 -1
  28. data/app/helpers/alchemy/admin/attachments_helper.rb +5 -5
  29. data/app/helpers/alchemy/admin/base_helper.rb +0 -7
  30. data/app/helpers/alchemy/admin/form_helper.rb +2 -1
  31. data/app/helpers/alchemy/pages_helper.rb +1 -1
  32. data/app/javascript/alchemy_admin/components/auto_submit.js +20 -0
  33. data/app/javascript/alchemy_admin/components/datepicker.js +8 -5
  34. data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +3 -2
  35. data/app/javascript/alchemy_admin/components/element_editor.js +25 -15
  36. data/app/javascript/alchemy_admin/components/element_select.js +43 -0
  37. data/app/javascript/alchemy_admin/components/index.js +5 -0
  38. data/app/javascript/alchemy_admin/components/link_buttons.js +6 -2
  39. data/app/javascript/alchemy_admin/components/remote_select.js +5 -1
  40. data/app/javascript/alchemy_admin/components/tinymce.js +93 -16
  41. data/app/javascript/alchemy_admin/dialog.js +1 -1
  42. data/app/javascript/alchemy_admin/file_editors.js +1 -1
  43. data/app/javascript/alchemy_admin/image_loader.js +4 -2
  44. data/app/javascript/alchemy_admin/picture_editors.js +7 -4
  45. data/app/javascript/alchemy_admin/picture_selector.js +4 -4
  46. data/app/jobs/alchemy/delete_picture_job.rb +12 -0
  47. data/app/models/alchemy/attachment.rb +2 -9
  48. data/app/models/alchemy/element.rb +1 -0
  49. data/app/models/alchemy/element_definition.rb +31 -0
  50. data/app/models/alchemy/ingredient.rb +1 -1
  51. data/app/models/alchemy/ingredients/boolean.rb +2 -1
  52. data/app/models/alchemy/language.rb +2 -7
  53. data/app/models/alchemy/page/page_naming.rb +4 -11
  54. data/app/models/alchemy/page/page_natures.rb +16 -11
  55. data/app/models/alchemy/page/publisher.rb +1 -1
  56. data/app/models/alchemy/page.rb +1 -6
  57. data/app/models/alchemy/page_definition.rb +1 -1
  58. data/app/models/alchemy/picture.rb +6 -17
  59. data/app/models/alchemy/resource.rb +15 -2
  60. data/app/models/alchemy/site/layout.rb +1 -0
  61. data/app/models/alchemy/site.rb +1 -6
  62. data/app/models/alchemy/storage_adapter/dragonfly/picture_url.rb +7 -2
  63. data/app/models/alchemy/storage_adapter/dragonfly.rb +24 -2
  64. data/app/models/concerns/alchemy/relatable_resource.rb +28 -0
  65. data/app/stylesheets/alchemy/_custom-properties.scss +162 -0
  66. data/app/stylesheets/alchemy/_mixins.scss +12 -24
  67. data/app/stylesheets/alchemy/_themes.scss +540 -0
  68. data/app/stylesheets/alchemy/admin/archive.scss +28 -8
  69. data/app/stylesheets/alchemy/admin/attachments.scss +10 -33
  70. data/app/stylesheets/alchemy/admin/base.scss +4 -1
  71. data/app/stylesheets/alchemy/admin/buttons.scss +7 -32
  72. data/app/stylesheets/alchemy/admin/dashboard.scss +13 -0
  73. data/app/stylesheets/alchemy/admin/dialogs.scss +17 -7
  74. data/app/stylesheets/alchemy/admin/element-select.scss +11 -0
  75. data/app/stylesheets/alchemy/admin/elements.scss +95 -34
  76. data/app/stylesheets/alchemy/admin/filters.scss +8 -9
  77. data/app/stylesheets/alchemy/admin/flatpickr.scss +12 -27
  78. data/app/stylesheets/alchemy/admin/form_fields.scss +0 -15
  79. data/app/stylesheets/alchemy/admin/forms.scss +3 -8
  80. data/app/stylesheets/alchemy/admin/frame.scss +5 -7
  81. data/app/stylesheets/alchemy/admin/icons.scss +0 -9
  82. data/app/stylesheets/alchemy/admin/image_library.scss +13 -55
  83. data/app/stylesheets/alchemy/admin/navigation.scss +1 -11
  84. data/app/stylesheets/alchemy/admin/node-select.scss +1 -10
  85. data/app/stylesheets/alchemy/admin/nodes.scss +6 -2
  86. data/app/stylesheets/alchemy/admin/notices.scss +5 -4
  87. data/app/stylesheets/alchemy/admin/page-select.scss +16 -0
  88. data/app/stylesheets/alchemy/admin/pagination.scss +1 -8
  89. data/app/stylesheets/alchemy/admin/preview_window.scss +12 -1
  90. data/app/stylesheets/alchemy/admin/resource_info.scss +106 -3
  91. data/app/stylesheets/alchemy/admin/search.scss +1 -1
  92. data/app/stylesheets/alchemy/admin/selects.scss +58 -31
  93. data/app/stylesheets/alchemy/admin/shoelace.scss +32 -62
  94. data/app/stylesheets/alchemy/admin/sitemap.scss +7 -18
  95. data/app/stylesheets/alchemy/admin/tables.scss +3 -3
  96. data/app/stylesheets/alchemy/admin/tags.scss +18 -35
  97. data/app/stylesheets/alchemy/admin/toolbar.scss +0 -6
  98. data/app/stylesheets/alchemy/admin/typography.scss +2 -5
  99. data/app/stylesheets/alchemy/admin.scss +1 -1
  100. data/app/stylesheets/alchemy/dark-theme.scss +5 -0
  101. data/app/stylesheets/alchemy/light-theme.scss +6 -0
  102. data/app/stylesheets/alchemy/theme.scss +13 -0
  103. data/app/stylesheets/tinymce/skins/content/alchemy/content.scss +8 -8
  104. data/app/stylesheets/tinymce/skins/content/alchemy-dark/content.scss +70 -0
  105. data/app/stylesheets/tinymce/skins/ui/alchemy/skin.scss +28 -43
  106. data/app/stylesheets/tinymce/skins/ui/alchemy-dark/content.scss +1 -0
  107. data/app/stylesheets/tinymce/skins/ui/alchemy-dark/skin.scss +3784 -0
  108. data/app/views/alchemy/admin/attachments/_files_list.html.erb +20 -10
  109. data/app/views/alchemy/admin/attachments/assign.js.erb +4 -3
  110. data/app/views/alchemy/admin/attachments/show.html.erb +55 -43
  111. data/app/views/alchemy/admin/crop.html.erb +1 -1
  112. data/app/views/alchemy/admin/dashboard/index.html.erb +1 -1
  113. data/app/views/alchemy/admin/dashboard/info.html.erb +36 -6
  114. data/app/views/alchemy/admin/elements/_form.html.erb +9 -9
  115. data/app/views/alchemy/admin/elements/_header.html.erb +12 -10
  116. data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +1 -1
  117. data/app/views/alchemy/admin/nodes/_form.html.erb +5 -1
  118. data/app/views/alchemy/admin/pages/info.html.erb +1 -1
  119. data/app/views/alchemy/admin/partials/_search_form.html.erb +1 -0
  120. data/app/views/alchemy/admin/pictures/_archive.html.erb +13 -23
  121. data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +1 -6
  122. data/app/views/alchemy/admin/pictures/_form.html.erb +10 -5
  123. data/app/views/alchemy/admin/pictures/_infos.html.erb +21 -52
  124. data/app/views/alchemy/admin/pictures/_library_sidebar.html.erb +7 -0
  125. data/app/views/alchemy/admin/pictures/_picture.html.erb +15 -16
  126. data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +20 -16
  127. data/app/views/alchemy/admin/pictures/_sorting_select.html.erb +13 -0
  128. data/app/views/alchemy/admin/pictures/_tag_list.html.erb +1 -1
  129. data/app/views/alchemy/admin/pictures/edit_multiple.html.erb +1 -6
  130. data/app/views/alchemy/admin/pictures/index.html.erb +3 -12
  131. data/app/views/alchemy/admin/pictures/show.html.erb +17 -14
  132. data/app/views/alchemy/admin/pictures/update.turbo_stream.erb +1 -1
  133. data/app/views/alchemy/admin/resources/_filter_bar.html.erb +5 -15
  134. data/app/views/alchemy/admin/resources/_resource_usage_info.html.erb +36 -0
  135. data/app/views/alchemy/admin/styleguide/index.html.erb +118 -66
  136. data/app/views/alchemy/admin/uploader/_button.html.erb +1 -1
  137. data/app/views/alchemy/base/error_notice.html.erb +1 -1
  138. data/app/views/alchemy/ingredients/_page_editor.html.erb +0 -1
  139. data/app/views/alchemy/ingredients/_richtext_editor.html.erb +0 -1
  140. data/app/views/alchemy/ingredients/_select_editor.html.erb +1 -2
  141. data/app/views/layouts/alchemy/admin.html.erb +25 -23
  142. data/config/locales/alchemy.en.yml +26 -8
  143. data/db/migrate/20250905140323_add_created_at_index_to_pictures_and_attachments.rb +14 -0
  144. data/lib/alchemy/configuration/base_option.rb +18 -5
  145. data/lib/alchemy/configuration/boolean_option.rb +2 -5
  146. data/lib/alchemy/configuration/collection_option.rb +69 -0
  147. data/lib/alchemy/configuration/configuration_option.rb +35 -0
  148. data/lib/alchemy/configuration/pathname_option.rb +12 -0
  149. data/lib/alchemy/configuration.rb +44 -6
  150. data/lib/alchemy/configurations/format_matchers.rb +1 -1
  151. data/lib/alchemy/configurations/importmap.rb +11 -0
  152. data/lib/alchemy/configurations/mailer.rb +2 -2
  153. data/lib/alchemy/configurations/main.rb +148 -3
  154. data/lib/alchemy/configurations/page_cache.rb +19 -0
  155. data/lib/alchemy/configurations/uploader.rb +2 -2
  156. data/lib/alchemy/deprecation.rb +1 -1
  157. data/lib/alchemy/engine.rb +43 -21
  158. data/lib/alchemy/install/tasks.rb +0 -12
  159. data/lib/alchemy/name_conversions.rb +6 -0
  160. data/lib/alchemy/tasks/tidy.rb +18 -0
  161. data/lib/alchemy/test_support/config_stubbing.rb +13 -4
  162. data/lib/alchemy/test_support/factories/language_factory.rb +8 -4
  163. data/lib/alchemy/test_support/factories/page_factory.rb +1 -0
  164. data/lib/alchemy/test_support/factories/picture_factory.rb +1 -0
  165. data/lib/alchemy/test_support/relatable_resource_examples.rb +58 -0
  166. data/lib/alchemy/tinymce.rb +0 -1
  167. data/lib/alchemy/version.rb +1 -1
  168. data/lib/alchemy.rb +18 -171
  169. data/lib/generators/alchemy/install/install_generator.rb +21 -10
  170. data/lib/generators/alchemy/install/templates/alchemy.rb.tt +88 -13
  171. data/lib/tasks/alchemy/assets.rake +1 -1
  172. data/lib/tasks/alchemy/tidy.rake +6 -0
  173. data/lib/tasks/alchemy/usage.rake +2 -0
  174. data/vendor/assets/stylesheets/tinymce/skins/content/dark/content.min.css +1 -0
  175. data/vendor/assets/stylesheets/tinymce/skins/content/default/content.min.css +1 -0
  176. data/vendor/assets/stylesheets/tinymce/skins/ui/oxide/skin.min.css +1 -0
  177. data/vendor/assets/stylesheets/tinymce/skins/ui/oxide-dark/content.min.css +1 -0
  178. data/vendor/assets/stylesheets/tinymce/skins/ui/oxide-dark/skin.min.css +1 -0
  179. data/vendor/javascript/clipboard.min.js +1 -1
  180. data/vendor/javascript/cropperjs.min.js +1 -1
  181. data/vendor/javascript/handlebars.min.js +3 -3
  182. data/vendor/javascript/jquery.min.js +1 -1
  183. data/vendor/javascript/select2.min.js +3 -3
  184. data/vendor/javascript/shoelace.min.js +92 -76
  185. data/vendor/javascript/sortable.min.js +2 -2
  186. data/vendor/javascript/tinymce.min.js +1 -1
  187. data/vendor/javascript/ungap-custom-elements.min.js +2 -2
  188. metadata +51 -36
  189. data/CHANGELOG.md +0 -2100
  190. data/CODE_OF_CONDUCT.md +0 -13
  191. data/CONTRIBUTING.md +0 -73
  192. data/Gemfile +0 -78
  193. data/Rakefile +0 -102
  194. data/SECURITY.md +0 -13
  195. data/alchemy_cms.gemspec +0 -97
  196. data/app/assets/builds/alchemy/custom-properties.css +0 -1
  197. data/app/helpers/alchemy/admin/elements_helper.rb +0 -25
  198. data/app/stylesheets/alchemy/custom-properties.css +0 -244
  199. data/bin/importmap +0 -4
  200. data/bin/rails +0 -9
  201. data/bin/rspec +0 -3
  202. data/bin/setup +0 -30
  203. data/bin/start +0 -17
  204. data/bun.lockb +0 -0
  205. data/bundles/remixicon.mjs +0 -153
  206. data/bundles/shoelace.js +0 -12
  207. data/bundles/tinymce.js +0 -22
  208. data/eslint.config.js +0 -18
  209. data/lib/alchemy/configuration/class_set_option.rb +0 -46
  210. data/lib/alchemy/configuration/integer_list_option.rb +0 -13
  211. data/lib/alchemy/configuration/list_option.rb +0 -22
  212. data/lib/alchemy/configuration/string_list_option.rb +0 -13
  213. data/lib/alchemy/upgrader/.keep +0 -0
  214. data/lib/alchemy/upgrader/tasks/.keep +0 -0
  215. data/rollup.config.mjs +0 -108
  216. data/vitest.config.js +0 -21
@@ -13,8 +13,21 @@ module Alchemy
13
13
  language.public? && !!public_version&.public?
14
14
  end
15
15
 
16
+ # Cache-Control max-age duration in seconds.
17
+ #
18
+ # You can set this via the `ALCHEMY_PAGE_CACHE_MAX_AGE` environment variable,
19
+ # in the `Alchemy.config.page_cache_max_age` configuration option,
20
+ # or in the pages definition in `config/alchemy/page_layouts.yml` file.
21
+ #
22
+ # Defaults to 600 seconds.
16
23
  def expiration_time
17
- public_until ? public_until - Time.current : nil
24
+ return 0 unless cache_page?
25
+
26
+ if definition.cache.to_s.match?(/\d+/)
27
+ definition.cache.to_i
28
+ else
29
+ Alchemy.config.page_cache.max_age
30
+ end
18
31
  end
19
32
 
20
33
  def rootpage?
@@ -144,17 +157,9 @@ module Alchemy
144
157
  # @returns Boolean
145
158
  #
146
159
  def cache_page?
147
- return false unless caching_enabled?
148
-
149
- page_layout = PageDefinition.get(self.page_layout)
150
- page_layout.cache != false && page_layout.searchresults != true
151
- end
152
-
153
- private
160
+ return false if !public? || restricted?
154
161
 
155
- def caching_enabled?
156
- Alchemy.config.cache_pages &&
157
- Rails.application.config.action_controller.perform_caching
162
+ definition.cache != false && definition.searchresults != true
158
163
  end
159
164
  end
160
165
  end
@@ -40,7 +40,7 @@ module Alchemy
40
40
  end
41
41
  end
42
42
 
43
- Alchemy.publish_targets.each { |p| p.perform_later(page) }
43
+ Alchemy.config.publish_targets.each { |p| p.perform_later(page) }
44
44
  end
45
45
 
46
46
  private
@@ -115,7 +115,7 @@ module Alchemy
115
115
  has_many :site_languages, through: :site, source: :languages
116
116
  has_many :folded_pages, dependent: :destroy
117
117
  has_many :legacy_urls, class_name: "Alchemy::LegacyPageUrl", dependent: :destroy
118
- has_many :nodes, class_name: "Alchemy::Node", inverse_of: :page
118
+ has_many :nodes, class_name: "Alchemy::Node", inverse_of: :page, dependent: :restrict_with_error
119
119
  has_many :versions, class_name: "Alchemy::PageVersion", inverse_of: :page, dependent: :destroy
120
120
  has_one :draft_version, -> { drafts }, class_name: "Alchemy::PageVersion"
121
121
  has_one :public_version, -> { published }, class_name: "Alchemy::PageVersion", autosave: -> { persisted? }
@@ -132,11 +132,6 @@ module Alchemy
132
132
  before_create -> { versions.build },
133
133
  if: -> { versions.none? }
134
134
 
135
- before_destroy if: -> { nodes.any? } do
136
- errors.add(:nodes, :still_present)
137
- throw(:abort)
138
- end
139
-
140
135
  before_save :set_language_code,
141
136
  if: -> { language.present? }
142
137
 
@@ -13,7 +13,7 @@ module Alchemy
13
13
  attribute :autogenerate, default: []
14
14
  attribute :layoutpage, :boolean, default: false
15
15
  attribute :unique, :boolean, default: false
16
- attribute :cache, :boolean, default: true
16
+ attribute :cache, default: true
17
17
  attribute :insert_elements_at, :string, default: "bottom"
18
18
  attribute :fixed_attributes, default: {}
19
19
  attribute :searchable, :boolean, default: true
@@ -32,14 +32,8 @@ module Alchemy
32
32
  include Alchemy::NameConversions
33
33
  include Alchemy::Taggable
34
34
  include Alchemy::TouchElements
35
+ include Alchemy::RelatableResource
35
36
 
36
- has_many :picture_ingredients,
37
- class_name: "Alchemy::Ingredients::Picture",
38
- foreign_key: "related_object_id",
39
- inverse_of: :related_object
40
-
41
- has_many :elements, through: :picture_ingredients
42
- has_many :pages, through: :elements
43
37
  has_many :descriptions, class_name: "Alchemy::PictureDescription", dependent: :destroy
44
38
 
45
39
  accepts_nested_attributes_for :descriptions, allow_destroy: true, reject_if: ->(attr) { attr[:text].blank? }
@@ -86,15 +80,16 @@ module Alchemy
86
80
 
87
81
  scope :named, ->(name) { where("#{table_name}.name LIKE ?", "%#{name}%") }
88
82
  scope :recent, -> { where("#{table_name}.created_at > ?", Time.current - 24.hours).order(:created_at) }
89
- scope :deletable,
90
- -> {
91
- where("#{table_name}.id NOT IN (SELECT related_object_id FROM alchemy_ingredients WHERE related_object_type = 'Alchemy::Picture')")
92
- }
93
83
  scope :without_tag, -> { left_outer_joins(:taggings).where(gutentag_taggings: {id: nil}) }
94
84
  scope :by_file_format, ->(file_format) do
95
85
  Alchemy.storage_adapter.by_file_format_scope(file_format)
96
86
  end
97
87
 
88
+ # Case insensitive Ransack searching and sorting for name attribute
89
+ ransacker :name, type: :string do
90
+ arel_table[:name].lower
91
+ end
92
+
98
93
  # Class methods
99
94
 
100
95
  class << self
@@ -251,12 +246,6 @@ module Alchemy
251
246
  pages.any? && pages.not_restricted.blank?
252
247
  end
253
248
 
254
- # Returns true if picture is not assigned to any Picture ingredient.
255
- #
256
- def deletable?
257
- picture_ingredients.empty?
258
- end
259
-
260
249
  def image_file_name
261
250
  Alchemy.storage_adapter.image_file_name(self)
262
251
  end
@@ -152,7 +152,7 @@ module Alchemy
152
152
  end
153
153
 
154
154
  def attributes
155
- @_attributes ||= model.columns.collect do |col|
155
+ @_attributes ||= model.columns.filter_map do |col|
156
156
  next if skipped_attributes.include?(col.name)
157
157
 
158
158
  {
@@ -161,7 +161,7 @@ module Alchemy
161
161
  relation: resource_relation(col.name),
162
162
  enum: enum_values_collection_for_select(col.name)
163
163
  }.delete_if { |_k, v| v.blank? }
164
- end.compact
164
+ end
165
165
  end
166
166
 
167
167
  def enum_values_collection_for_select(column_name)
@@ -199,6 +199,19 @@ module Alchemy
199
199
  attributes.reject { |h| restricted_attributes.map(&:to_s).include?(h[:name].to_s) }
200
200
  end
201
201
 
202
+ # Returns a list of attribute names that are permitted to be updated.
203
+ # Uses {editable_attributes} and adds {tag_list} if model is taggable.
204
+ # Used in strong parameters of Alchemy::Admin::ResourcesController.
205
+ #
206
+ # @return [Array<String>]
207
+ def permitted_attributes
208
+ attrs = editable_attributes.map { _1[:name] }
209
+ if model < Alchemy::Taggable
210
+ attrs << "tag_list"
211
+ end
212
+ attrs
213
+ end
214
+
202
215
  # Returns all attribute names that are searchable in the admin interface
203
216
  #
204
217
  def searchable_attribute_names
@@ -3,6 +3,7 @@
3
3
  module Alchemy
4
4
  module Site::Layout
5
5
  extend ActiveSupport::Concern
6
+
6
7
  SITE_DEFINITIONS_FILE = Rails.root.join("config/alchemy/site_layouts.yml")
7
8
 
8
9
  module ClassMethods
@@ -21,12 +21,7 @@ module Alchemy
21
21
  validates_uniqueness_of :host, case_sensitive: false
22
22
 
23
23
  # associations
24
- has_many :languages
25
-
26
- before_destroy if: -> { languages.any? } do
27
- errors.add(:languages, :still_present)
28
- throw(:abort)
29
- end
24
+ has_many :languages, dependent: :restrict_with_error
30
25
 
31
26
  scope :published, -> { where(public: true) }
32
27
 
@@ -26,8 +26,9 @@ module Alchemy
26
26
  #
27
27
  # @return [String]
28
28
  #
29
- def call(options = {})
30
- @variant = PictureVariant.new(picture, options.slice(*TRANSFORMATION_OPTIONS))
29
+ def call(variant_options = {})
30
+ set_variant(variant_options)
31
+
31
32
  params = {
32
33
  basename: picture.name,
33
34
  ext: variant.render_format,
@@ -41,6 +42,10 @@ module Alchemy
41
42
 
42
43
  private
43
44
 
45
+ def set_variant(options = {})
46
+ @variant = PictureVariant.new(picture, options.slice(*TRANSFORMATION_OPTIONS))
47
+ end
48
+
44
49
  def processible_image?
45
50
  variant.image.is_a?(::Dragonfly::Job)
46
51
  end
@@ -15,6 +15,10 @@ module Alchemy
15
15
 
16
16
  has_many :thumbs, class_name: "Alchemy::PictureThumb", dependent: :destroy
17
17
 
18
+ before_save if: :image_file_name do
19
+ self.image_file_name = sanitized_filename(image_file_name)
20
+ end
21
+
18
22
  # Create important thumbnails upfront
19
23
  after_create -> { PictureThumb.generate_thumbs!(self) },
20
24
  if: :has_convertible_format?
@@ -30,6 +34,10 @@ module Alchemy
30
34
  write_attribute(:file_mime_type, file.mime_type)
31
35
  }
32
36
  end
37
+
38
+ before_save if: :file_name do
39
+ self.file_name = sanitized_filename(file_name)
40
+ end
33
41
  end
34
42
  end
35
43
  end
@@ -38,16 +46,30 @@ module Alchemy
38
46
 
39
47
  CONVERTIBLE_FILE_FORMATS = %w[gif jpg jpeg png webp].freeze
40
48
 
49
+ # Allows to set a custom Attachment Url class
50
+ def attachment_url_class=(klass)
51
+ @_attachment_url_class = klass
52
+ end
53
+
54
+ # Returns the class used to generate attachment urls
55
+ # @return [Class] - defaults to Alchemy::StorageAdapter::Dragonfly::AttachmentUrl
41
56
  def attachment_url_class
42
- AttachmentUrl
57
+ @_attachment_url_class ||= AttachmentUrl
43
58
  end
44
59
 
45
60
  def preprocessor_class
46
61
  Preprocessor
47
62
  end
48
63
 
64
+ # Allows to set a custom Picture Url class
65
+ def picture_url_class=(klass)
66
+ @_picture_url_class = klass
67
+ end
68
+
69
+ # Returns the class used to generate picture urls
70
+ # @return [Class] - defaults to Alchemy::StorageAdapter::Dragonfly::PictureUrl
49
71
  def picture_url_class
50
- PictureUrl
72
+ @_picture_url_class ||= PictureUrl
51
73
  end
52
74
 
53
75
  def file_formats(class_name, scope:)
@@ -0,0 +1,28 @@
1
+ module Alchemy
2
+ module RelatableResource
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ scope :deletable, -> do
7
+ where(
8
+ "#{table_name}.id NOT IN (SELECT related_object_id FROM alchemy_ingredients WHERE related_object_id IS NOT NULL AND related_object_type = ?)",
9
+ name
10
+ )
11
+ end
12
+
13
+ has_many :related_ingredients,
14
+ class_name: "Alchemy::Ingredient",
15
+ foreign_key: "related_object_id",
16
+ as: :related_object
17
+
18
+ has_many :elements, through: :related_ingredients
19
+ has_many :pages, through: :elements
20
+ end
21
+
22
+ # Returns true if object is not assigned to any ingredient.
23
+ #
24
+ def deletable?
25
+ related_ingredients.none?
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,162 @@
1
+ :root {
2
+ /* Dark Colors */
3
+ --a-lightest-grey: hsl(200deg, 5%, 90%);
4
+ --a-lighter-grey: hsl(200deg, 5%, 70%);
5
+ --a-light-grey: hsl(200deg, 5%, 35%);
6
+ --a-grey: hsl(200deg, 5%, 22.5%);
7
+ --a-dark-grey: hsl(200deg, 5%, 17.5%);
8
+ --a-darker-grey: hsl(200deg, 5%, 15%);
9
+ --a-darkest-grey: hsl(200deg, 5%, 12.5%);
10
+
11
+ /* Light Colors */
12
+ --color-blue_very_light: hsl(203deg, 32%, 97%);
13
+ --color-blue_light: hsl(203deg, 32%, 85%);
14
+ --color-blue_medium: hsl(212deg, 52%, 36%);
15
+ --color-blue_dark: hsl(212deg, 52%, 26%);
16
+
17
+ --color-green_very_light: hsl(88deg, 47%, 88%);
18
+ --color-green_light: hsl(127deg, 25%, 69%);
19
+ --color-green_medium: hsl(127deg, 25%, 48%);
20
+ --color-green_dark: hsl(128deg, 32%, 26%);
21
+
22
+ --color-yellow_light: hsl(60deg, 81%, 92%);
23
+ --color-yellow_medium: hsl(56deg, 68%, 85%);
24
+ --color-yellow_dark: hsl(56deg, 53%, 29%);
25
+
26
+ --color-orange_medium: hsl(42deg, 100%, 74%);
27
+ --color-orange_dark: hsl(28deg, 77%, 68%);
28
+ --color-orange_very_dark: hsl(28deg, 77%, 48%);
29
+
30
+ --color-red_very_light: hsl(0deg, 47%, 88%);
31
+ --color-red_light: hsl(0deg, 25%, 69%);
32
+ --color-red_medium: hsl(0deg, 51%, 42%);
33
+ --color-red_dark: hsl(0deg, 51%, 25%);
34
+
35
+ --color-grey_very_light: hsl(0deg, 0%, 97%);
36
+ --color-grey_light: hsl(0deg, 0%, 94%);
37
+ --color-grey_medium: hsl(0deg, 0%, 78%);
38
+ --color-grey_dark: hsl(0deg, 0%, 40%);
39
+ --color-grey_very_dark: hsl(0deg, 0%, 20%);
40
+
41
+ --color-white: hsl(0deg, 0%, 100%);
42
+
43
+ /* Sizes */
44
+ --spacing-0: 2px;
45
+ --spacing-1: 4px;
46
+ --spacing-2: 8px;
47
+ --spacing-3: 12px;
48
+ --spacing-4: 16px;
49
+ --spacing-5: 20px;
50
+ --spacing-6: 24px;
51
+ --spacing-7: 28px;
52
+ --spacing-8: 32px;
53
+
54
+ /* Font Families */
55
+ --font-mono:
56
+ Menlo, Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal,
57
+ monospace;
58
+ --font-sans:
59
+ "Open Sans", "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Verdana,
60
+ Tahoma, sans-serif;
61
+
62
+ /* Font Size - at the moment only in Pixel, because the base font size is 12px */
63
+ --font-size_small: 10px; /* 0.875rem */
64
+ --font-size_medium: 12px; /* 1rem */
65
+ --font-size_large: 16px; /* 1.25 rem */
66
+
67
+ --font-weight_normal: 500;
68
+ --font-weight_bold: 700;
69
+
70
+ --font-default: var(--font-size_medium) var(--font-sans);
71
+
72
+ /* Borders */
73
+ --border-radius_medium: 3px;
74
+ --border-width_small: 1px;
75
+ --border-style: solid;
76
+ --border-default: var(--border-width_small) var(--border-style)
77
+ var(--border-color);
78
+
79
+ /* Transitions */
80
+ --transition-duration: 250ms;
81
+ --transition-easing: linear;
82
+
83
+ /* Button */
84
+ --button-border-radius: var(--border-radius_medium);
85
+ --button-border-style: var(--border-style);
86
+ --button-border-width: var(--border-width_small);
87
+ --button-border: var(--button-border-width) var(--button-border-style)
88
+ var(--button-border-color);
89
+ --button-font-size: var(--font-size_medium);
90
+ --button-font-weight: var(--font-weight_bold);
91
+ --button-height: 30px;
92
+ --button-line-height: var(--form-field-line-height);
93
+ --button-margin: var(--form-field-margin);
94
+ --button-padding: var(--spacing-1) var(--spacing-5);
95
+ --button-small-padding: 0.4em 0.8em;
96
+
97
+ /* Date Picker */
98
+ --datepicker-font-size: var(--font-size_medium);
99
+
100
+ /* Dialog */
101
+ --dialog-header-height: 36px;
102
+ --dialog-header-font-size: var(--font-size_medium);
103
+ --dialog-header-padding: 0 var(--spacing-2);
104
+ --dialog-transition-duration: 150ms;
105
+
106
+ /* Elements */
107
+ --elements-window-width: 0px;
108
+ --elements-window-min-width: 400px;
109
+
110
+ /* Form */
111
+ --form-left-column-width: 35%;
112
+ --form-right-column-width: 65%;
113
+
114
+ /* Form Field */
115
+ --form-field-margin: var(--spacing-1) 0;
116
+ --form-field-height: 31px;
117
+ --form-field-addon-width: 30px;
118
+ --form-field-border-width: var(--border-width_small);
119
+ --form-field-border-style: var(--border-style);
120
+ --form-field-padding: var(--spacing-1) var(--spacing-2);
121
+ --form-field-font-size: var(--font-size_medium);
122
+ --form-field-line-height: 1.25;
123
+
124
+ /* Header */
125
+ --header-height: 29px;
126
+ --header-border-bottom: var(--border-default);
127
+
128
+ /* Icon Button */
129
+ --icon-button-height: 30px;
130
+ --icon-button-width: 30px;
131
+ --icon-button-medium-height: 26px;
132
+ --icon-button-medium-width: 26px;
133
+ --icon-button-small-height: 15px;
134
+ --icon-button-small-width: 15px;
135
+
136
+ /* Icons */
137
+ --icon-size-xs: 0.75rem;
138
+ --icon-size-sm: 0.875rem;
139
+ --icon-size-1x: 1rem;
140
+ --icon-size-md: 1.3rem;
141
+ --icon-size-xl: 1.5rem;
142
+ --icon-size-xxl: 1.6rem;
143
+
144
+ /* Navigation */
145
+ --main-menu-width: 150px;
146
+ --top-menu-height: 75px;
147
+ --main-menu-entry-max-width: 110px;
148
+
149
+ /* Pagination */
150
+ --pagination-height: 52px;
151
+
152
+ /* Select */
153
+ --select-medium-width: 90px;
154
+ --select-large-width: 120px;
155
+
156
+ /* Sidebar */
157
+ --sidebar-width: 232px;
158
+
159
+ /* Toolbar */
160
+ --toolbar-height: 46px;
161
+ --toolbar-padding: var(--spacing-2) var(--spacing-1);
162
+ }
@@ -21,6 +21,7 @@
21
21
  $border-radius: var(--button-border-radius),
22
22
  $border: var(--button-border),
23
23
  $box-shadow: var(--button-box-shadow),
24
+ $height: var(--button-height),
24
25
  $padding: var(--button-padding),
25
26
  $margin: var(--button-margin),
26
27
  $color: var(--button-text-color),
@@ -34,7 +35,7 @@
34
35
  font-family: var(--font-sans);
35
36
  font-size: var(--button-font-size);
36
37
  font-weight: var(--button-font-weight);
37
- height: var(--button-height);
38
+ height: $height;
38
39
  padding: $padding;
39
40
  cursor: pointer;
40
41
  border-radius: $border-radius;
@@ -66,14 +67,9 @@
66
67
  }
67
68
 
68
69
  &.disabled,
69
- &[disabled],
70
- &.disabled:active,
71
- &[disabled]:active,
72
- &.disabled:focus,
73
- &[disabled]:focus,
74
- &.disabled:hover,
75
- &[disabled]:hover {
76
- background-color: hsla(0deg, 0%, 100%, 0.5);
70
+ &[disabled] {
71
+ background-color: var(--button-disabled-bg-color);
72
+ border-color: var(--button-border-color);
77
73
  cursor: not-allowed;
78
74
  box-shadow: none;
79
75
  outline: none;
@@ -95,28 +91,20 @@
95
91
  float: left;
96
92
  text-align: right;
97
93
  font-size: var(--font-size_medium);
98
- }
99
-
100
- @mixin form-value-display {
101
- float: right;
102
- width: var(--form-right-column-width);
103
- margin: var(--form-field-margin);
104
- padding: var(--spacing-1) var(--spacing-2);
105
- line-height: 21px;
106
- min-height: var(--form-field-height);
107
- background: white;
108
- border-radius: var(--border-radius_medium);
94
+ color: var(--form-field-label-color);
109
95
  }
110
96
 
111
97
  @mixin form-hint(
112
- $background-color: var(--color-yellow_light),
113
- $border-color: var(--color-grey_light)
98
+ $background-color: var(--hint-background-color),
99
+ $border-color: var(--hint-border-color),
100
+ $color: var(--hint-text-color)
114
101
  ) {
115
102
  font-size: var(--font-size_small);
116
103
  line-height: 1.5em;
117
104
  padding: var(--spacing-1);
118
105
  background-color: $background-color;
119
106
  border: 1px solid $border-color;
107
+ color: $color;
120
108
  display: block;
121
109
  clear: both;
122
110
  border-radius: var(--border-radius_medium);
@@ -159,7 +147,7 @@
159
147
  vertical-align: middle;
160
148
  padding: $padding;
161
149
  margin: $margin;
162
- background-color: var(--color-grey_light);
150
+ background-color: var(--tag-background-color);
163
151
  overflow: hidden;
164
152
  position: relative;
165
153
  border-radius: var(--border-radius_medium);
@@ -175,7 +163,7 @@
175
163
  align-self: start;
176
164
  flex-shrink: 0;
177
165
  content: "";
178
- background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="hsla(224deg, 23%, 26%, 0.75)"><path d="M10.9042 2.10025L20.8037 3.51446L22.2179 13.414L13.0255 22.6063C12.635 22.9969 12.0019 22.9969 11.6113 22.6063L1.71184 12.7069C1.32131 12.3163 1.32131 11.6832 1.71184 11.2926L10.9042 2.10025ZM11.6113 4.22157L3.83316 11.9997L12.3184 20.485L20.0966 12.7069L19.036 5.28223L11.6113 4.22157ZM13.7327 10.5855C12.9516 9.80448 12.9516 8.53815 13.7327 7.7571C14.5137 6.97606 15.78 6.97606 16.5611 7.7571C17.3421 8.53815 17.3421 9.80448 16.5611 10.5855C15.78 11.3666 14.5137 11.3666 13.7327 10.5855Z"></path></svg>');
166
+ background-image: var(--tag-background-image);
179
167
  width: 12px;
180
168
  height: 12px;
181
169
  margin-top: var(--spacing-0);