alchemy_cms 8.3.11 → 8.4.0
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/LICENSE +1 -1
- data/README.md +58 -10
- data/app/assets/builds/alchemy/admin/page-select.css +1 -1
- data/app/assets/builds/alchemy/admin/print.css +1 -1
- data/app/assets/builds/alchemy/admin/remote-select.css +1 -0
- data/app/assets/builds/alchemy/admin/tags-autocomplete.css +1 -0
- data/app/assets/builds/alchemy/admin.css +4 -4
- data/app/assets/builds/alchemy/alchemy_admin.min.js +1 -1
- data/app/assets/builds/alchemy/alchemy_admin.min.js.map +1 -1
- data/app/assets/builds/alchemy/dark-theme.css +1 -1
- data/app/assets/builds/alchemy/light-theme.css +1 -1
- data/app/assets/builds/alchemy/preview.min.js +1 -1
- data/app/assets/builds/alchemy/theme.css +1 -1
- 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 -1
- data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css +1 -1
- data/app/assets/builds/tinymce/skins/ui/alchemy-dark/skin.min.css +1 -1
- data/app/assets/images/alchemy/icons-sprite.svg +1 -1
- data/app/components/alchemy/admin/dashboard/widgets/greeting.html.erb +1 -1
- data/app/components/alchemy/admin/dashboard/widgets/news_reader.html.erb +38 -0
- data/app/components/alchemy/admin/dashboard/widgets/news_reader.rb +54 -0
- data/app/components/alchemy/admin/dashboard/widgets/online_users.html.erb +1 -1
- data/app/components/alchemy/admin/dashboard/widgets/recent_pages.html.erb +1 -1
- data/app/components/alchemy/admin/dashboard/widgets/system_info.html.erb +11 -3
- data/app/components/alchemy/admin/dashboard/widgets/system_info.rb +6 -1
- data/app/components/alchemy/admin/element_editor.html.erb +1 -1
- data/app/components/alchemy/admin/element_select.rb +30 -20
- data/app/components/alchemy/admin/element_status_indicators.html.erb +16 -0
- data/app/components/alchemy/admin/element_status_indicators.rb +31 -0
- data/app/components/alchemy/admin/link_dialog/external_tab.rb +2 -1
- data/app/components/alchemy/admin/main_navigation/entry.rb +61 -0
- data/app/components/alchemy/admin/main_navigation/menu.rb +21 -0
- data/app/components/alchemy/admin/main_navigation/sub_navigation.rb +31 -0
- data/app/components/alchemy/admin/page_node.html.erb +1 -1
- data/app/components/alchemy/admin/page_permitted_roles_select.html.erb +11 -0
- data/app/components/alchemy/admin/page_permitted_roles_select.rb +35 -0
- data/app/components/alchemy/admin/page_select.rb +18 -8
- data/app/components/alchemy/admin/preview_time_select.rb +1 -1
- data/app/components/alchemy/admin/toolbar_button.rb +43 -20
- data/app/components/alchemy/admin/uploader_button.rb +66 -0
- data/app/components/alchemy/ingredients/base_editor.rb +10 -1
- data/app/components/alchemy/ingredients/boolean_editor.rb +1 -0
- data/app/components/alchemy/ingredients/color_editor.rb +6 -3
- data/app/components/alchemy/ingredients/datetime_view.rb +1 -1
- data/app/components/alchemy/ingredients/picture_view.rb +2 -4
- data/app/components/alchemy/menubar.html.erb +157 -0
- data/app/components/alchemy/menubar.rb +15 -0
- data/app/controllers/alchemy/admin/attachments_controller.rb +12 -9
- data/app/controllers/alchemy/admin/base_controller.rb +34 -21
- data/app/controllers/alchemy/admin/dashboard_controller.rb +4 -0
- data/app/controllers/alchemy/admin/elements_controller.rb +4 -23
- data/app/controllers/alchemy/admin/languages_controller.rb +1 -1
- data/app/controllers/alchemy/admin/nodes_controller.rb +1 -1
- data/app/controllers/alchemy/admin/pages_controller.rb +1 -1
- data/app/controllers/alchemy/admin/pictures_controller.rb +6 -3
- data/app/controllers/alchemy/admin/resources_controller.rb +5 -5
- data/app/controllers/alchemy/admin/sites_controller.rb +1 -1
- data/app/controllers/alchemy/base_controller.rb +24 -16
- data/app/controllers/alchemy/pages_controller.rb +29 -8
- data/app/controllers/alchemy/sitemap_controller.rb +58 -0
- data/app/controllers/concerns/alchemy/admin/csp_protection.rb +43 -0
- data/app/controllers/concerns/alchemy/admin/current_language.rb +1 -1
- data/app/controllers/concerns/alchemy/admin/resource_filter.rb +3 -1
- data/{lib → app/controllers/concerns}/alchemy/controller_actions.rb +13 -0
- data/app/controllers/concerns/alchemy/legacy_page_redirects.rb +4 -4
- data/app/helpers/alchemy/admin/base_helper.rb +4 -1
- data/app/helpers/alchemy/admin/form_helper.rb +0 -2
- data/app/helpers/alchemy/admin/navigation_helper.rb +10 -181
- data/app/helpers/alchemy/elements_block_helper.rb +14 -0
- data/app/helpers/alchemy/resources_helper.rb +15 -1
- data/app/javascript/alchemy_admin/components/attachment_select.js +11 -20
- data/app/javascript/alchemy_admin/components/auto_submit.js +2 -4
- data/app/javascript/alchemy_admin/components/button.js +0 -6
- data/app/javascript/alchemy_admin/components/clear_schedule_button.js +21 -0
- data/app/javascript/alchemy_admin/components/color_select.js +50 -45
- data/app/javascript/alchemy_admin/components/conditional_field.js +50 -0
- data/app/javascript/alchemy_admin/components/dirty_guard.js +77 -0
- data/app/javascript/alchemy_admin/components/dom_id_select.js +17 -10
- data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +7 -2
- data/app/javascript/alchemy_admin/components/element_editor.js +29 -56
- data/app/javascript/alchemy_admin/components/element_select.js +26 -66
- data/app/javascript/alchemy_admin/components/icon.js +9 -4
- data/app/javascript/alchemy_admin/{image_cropper.js → components/image_cropper.js} +39 -53
- data/app/javascript/alchemy_admin/components/image_overlay_link.js +14 -0
- data/app/javascript/alchemy_admin/components/index.js +12 -1
- data/app/javascript/alchemy_admin/components/main_navi_entry.js +75 -0
- data/app/javascript/alchemy_admin/components/menu_node.js +73 -0
- data/app/javascript/alchemy_admin/components/news_reader.js +109 -0
- data/app/javascript/alchemy_admin/components/node_folder.js +36 -0
- data/app/javascript/alchemy_admin/components/node_form.js +34 -0
- data/app/javascript/alchemy_admin/components/node_select.js +25 -30
- data/app/javascript/alchemy_admin/components/node_tree.js +56 -0
- data/app/javascript/alchemy_admin/components/page_node.js +2 -1
- data/app/javascript/alchemy_admin/components/page_select.js +16 -34
- data/app/javascript/alchemy_admin/components/picture_archive.js +92 -0
- data/app/javascript/alchemy_admin/components/picture_editor.js +2 -1
- data/app/javascript/alchemy_admin/components/preview_window.js +11 -3
- data/app/javascript/alchemy_admin/components/remote_select.js +461 -81
- data/app/javascript/alchemy_admin/components/select.js +26 -88
- data/app/javascript/alchemy_admin/components/sitemap.js +2 -1
- data/app/javascript/alchemy_admin/components/sortable_elements.js +2 -1
- data/app/javascript/alchemy_admin/components/tags_autocomplete.js +63 -48
- data/app/javascript/alchemy_admin/components/tinymce.js +2 -1
- data/app/javascript/alchemy_admin/components/uploader/file_upload.js +3 -2
- data/app/javascript/alchemy_admin/components/uploader.js +19 -3
- data/app/javascript/alchemy_admin/confirm_dialog.js +7 -1
- data/app/javascript/alchemy_admin/dialog.js +182 -110
- data/app/javascript/alchemy_admin/hotkeys.js +0 -5
- data/app/javascript/alchemy_admin/i18n.js +3 -9
- data/app/javascript/alchemy_admin/image_overlay.js +58 -42
- data/app/javascript/alchemy_admin/initializer.js +2 -2
- data/app/javascript/alchemy_admin/link_dialog.js +15 -2
- data/app/javascript/alchemy_admin/routes.js +18 -0
- data/app/javascript/alchemy_admin/turbo_stream_actions.js +88 -0
- data/app/javascript/alchemy_admin/utils/ajax.js +14 -3
- data/app/javascript/alchemy_admin/utils/events.js +5 -0
- data/app/javascript/alchemy_admin/utils/json_script.js +12 -0
- data/app/javascript/alchemy_admin/utils/tom_select.js +171 -0
- data/app/javascript/alchemy_admin.js +2 -19
- data/app/javascript/preview.js +131 -113
- data/app/jobs/alchemy/invalidate_elements_cache_job.rb +1 -1
- data/app/models/alchemy/admin/content_security_policy.rb +143 -0
- data/app/models/alchemy/admin/filters/base.rb +7 -0
- data/app/models/alchemy/admin/filters/datepicker.rb +1 -1
- data/app/models/alchemy/admin/filters/select.rb +20 -7
- data/app/models/alchemy/attachment.rb +14 -7
- data/app/models/alchemy/cache_control.rb +104 -0
- data/app/models/alchemy/current.rb +1 -1
- data/app/models/alchemy/eager_loading.rb +0 -2
- data/app/models/alchemy/element.rb +23 -8
- data/app/models/alchemy/element_definition.rb +1 -0
- data/app/models/alchemy/elements_repository.rb +11 -0
- data/app/models/alchemy/folded_page.rb +5 -1
- data/app/models/alchemy/ingredient.rb +26 -0
- data/app/models/alchemy/ingredients/datetime.rb +1 -1
- data/app/models/alchemy/ingredients/link.rb +2 -0
- data/app/models/alchemy/ingredients/picture.rb +3 -0
- data/app/models/alchemy/ingredients/text.rb +2 -0
- data/app/models/alchemy/language.rb +19 -7
- data/app/models/alchemy/legacy_page_url.rb +6 -1
- data/app/models/alchemy/node.rb +51 -0
- data/app/models/alchemy/page/page_natures.rb +41 -8
- data/app/models/alchemy/page/page_scopes.rb +23 -0
- data/app/models/alchemy/page/sitemap_cache_key.rb +56 -0
- data/app/models/alchemy/page.rb +88 -32
- data/app/models/alchemy/page_definition.rb +1 -1
- data/app/models/alchemy/page_mutex.rb +16 -0
- data/app/models/alchemy/page_version.rb +24 -1
- data/app/models/alchemy/permissions.rb +6 -5
- data/app/models/alchemy/picture.rb +15 -7
- data/app/models/alchemy/picture_description.rb +22 -0
- data/app/models/alchemy/picture_thumb.rb +18 -0
- data/app/models/alchemy/publishable/timestamp_resolver.rb +70 -0
- data/app/models/alchemy/resource.rb +1 -1
- data/app/models/alchemy/safe_url_validator.rb +34 -0
- data/app/models/alchemy/site.rb +8 -3
- data/app/models/alchemy/storage_adapter/active_storage/preprocessor.rb +7 -3
- data/app/models/alchemy/storage_adapter/active_storage.rb +4 -1
- data/app/models/alchemy/tag.rb +10 -4
- data/{lib → app/models/concerns}/alchemy/filetypes.rb +5 -0
- data/app/models/concerns/alchemy/picture_thumbnails.rb +61 -6
- data/app/models/concerns/alchemy/publishable.rb +14 -27
- data/app/models/concerns/alchemy/taggable.rb +92 -0
- data/app/models/concerns/alchemy/user_methods.rb +95 -0
- data/app/services/alchemy/duplicate_element.rb +5 -2
- data/app/services/alchemy/element_preloader.rb +1 -2
- data/app/stylesheets/alchemy/_custom-properties.scss +1 -0
- data/app/stylesheets/alchemy/_extends.scss +0 -8
- data/app/stylesheets/alchemy/_themes.scss +10 -9
- data/app/stylesheets/alchemy/admin/_empty-state.scss +29 -0
- data/app/stylesheets/alchemy/admin/_help.scss +10 -0
- data/app/stylesheets/alchemy/admin/_tom-select.scss +104 -15
- data/app/stylesheets/alchemy/admin/archive.scss +56 -11
- data/app/stylesheets/alchemy/admin/base.scss +4 -5
- data/app/stylesheets/alchemy/admin/buttons.scss +12 -1
- data/app/stylesheets/alchemy/admin/dashboard.scss +150 -20
- data/app/stylesheets/alchemy/admin/dialogs.scss +27 -36
- data/app/stylesheets/alchemy/admin/element-select.scss +1 -1
- data/app/stylesheets/alchemy/admin/elements.scss +8 -24
- data/app/stylesheets/alchemy/admin/flash.scss +2 -1
- data/app/stylesheets/alchemy/admin/forms.scss +60 -24
- data/app/stylesheets/alchemy/admin/frame.scss +40 -4
- data/app/stylesheets/alchemy/admin/image_library.scss +9 -12
- data/app/stylesheets/alchemy/admin/navigation.scss +17 -14
- data/app/stylesheets/alchemy/admin/node-select.scss +13 -37
- data/app/stylesheets/alchemy/admin/nodes.scss +1 -1
- data/app/stylesheets/alchemy/admin/page-select.scss +6 -97
- data/app/stylesheets/alchemy/admin/print.scss +0 -4
- data/app/stylesheets/alchemy/admin/remote-select.scss +170 -0
- data/app/stylesheets/alchemy/admin/search.scss +2 -3
- data/app/stylesheets/alchemy/admin/selects.scss +1 -294
- data/app/stylesheets/alchemy/admin/sitemap.scss +2 -1
- data/app/stylesheets/alchemy/admin/tables.scss +7 -1
- data/app/stylesheets/alchemy/admin/tags-autocomplete.scss +17 -0
- data/app/stylesheets/alchemy/admin/tags.scss +0 -13
- data/app/stylesheets/alchemy/admin/toolbar.scss +9 -6
- data/app/stylesheets/alchemy/admin/upload.scss +61 -1
- data/app/stylesheets/alchemy/admin.scss +4 -1
- data/app/types/alchemy/cache_control_type.rb +84 -0
- data/app/views/alchemy/_edit_mode.html.erb +1 -1
- data/app/views/alchemy/_menubar.html.erb +2 -154
- data/app/views/alchemy/_preview_mode_code.html.erb +0 -3
- data/app/views/alchemy/admin/_left_menu.html.erb +18 -5
- data/app/views/alchemy/admin/_top_menu.html.erb +1 -1
- data/app/views/alchemy/admin/attachments/_archive_overlay.html.erb +5 -4
- data/app/views/alchemy/admin/attachments/_file_to_assign.html.erb +3 -3
- data/app/views/alchemy/admin/attachments/_files_list.html.erb +71 -83
- data/app/views/alchemy/admin/attachments/assign.turbo_stream.erb +8 -0
- data/app/views/alchemy/admin/attachments/index.html.erb +32 -7
- data/app/views/alchemy/admin/clipboard/insert.turbo_stream.erb +1 -1
- data/app/views/alchemy/admin/crop.html.erb +20 -20
- data/app/views/alchemy/admin/dashboard/_footer.html.erb +0 -3
- data/app/views/alchemy/admin/dashboard/_top.html.erb +1 -0
- data/app/views/alchemy/admin/dashboard/help.html.erb +84 -57
- data/app/views/alchemy/admin/dashboard/license.html.erb +6 -0
- data/app/views/alchemy/admin/elements/_footer.html.erb +0 -1
- data/app/views/alchemy/admin/elements/_form.html.erb +1 -1
- data/app/views/alchemy/admin/elements/_header.html.erb +2 -19
- data/app/views/alchemy/admin/elements/_preview_text_quote.html.erb +3 -0
- data/app/views/alchemy/admin/elements/_schedule.html.erb +7 -7
- data/app/views/alchemy/admin/elements/_toolbar.html.erb +1 -1
- data/app/views/alchemy/admin/elements/create.turbo_stream.erb +1 -1
- data/app/views/alchemy/admin/elements/index.html.erb +1 -1
- data/app/views/alchemy/admin/elements/publish.turbo_stream.erb +2 -2
- data/app/views/alchemy/admin/elements/update.turbo_stream.erb +31 -0
- data/app/views/alchemy/admin/ingredients/update.turbo_stream.erb +6 -3
- data/app/views/alchemy/admin/languages/_table.html.erb +0 -2
- data/app/views/alchemy/admin/languages/index.html.erb +25 -29
- data/app/views/alchemy/admin/layoutpages/_layoutpage.html.erb +1 -3
- data/app/views/alchemy/admin/layoutpages/edit.html.erb +1 -1
- data/app/views/alchemy/admin/layoutpages/index.html.erb +26 -7
- data/app/views/alchemy/admin/nodes/_form.html.erb +3 -21
- data/app/views/alchemy/admin/nodes/_node.html.erb +93 -88
- data/app/views/alchemy/admin/nodes/index.html.erb +15 -23
- data/app/views/alchemy/admin/pages/_create_language_form.html.erb +2 -6
- data/app/views/alchemy/admin/pages/_form.html.erb +2 -1
- data/app/views/alchemy/admin/pages/_publication_fields.html.erb +1 -1
- data/app/views/alchemy/admin/pages/_table.html.erb +2 -4
- data/app/views/alchemy/admin/pages/_toolbar.html.erb +2 -1
- data/app/views/alchemy/admin/pages/edit.html.erb +10 -21
- data/app/views/alchemy/admin/pages/flush.turbo_stream.erb +1 -1
- data/app/views/alchemy/admin/pages/index.html.erb +26 -31
- data/app/views/alchemy/admin/pages/info.html.erb +2 -2
- data/app/views/alchemy/admin/pages/update.turbo_stream.erb +2 -2
- data/app/views/alchemy/admin/partials/_language_tree_select.html.erb +1 -1
- data/app/views/alchemy/admin/partials/_routes.html.erb +24 -48
- data/app/views/alchemy/admin/pictures/_archive.html.erb +3 -17
- data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +1 -1
- data/app/views/alchemy/admin/pictures/_filter_and_size_bar.html.erb +5 -3
- data/app/views/alchemy/admin/pictures/_form.html.erb +0 -1
- data/app/views/alchemy/admin/pictures/_infos.html.erb +5 -1
- data/app/views/alchemy/admin/pictures/_picture.html.erb +1 -1
- data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +3 -4
- data/app/views/alchemy/admin/pictures/assign.turbo_stream.erb +7 -0
- data/app/views/alchemy/admin/pictures/index.html.erb +38 -30
- data/app/views/alchemy/admin/pictures/show.html.erb +41 -41
- data/app/views/alchemy/admin/resources/_empty_state.html.erb +27 -0
- data/app/views/alchemy/admin/resources/_pagination.html.erb +1 -1
- data/app/views/alchemy/admin/resources/_per_page_select.html.erb +1 -1
- data/app/views/alchemy/admin/resources/_resource_table.html.erb +0 -2
- data/app/views/alchemy/admin/resources/_resource_usage_info.html.erb +3 -2
- data/app/views/alchemy/admin/resources/index.html.erb +32 -13
- data/app/views/alchemy/admin/sites/index.html.erb +24 -22
- data/app/views/alchemy/admin/styleguide/index.html.erb +35 -12
- data/app/views/alchemy/admin/tags/index.html.erb +27 -25
- data/app/views/alchemy/admin/tinymce/_setup.html.erb +6 -8
- data/app/views/alchemy/admin/translations/_en.js +3 -0
- data/app/views/alchemy/admin/uploader/_button.html.erb +11 -17
- data/app/views/alchemy/admin/uploader/_setup.html.erb +3 -3
- data/app/views/alchemy/pages/sitemap.xml.erb +2 -0
- data/app/views/kaminari/alchemy/_first_page.html.erb +1 -2
- data/app/views/kaminari/alchemy/_gap.html.erb +0 -1
- data/app/views/kaminari/alchemy/_last_page.html.erb +1 -2
- data/app/views/kaminari/alchemy/_next_page.html.erb +1 -2
- data/app/views/kaminari/alchemy/_page.html.erb +1 -2
- data/app/views/kaminari/alchemy/_paginator.html.erb +0 -1
- data/app/views/kaminari/alchemy/_prev_page.html.erb +1 -2
- data/app/views/layouts/alchemy/admin/dialog.html.erb +3 -0
- data/app/views/layouts/alchemy/admin.html.erb +5 -6
- data/config/brakeman.ignore +23 -1
- data/config/importmap.rb +10 -8
- data/config/initializers/dragonfly.rb +21 -17
- data/config/locales/alchemy.en.yml +52 -11
- data/config/routes.rb +3 -3
- data/db/migrate/20260727120000_add_permitted_roles_to_alchemy_pages.rb +23 -0
- data/db/migrate/20260820120000_add_cached_tag_list_to_taggables.rb +57 -0
- data/lib/alchemy/configuration/number_option.rb +13 -0
- data/lib/alchemy/configuration/proc_option.rb +13 -0
- data/lib/alchemy/configuration.rb +2 -0
- data/lib/alchemy/configurations/admin_components.rb +30 -0
- data/lib/alchemy/configurations/format_matchers.rb +7 -1
- data/lib/alchemy/configurations/main.rb +103 -4
- data/lib/alchemy/configurations/menu.rb +91 -0
- data/lib/alchemy/configurations/menu_item.rb +123 -0
- data/lib/alchemy/configurations/sitemap.rb +1 -0
- data/lib/alchemy/configurations/uploader.rb +15 -1
- data/lib/alchemy/engine.rb +0 -6
- data/lib/alchemy/i18n.rb +26 -0
- data/lib/alchemy/modules.rb +52 -50
- data/lib/alchemy/tasks/generate_thumbnails.rb +99 -0
- data/lib/alchemy/tasks/tidy.rb +1 -1
- data/lib/alchemy/test_support/capybara_helpers.rb +48 -37
- data/lib/alchemy/test_support/current_language_shared_examples.rb +1 -1
- data/lib/alchemy/test_support/factories/dummy_user_factory.rb +4 -1
- data/lib/alchemy/test_support/having_picture_thumbnails_examples.rb +132 -0
- data/lib/alchemy/test_support/shared_ingredient_examples.rb +52 -0
- data/lib/alchemy/test_support/shared_publishable_examples.rb +29 -4
- data/lib/alchemy/upgrader/eight_four.rb +237 -0
- data/lib/alchemy/upgrader/register_module_rewriter.rb +177 -0
- data/lib/alchemy/upgrader.rb +2 -1
- data/lib/alchemy/version.rb +1 -1
- data/lib/alchemy.rb +0 -1
- data/lib/alchemy_cms.rb +0 -13
- data/lib/generators/alchemy/elements/templates/view.html.erb +1 -1
- data/lib/generators/alchemy/elements/templates/view.html.haml +1 -1
- data/lib/generators/alchemy/elements/templates/view.html.slim +1 -1
- data/lib/generators/alchemy/install/install_generator.rb +2 -13
- data/lib/generators/alchemy/install/templates/alchemy.rb.tt +24 -9
- data/lib/generators/alchemy/module/module_generator.rb +1 -0
- data/lib/generators/alchemy/module/templates/ability.rb.tt +1 -1
- data/lib/generators/alchemy/module/templates/module_config.rb.tt +16 -18
- data/lib/generators/alchemy/user_columns_migration/templates/migration.rb.tt +19 -0
- data/lib/generators/alchemy/user_columns_migration/user_columns_migration_generator.rb +30 -0
- data/lib/tasks/alchemy/thumbnails.rake +16 -29
- data/lib/tasks/alchemy/upgrade.rake +45 -3
- data/vendor/javascript/cropperjs.min.js +3 -3
- data/vendor/javascript/floating-ui.min.js +1 -1
- data/vendor/javascript/tinymce.min.js +5 -5
- data/vendor/javascript/tom-select.min.js +1 -1
- metadata +85 -65
- data/app/javascript/alchemy_admin/components/action.js +0 -50
- data/app/javascript/alchemy_admin/dirty.js +0 -55
- data/app/javascript/alchemy_admin/node_tree.js +0 -82
- data/app/javascript/alchemy_admin/picture_selector.js +0 -67
- data/app/javascript/alchemy_admin/templates/compiled.js +0 -1
- data/app/javascript/alchemy_admin/templates/node_folder.hbs +0 -3
- data/app/stylesheets/alchemy/admin/attachment-select.scss +0 -19
- data/app/views/alchemy/admin/_main_navi.html.erb +0 -6
- data/app/views/alchemy/admin/attachments/assign.js.erb +0 -15
- data/app/views/alchemy/admin/pages/_page_layout_filter.html.erb +0 -30
- data/app/views/alchemy/admin/partials/_main_navigation_entry.html.erb +0 -20
- data/app/views/alchemy/admin/partials/_sub_navigation.html.erb +0 -11
- data/app/views/alchemy/admin/pictures/assign.js.erb +0 -13
- data/app/views/alchemy/base/error_notice.js.erb +0 -1
- data/app/views/alchemy/base/permission_denied.js.erb +0 -3
- data/app/views/alchemy/base/redirect.js.erb +0 -11
- data/config/alchemy/config.yml +0 -217
- data/config/alchemy/modules.yml +0 -82
- data/config/initializers/rails_live_reload.rb +0 -13
- data/lib/alchemy/dev_support/live_reload_watcher.rb +0 -5
- data/lib/alchemy/taggable.rb +0 -49
- data/lib/generators/alchemy/install/templates/dragonfly.rb.tt +0 -34
- data/vendor/assets/stylesheets/alchemy_admin/select2.css +0 -1
- data/vendor/javascript/handlebars.min.js +0 -29
- data/vendor/javascript/jquery.min.js +0 -2
- data/vendor/javascript/rails-ujs.min.js +0 -1
- data/vendor/javascript/select2.min.js +0 -23
- /data/{lib → app/controllers/concerns}/alchemy/ability_helper.rb +0 -0
- /data/{lib → app/controllers/concerns}/alchemy/admin/locale.rb +0 -0
- /data/{lib → app/controllers/concerns}/alchemy/admin/preview_time.rb +0 -0
- /data/{lib → app/controllers/concerns}/alchemy/admin/timezone.rb +0 -0
- /data/{lib → app/controllers/concerns}/alchemy/configuration_methods.rb +0 -0
- /data/{lib → app/models}/alchemy/admin/preview_url.rb +0 -0
- /data/{lib → app/models/concerns}/alchemy/hints.rb +0 -0
- /data/{lib → app/models/concerns}/alchemy/name_conversions.rb +0 -0
- /data/{lib → app/services}/alchemy/elements_finder.rb +0 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
# Validates that a url does not carry an executable scheme.
|
|
5
|
+
#
|
|
6
|
+
# Only schemes listed in +Alchemy.config.allowed_url_schemes+ pass. Urls
|
|
7
|
+
# without a scheme (absolute and relative paths, anchors, queries and
|
|
8
|
+
# protocol relative urls) pass as well, they cannot carry executable code.
|
|
9
|
+
#
|
|
10
|
+
# validates_with Alchemy::SafeUrlValidator, attributes: [:url]
|
|
11
|
+
#
|
|
12
|
+
class SafeUrlValidator < ActiveModel::EachValidator
|
|
13
|
+
SCHEME_REGEX = /\A(?<scheme>[a-z][a-z0-9+.-]*):/i
|
|
14
|
+
|
|
15
|
+
# Browsers drop these before they parse a url, which makes "java\tscript:"
|
|
16
|
+
# a working "javascript:" url. They have to go before the scheme is read.
|
|
17
|
+
STRIPPED_CHARS = /[\u0000-\u0020]/
|
|
18
|
+
|
|
19
|
+
def validate_each(record, attribute, value)
|
|
20
|
+
return if value.blank?
|
|
21
|
+
|
|
22
|
+
scheme = SCHEME_REGEX.match(value.to_s.gsub(STRIPPED_CHARS, ""))&.[](:scheme)&.downcase
|
|
23
|
+
return if scheme.nil? || allowed_schemes.include?(scheme)
|
|
24
|
+
|
|
25
|
+
record.errors.add(attribute, :invalid_url_scheme, scheme: scheme)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def allowed_schemes
|
|
31
|
+
Alchemy.config.allowed_url_schemes.map(&:downcase)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/app/models/alchemy/site.rb
CHANGED
|
@@ -5,13 +5,18 @@
|
|
|
5
5
|
# Table name: alchemy_sites
|
|
6
6
|
#
|
|
7
7
|
# id :integer not null, primary key
|
|
8
|
+
# aliases :text
|
|
8
9
|
# host :string
|
|
9
10
|
# name :string
|
|
11
|
+
# public :boolean default(FALSE), not null
|
|
12
|
+
# redirect_to_primary_host :boolean default(FALSE), not null
|
|
10
13
|
# created_at :datetime not null
|
|
11
14
|
# updated_at :datetime not null
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
+
#
|
|
16
|
+
# Indexes
|
|
17
|
+
#
|
|
18
|
+
# alchemy_sites_public_hosts_idx (host,public)
|
|
19
|
+
# index_alchemy_sites_on_host (host)
|
|
15
20
|
#
|
|
16
21
|
|
|
17
22
|
module Alchemy
|
|
@@ -23,9 +23,13 @@ module Alchemy
|
|
|
23
23
|
attr_reader :attachable
|
|
24
24
|
|
|
25
25
|
class << self
|
|
26
|
-
def generate_thumbs!(
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
def generate_thumbs!(picture)
|
|
27
|
+
format = picture.image_file_extension || "jpg"
|
|
28
|
+
Alchemy::Picture::THUMBNAIL_SIZES.each_value do |size|
|
|
29
|
+
transformations = Alchemy::DragonflyToImageProcessing.call(
|
|
30
|
+
flatten: true, format: format, size: size
|
|
31
|
+
)
|
|
32
|
+
::ActiveStorage::TransformJob.perform_later(picture.image_file.blob, transformations)
|
|
29
33
|
end
|
|
30
34
|
end
|
|
31
35
|
|
|
@@ -5,13 +5,16 @@ module Alchemy
|
|
|
5
5
|
def self.included(base)
|
|
6
6
|
base.has_one_attached :image_file do |attachable|
|
|
7
7
|
Alchemy.storage_adapter.preprocessor_class.new(attachable).call
|
|
8
|
-
Alchemy.storage_adapter.preprocessor_class.generate_thumbs!(attachable)
|
|
9
8
|
end
|
|
10
9
|
|
|
11
10
|
base.after_create_commit if: :svg? do
|
|
12
11
|
SanitizeSvgJob.perform_later(self, file_accessor: :image_file)
|
|
13
12
|
end
|
|
14
13
|
|
|
14
|
+
base.after_create_commit if: :has_convertible_format? do
|
|
15
|
+
Alchemy.storage_adapter.preprocessor_class.generate_thumbs!(self)
|
|
16
|
+
end
|
|
17
|
+
|
|
15
18
|
# The image file's dirty state is cleared by the time
|
|
16
19
|
# +after_update_commit+ runs, so capture it here to know whether the
|
|
17
20
|
# blob was actually replaced (rather than only its metadata updated).
|
data/app/models/alchemy/tag.rb
CHANGED
|
@@ -5,8 +5,15 @@
|
|
|
5
5
|
# Table name: gutentag_tags
|
|
6
6
|
#
|
|
7
7
|
# id :integer not null, primary key
|
|
8
|
-
# name :string
|
|
9
|
-
# taggings_count :integer default(0)
|
|
8
|
+
# name :string not null
|
|
9
|
+
# taggings_count :integer default(0), not null
|
|
10
|
+
# created_at :datetime not null
|
|
11
|
+
# updated_at :datetime not null
|
|
12
|
+
#
|
|
13
|
+
# Indexes
|
|
14
|
+
#
|
|
15
|
+
# index_gutentag_tags_on_name (name) UNIQUE
|
|
16
|
+
# index_gutentag_tags_on_taggings_count (taggings_count)
|
|
10
17
|
#
|
|
11
18
|
|
|
12
19
|
# Just holds some useful tag methods.
|
|
@@ -24,8 +31,7 @@ module Alchemy
|
|
|
24
31
|
# Replaces tag with new tag on all models tagged with tag.
|
|
25
32
|
def self.replace(tag, new_tag)
|
|
26
33
|
tag.taggings.collect(&:taggable).each do |taggable|
|
|
27
|
-
taggable.
|
|
28
|
-
taggable.tag_list << new_tag.name
|
|
34
|
+
taggable.tag_names = taggable.tag_names - [tag.name] + [new_tag.name]
|
|
29
35
|
taggable.save
|
|
30
36
|
end
|
|
31
37
|
end
|
|
@@ -5,9 +5,13 @@ module Alchemy
|
|
|
5
5
|
ARCHIVE_FILE_TYPES = ["application/zip", "application/x-rar"]
|
|
6
6
|
|
|
7
7
|
AUDIO_FILE_TYPES = [
|
|
8
|
+
"audio/aac",
|
|
9
|
+
"audio/flac",
|
|
8
10
|
"audio/mpeg",
|
|
9
11
|
"audio/mp4",
|
|
12
|
+
"audio/ogg",
|
|
10
13
|
"audio/wav",
|
|
14
|
+
"audio/webm",
|
|
11
15
|
"audio/x-wav"
|
|
12
16
|
]
|
|
13
17
|
|
|
@@ -29,6 +33,7 @@ module Alchemy
|
|
|
29
33
|
"video/x-flv",
|
|
30
34
|
"video/mp4",
|
|
31
35
|
"video/mpeg",
|
|
36
|
+
"video/ogg",
|
|
32
37
|
"video/quicktime",
|
|
33
38
|
"video/webm",
|
|
34
39
|
"video/x-msvideo",
|
|
@@ -37,27 +37,74 @@ module Alchemy
|
|
|
37
37
|
def picture_url(options = {})
|
|
38
38
|
return if picture.nil?
|
|
39
39
|
|
|
40
|
-
picture.url(picture_url_options
|
|
40
|
+
picture.url(picture_url_options(options)) || "missing-image.png"
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
# Picture rendering options
|
|
44
44
|
#
|
|
45
|
-
# Returns the
|
|
46
|
-
# together with the +crop_from+ and +crop_size+ values
|
|
45
|
+
# Returns the render format of the associated +Alchemy::Picture+
|
|
46
|
+
# together with the +crop_from+ and +crop_size+ values.
|
|
47
|
+
#
|
|
48
|
+
# The render format is taken from the +format+ option, the +format+ setting
|
|
49
|
+
# of the ingredient definition or the +default_render_format+ of the picture.
|
|
50
|
+
# A format is only applied if the picture can be converted.
|
|
51
|
+
#
|
|
52
|
+
# @param options [Hash] Options that override the defaults (i.e. +size+ or +format+ of a srcset entry)
|
|
47
53
|
#
|
|
48
54
|
# @return [HashWithIndifferentAccess]
|
|
49
|
-
def picture_url_options
|
|
55
|
+
def picture_url_options(options = {})
|
|
50
56
|
return {} if picture.nil?
|
|
51
57
|
|
|
58
|
+
options = options.symbolize_keys
|
|
52
59
|
crop = !!settings[:crop]
|
|
53
60
|
|
|
54
61
|
{
|
|
55
|
-
format:
|
|
62
|
+
format: picture_render_format(options.delete(:format) || settings[:format]),
|
|
56
63
|
crop: crop,
|
|
57
64
|
crop_from: crop && crop_from.presence || nil,
|
|
58
65
|
crop_size: crop && crop_size.presence || nil,
|
|
59
66
|
size: settings[:size]
|
|
60
|
-
}.with_indifferent_access
|
|
67
|
+
}.merge(options).with_indifferent_access
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Picture rendering options for each entry of the +srcset+ setting
|
|
71
|
+
#
|
|
72
|
+
# Entries can either be a size string (i.e. +"800x600"+) or a hash
|
|
73
|
+
# with +size+ and optional +format+ keys.
|
|
74
|
+
#
|
|
75
|
+
# @param srcset [Array<String, Hash>] Defaults to the +srcset+ setting
|
|
76
|
+
#
|
|
77
|
+
# @return [Array<HashWithIndifferentAccess>]
|
|
78
|
+
def picture_srcset_variant_options(srcset = settings.fetch(:srcset, []))
|
|
79
|
+
return [] if picture.nil?
|
|
80
|
+
|
|
81
|
+
srcset.map do |src|
|
|
82
|
+
picture_url_options(src.is_a?(Hash) ? src : {size: src})
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Url, dimensions and mime type for each entry of the +srcset+ setting
|
|
87
|
+
#
|
|
88
|
+
# === Example:
|
|
89
|
+
#
|
|
90
|
+
# picture_srcset_options
|
|
91
|
+
# # => [{url: "/pictures/1/360x360/cats.webp", desc: "360w", width: "360", height: "360", type: "image/webp"}]
|
|
92
|
+
#
|
|
93
|
+
# @param srcset [Array<String, Hash>] Defaults to the +srcset+ setting
|
|
94
|
+
#
|
|
95
|
+
# @return [Array<Hash>]
|
|
96
|
+
def picture_srcset_options(srcset = settings.fetch(:srcset, []))
|
|
97
|
+
picture_srcset_variant_options(srcset).map do |options|
|
|
98
|
+
width, height = options[:size].to_s.split("x", 2)
|
|
99
|
+
|
|
100
|
+
{
|
|
101
|
+
url: picture_url(options),
|
|
102
|
+
desc: width.present? ? "#{width}w" : "#{height}h",
|
|
103
|
+
width: width,
|
|
104
|
+
height: height,
|
|
105
|
+
type: Mime::Type.lookup_by_extension(options[:format]).to_s
|
|
106
|
+
}
|
|
107
|
+
end
|
|
61
108
|
end
|
|
62
109
|
|
|
63
110
|
# Returns an url for the thumbnail representation of the assigned picture
|
|
@@ -109,6 +156,14 @@ module Alchemy
|
|
|
109
156
|
|
|
110
157
|
private
|
|
111
158
|
|
|
159
|
+
def picture_render_format(format)
|
|
160
|
+
if format.present? && picture.has_convertible_format?
|
|
161
|
+
format.to_s
|
|
162
|
+
else
|
|
163
|
+
picture.default_render_format
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
112
167
|
# An Image smaller than dimensions
|
|
113
168
|
# can not be cropped to given size - unless upsample is true.
|
|
114
169
|
#
|
|
@@ -3,13 +3,10 @@ module Alchemy
|
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
4
|
|
|
5
5
|
included do
|
|
6
|
-
scope :draft, -> {
|
|
7
|
-
scope :scheduled, -> {
|
|
8
|
-
|
|
6
|
+
scope :draft, -> { Alchemy.config.publishable_resolver.draft(all) }
|
|
7
|
+
scope :scheduled, -> { Alchemy.config.publishable_resolver.scheduled(all) }
|
|
9
8
|
scope :published, ->(at: Current.preview_time) {
|
|
10
|
-
|
|
11
|
-
.where("#{table_name}.public_on <= :at", at:)
|
|
12
|
-
.where("#{table_name}.public_until IS NULL OR #{table_name}.public_until > :at", at:)
|
|
9
|
+
Alchemy.config.publishable_resolver.published(all, at:)
|
|
13
10
|
}
|
|
14
11
|
|
|
15
12
|
validate do
|
|
@@ -23,42 +20,32 @@ module Alchemy
|
|
|
23
20
|
|
|
24
21
|
# Determines if this record is public
|
|
25
22
|
#
|
|
26
|
-
#
|
|
27
|
-
# and returns true if the time given (+Time.current+ per default)
|
|
28
|
-
# is in this timespan.
|
|
29
|
-
#
|
|
30
|
-
# @param at [DateTime] (Time.current)
|
|
23
|
+
# @param at [DateTime] (Current.preview_time)
|
|
31
24
|
# @returns Boolean
|
|
32
25
|
def public?(at: Current.preview_time)
|
|
33
|
-
|
|
26
|
+
publishable_resolver.public?(at:)
|
|
34
27
|
end
|
|
35
28
|
alias_method :public, :public?
|
|
36
29
|
|
|
30
|
+
# Determines if this record has a future publication or expiration event
|
|
31
|
+
#
|
|
32
|
+
# @param at [DateTime] (Current.preview_time)
|
|
33
|
+
# @returns Boolean
|
|
37
34
|
def scheduled?(at: Current.preview_time)
|
|
38
|
-
|
|
35
|
+
publishable_resolver.scheduled?(at:)
|
|
39
36
|
end
|
|
40
37
|
|
|
41
38
|
# Determines if this record is publishable
|
|
42
39
|
#
|
|
43
|
-
# A record is publishable if a +public_on+ timestamp is set and not expired yet.
|
|
44
|
-
#
|
|
45
40
|
# @returns Boolean
|
|
46
41
|
def publishable?
|
|
47
|
-
|
|
42
|
+
publishable_resolver.publishable?
|
|
48
43
|
end
|
|
49
44
|
|
|
50
|
-
|
|
51
|
-
# @param at [DateTime] (Time.current)
|
|
52
|
-
# @returns Boolean
|
|
53
|
-
def already_public_for?(at: Current.preview_time)
|
|
54
|
-
!public_on.nil? && public_on <= at
|
|
55
|
-
end
|
|
45
|
+
private
|
|
56
46
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
# @returns Boolean
|
|
60
|
-
def still_public_for?(at: Current.preview_time)
|
|
61
|
-
public_until.nil? || public_until > at
|
|
47
|
+
def publishable_resolver
|
|
48
|
+
@_publishable_resolver ||= Alchemy.config.publishable_resolver.new(self)
|
|
62
49
|
end
|
|
63
50
|
end
|
|
64
51
|
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
# ActsAsTaggableOn to Gutentag interface compatibility module
|
|
5
|
+
# Include this module to add tagging support to your model.
|
|
6
|
+
module Taggable
|
|
7
|
+
def self.included(base)
|
|
8
|
+
Gutentag::ActiveRecord.call base
|
|
9
|
+
base.extend ClassMethods
|
|
10
|
+
|
|
11
|
+
# Opt-in denormalized cache, maintained only for taggables whose table
|
|
12
|
+
# has a cached_tag_list column. serialize defers until the schema loads,
|
|
13
|
+
# so declaring it unconditionally never touches the DB at boot and is
|
|
14
|
+
# harmless for taggables without the column.
|
|
15
|
+
base.attribute :cached_tag_list, default: -> { [] }
|
|
16
|
+
base.serialize :cached_tag_list, coder: JSON
|
|
17
|
+
base.before_save :remember_tag_name_change
|
|
18
|
+
base.after_save :cache_tag_list
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# The public list of tag names. Reads the denormalized cache column so the
|
|
22
|
+
# common read paths (rendering, serializing) issue no query. Models without
|
|
23
|
+
# the cache column fall back to gutentag's lazy-loading tag_names.
|
|
24
|
+
#
|
|
25
|
+
# The column is nullable, so rows written before the cache was introduced
|
|
26
|
+
# read back nil, which we coerce to an empty array.
|
|
27
|
+
def tag_list
|
|
28
|
+
caches_tag_list? ? (cached_tag_list || []) : tag_names
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Set a list of tags
|
|
32
|
+
# Pass a String with comma separated tag names or
|
|
33
|
+
# an Array of tag names
|
|
34
|
+
def tag_list=(tags)
|
|
35
|
+
case tags
|
|
36
|
+
when String
|
|
37
|
+
self.tag_names = tags.split(/,\s*/)
|
|
38
|
+
when Array
|
|
39
|
+
self.tag_names = tags
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# gutentag reconciles the tags in an after_save callback, and reading its
|
|
46
|
+
# tag_names accessor before that runs would lazy-load from the still-empty
|
|
47
|
+
# tags association of an unsaved record. So we remember whether the tags
|
|
48
|
+
# changed here (query-free) and read the reconciled value after the save.
|
|
49
|
+
def remember_tag_name_change
|
|
50
|
+
@tag_names_changed = will_save_change_to_tag_names?
|
|
51
|
+
true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Keeps the denormalized cached_tag_list column in sync with the tags.
|
|
55
|
+
# Only runs when the tags actually changed, so untagged saves stay
|
|
56
|
+
# query-free.
|
|
57
|
+
def cache_tag_list
|
|
58
|
+
return unless @tag_names_changed
|
|
59
|
+
return unless caches_tag_list?
|
|
60
|
+
|
|
61
|
+
update_column(:cached_tag_list, Array(tag_names))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def caches_tag_list?
|
|
65
|
+
self.class.column_names.include?("cached_tag_list")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
module ClassMethods
|
|
69
|
+
def tagged_with(names = [], **args)
|
|
70
|
+
if names.is_a? String
|
|
71
|
+
names = names.split(/,\s*/)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
unless args[:match]
|
|
75
|
+
args[:match] = :all
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if names.any?
|
|
79
|
+
args[:names] = names
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
super(args)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Returns all unique tags
|
|
86
|
+
def tag_counts
|
|
87
|
+
Gutentag::Tag.distinct.joins(:taggings)
|
|
88
|
+
.where(gutentag_taggings: {taggable_type: name})
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
module Alchemy
|
|
2
|
+
module UserMethods
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
# Unlock all locked pages before destroy.
|
|
7
|
+
before_destroy :unlock_pages!
|
|
8
|
+
|
|
9
|
+
scope :alchemy_admins, -> { with_alchemy_role(:admin) }
|
|
10
|
+
|
|
11
|
+
scope :with_alchemy_role, ->(role) {
|
|
12
|
+
role = role.to_s.downcase
|
|
13
|
+
unless Alchemy.config.user_roles.include?(role)
|
|
14
|
+
Alchemy::Logger.warn("Unknown Alchemy role: #{role.inspect}")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
col = arel_table[:alchemy_roles]
|
|
18
|
+
where(
|
|
19
|
+
col.eq(role)
|
|
20
|
+
.or(col.matches("#{role} %"))
|
|
21
|
+
.or(col.matches("% #{role} %"))
|
|
22
|
+
.or(col.matches("% #{role}"))
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
validates :alchemy_roles,
|
|
27
|
+
inclusion: {
|
|
28
|
+
in: Alchemy.config.user_roles,
|
|
29
|
+
message: "is not included in #{Alchemy.config.user_roles.join(", ")}"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Returns all pages locked by user.
|
|
33
|
+
#
|
|
34
|
+
# A page gets locked when the user starts to edit the page.
|
|
35
|
+
#
|
|
36
|
+
has_many :locked_pages,
|
|
37
|
+
-> { order(:locked_at) },
|
|
38
|
+
foreign_key: :locked_by,
|
|
39
|
+
class_name: "Alchemy::Page",
|
|
40
|
+
inverse_of: :locker
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class_methods do
|
|
44
|
+
def human_alchemy_rolename(role)
|
|
45
|
+
Alchemy.t("user_roles.#{role}")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def alchemy_admin?
|
|
50
|
+
has_alchemy_role?("admin")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def alchemy_roles
|
|
54
|
+
if self.class.columns_hash["alchemy_roles"]
|
|
55
|
+
read_attribute(:alchemy_roles).to_s.split(" ")
|
|
56
|
+
else
|
|
57
|
+
Alchemy::Logger.warn <<-WARN.strip_heredoc
|
|
58
|
+
The `alchemy_roles` column is missing in the #{self.class.table_name} table!
|
|
59
|
+
|
|
60
|
+
Please run
|
|
61
|
+
|
|
62
|
+
rails generate alchemy:user_columns_migration
|
|
63
|
+
|
|
64
|
+
to add it.
|
|
65
|
+
|
|
66
|
+
Falling back to default role: `member`.
|
|
67
|
+
WARN
|
|
68
|
+
["member"]
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def alchemy_roles=(roles)
|
|
73
|
+
roles = Array(roles).map { _1.to_s.strip }
|
|
74
|
+
roles.reject!(&:blank?)
|
|
75
|
+
roles.uniq!
|
|
76
|
+
super(roles.join(" "))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Returns true if the user has the given role.
|
|
80
|
+
def has_alchemy_role?(role)
|
|
81
|
+
alchemy_roles.include?(role.to_s)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def human_alchemy_roles
|
|
85
|
+
alchemy_roles.map do |role|
|
|
86
|
+
self.class.human_alchemy_rolename(role)
|
|
87
|
+
end.to_sentence
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Calls unlock on all locked pages
|
|
91
|
+
def unlock_pages!
|
|
92
|
+
locked_pages.each(&:unlock!)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -4,6 +4,7 @@ module Alchemy
|
|
|
4
4
|
class DuplicateElement
|
|
5
5
|
SKIPPED_ATTRIBUTES_ON_COPY = [
|
|
6
6
|
"cached_tag_list",
|
|
7
|
+
"tag_names",
|
|
7
8
|
"created_at",
|
|
8
9
|
"creator_id",
|
|
9
10
|
"position",
|
|
@@ -27,11 +28,13 @@ module Alchemy
|
|
|
27
28
|
.merge(differences)
|
|
28
29
|
.merge(
|
|
29
30
|
autogenerate_ingredients: false,
|
|
30
|
-
autogenerate_nested_elements: false
|
|
31
|
-
tags: source_element.tags
|
|
31
|
+
autogenerate_nested_elements: false
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
new_element = Element.new(attributes)
|
|
35
|
+
# Only assign tags when the source has any, so copying untagged elements
|
|
36
|
+
# (the common case) does not trigger gutentag's tag reconciliation.
|
|
37
|
+
new_element.tag_list = source_element.tag_list if source_element.tag_list.present?
|
|
35
38
|
new_element.ingredients = source_element.ingredients.map(&:dup)
|
|
36
39
|
new_element.save!
|
|
37
40
|
|
|
@@ -152,8 +152,10 @@
|
|
|
152
152
|
--pagination-disabled-text-color: var(--a-light-grey);
|
|
153
153
|
|
|
154
154
|
/* Panels */
|
|
155
|
-
--panel-background-color: var(--
|
|
155
|
+
--panel-background-color: var(--body-background-color);
|
|
156
156
|
--panel-border-color: var(--a-grey);
|
|
157
|
+
--panel-box-shadow: 0 var(--spacing-0) var(--spacing-2)
|
|
158
|
+
hsla(0deg, 0%, 0%, 0.5);
|
|
157
159
|
|
|
158
160
|
--picture-thumbnail-background-color: var(--a-dark-grey);
|
|
159
161
|
--picture-thumbnail-toolbar-bg-color: var(--a-darker-grey);
|
|
@@ -172,6 +174,8 @@
|
|
|
172
174
|
--select-hover-bg-color: var(--color-blue_dark);
|
|
173
175
|
--select-hover-border-color: var(--form-field-border-color);
|
|
174
176
|
--select-hover-text-color: var(--text-color);
|
|
177
|
+
--select-selected-bg-color: var(--a-grey);
|
|
178
|
+
--select-selected-marker-color: var(--color-blue_medium);
|
|
175
179
|
--select-background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" fill="hsl(200deg, 5%, 70%)" viewBox="0 0 24 24"><path d="M11.9997 13.1714L16.9495 8.22168L18.3637 9.63589L11.9997 15.9999L5.63574 9.63589L7.04996 8.22168L11.9997 13.1714Z"></path></svg>');
|
|
176
180
|
--select-close-icon: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="hsl(200deg, 5%, 70%)"><path d="M11.9997 10.5865L16.9495 5.63672L18.3637 7.05093L13.4139 12.0007L18.3637 16.9504L16.9495 18.3646L11.9997 13.4149L7.04996 18.3646L5.63574 16.9504L10.5855 12.0007L5.63574 7.05093L7.04996 5.63672L11.9997 10.5865Z"></path></svg>');
|
|
177
181
|
--select-disabled-background-color: transparent;
|
|
@@ -284,12 +288,6 @@
|
|
|
284
288
|
--sl-color-neutral-800: var(--a-lightest-grey);
|
|
285
289
|
--sl-color-neutral-900: var(--a-lightest-grey);
|
|
286
290
|
--sl-color-neutral-950: var(--a-lightest-grey);
|
|
287
|
-
|
|
288
|
-
.select2-container.select2-container-multi
|
|
289
|
-
.select2-search-choice-close:before,
|
|
290
|
-
.select2-container.select2-allowclear .select2-search-choice-close:before {
|
|
291
|
-
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="hsl(200deg, 5%, 70%)"><path d="M11.9997 10.5865L16.9495 5.63672L18.3637 7.05093L13.4139 12.0007L18.3637 16.9504L16.9495 18.3646L11.9997 13.4149L7.04996 18.3646L5.63574 16.9504L10.5855 12.0007L5.63574 7.05093L7.04996 5.63672L11.9997 10.5865Z"></path></svg>');
|
|
292
|
-
}
|
|
293
291
|
}
|
|
294
292
|
|
|
295
293
|
@mixin light-theme {
|
|
@@ -434,8 +432,9 @@
|
|
|
434
432
|
--pagination-disabled-text-color: #c0c0c0;
|
|
435
433
|
|
|
436
434
|
/* Panels */
|
|
437
|
-
--panel-background-color: var(--color
|
|
438
|
-
--panel-border-color: var(--
|
|
435
|
+
--panel-background-color: var(--body-background-color);
|
|
436
|
+
--panel-border-color: var(--color-grey_light);
|
|
437
|
+
--panel-box-shadow: var(--dashboard-widget-box-shadow);
|
|
439
438
|
|
|
440
439
|
--picture-tool-background-color: var(--color-white);
|
|
441
440
|
--picture-tool-hover-background-color: var(--color-grey_very_light);
|
|
@@ -452,6 +451,8 @@
|
|
|
452
451
|
--select-hover-bg-color: var(--color-blue_dark);
|
|
453
452
|
--select-hover-border-color: var(--border-color);
|
|
454
453
|
--select-hover-text-color: var(--color-white);
|
|
454
|
+
--select-selected-bg-color: var(--color-grey_light);
|
|
455
|
+
--select-selected-marker-color: var(--color-blue_medium);
|
|
455
456
|
--select-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="M11.9999 13.1714L16.9497 8.22168L18.3639 9.63589L11.9999 15.9999L5.63599 9.63589L7.0502 8.22168L11.9999 13.1714Z"></path></svg>');
|
|
456
457
|
--select-close-icon: 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="M11.9997 10.5865L16.9495 5.63672L18.3637 7.05093L13.4139 12.0007L18.3637 16.9504L16.9495 18.3646L11.9997 13.4149L7.04996 18.3646L5.63574 16.9504L10.5855 12.0007L5.63574 7.05093L7.04996 5.63672L11.9997 10.5865Z"></path></svg>');
|
|
457
458
|
--select-disabled-background-color: transparent;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
.empty-state {
|
|
2
|
+
width: 100%;
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
align-items: center;
|
|
6
|
+
gap: var(--spacing-2);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.empty-state-header {
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
align-items: center;
|
|
13
|
+
gap: var(--spacing-2);
|
|
14
|
+
margin-top: var(--spacing-2);
|
|
15
|
+
width: 100%;
|
|
16
|
+
|
|
17
|
+
> svg {
|
|
18
|
+
width: var(--icon-size-xl);
|
|
19
|
+
height: auto;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.empty-state-notice {
|
|
23
|
+
text-align: center;
|
|
24
|
+
padding-bottom: var(--spacing-4);
|
|
25
|
+
border-bottom: 1px solid var(--panel-border-color);
|
|
26
|
+
margin-bottom: var(--spacing-4);
|
|
27
|
+
text-shadow: var(--text-shadow-light);
|
|
28
|
+
}
|
|
29
|
+
}
|