alchemy_cms 8.3.10 → 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 -29
- 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/stylesheets/tinymce/skins/ui/alchemy/skin.scss +19 -0
- data/app/stylesheets/tinymce/skins/ui/alchemy-dark/skin.scss +19 -0
- 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 +53 -12
- 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 +39 -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,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
# Renders a search engine compatible XML sitemap.
|
|
5
|
+
#
|
|
6
|
+
# The sitemap contains absolute urls, so it has to follow the site's primary
|
|
7
|
+
# host redirect just like a page request does.
|
|
8
|
+
#
|
|
9
|
+
class SitemapController < Alchemy::BaseController
|
|
10
|
+
include SiteRedirects
|
|
11
|
+
|
|
12
|
+
# Still the pages template, because apps override it at that path.
|
|
13
|
+
#
|
|
14
|
+
# TODO: Move to alchemy/sitemap/show in Alchemy 9.0, together with the
|
|
15
|
+
# deprecated PagesController#sitemap.
|
|
16
|
+
TEMPLATE = "alchemy/pages/sitemap"
|
|
17
|
+
|
|
18
|
+
def show
|
|
19
|
+
if sitemap_caching_enabled?
|
|
20
|
+
expires_in sitemap_max_age, public: true
|
|
21
|
+
|
|
22
|
+
# The etag is checked before the pages are loaded, so a revalidating
|
|
23
|
+
# client only costs us the cache key queries, not a full render.
|
|
24
|
+
return unless stale?(etag: sitemap_cache_key, public: true)
|
|
25
|
+
|
|
26
|
+
@sitemap_cache_key = sitemap_cache_key
|
|
27
|
+
@sitemap_max_age = sitemap_max_age
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Left unloaded on purpose. The template wraps the urls in a fragment
|
|
31
|
+
# cache, so a cache hit never runs this query.
|
|
32
|
+
@pages = Page.sitemap
|
|
33
|
+
|
|
34
|
+
render template: TEMPLATE, layout: "alchemy/sitemap"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def sitemap_cache_key
|
|
40
|
+
@_sitemap_cache_key ||= Page::SitemapCacheKey.new(
|
|
41
|
+
site: Current.site,
|
|
42
|
+
base_url: request.base_url,
|
|
43
|
+
max_age: sitemap_max_age
|
|
44
|
+
).call
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def sitemap_max_age
|
|
48
|
+
Alchemy.config.sitemap.max_age
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The sitemap has its own cache setting and does not follow the page cache.
|
|
52
|
+
# A max_age of zero (or less) disables it, because it would leave the time
|
|
53
|
+
# bucket undefined.
|
|
54
|
+
def sitemap_caching_enabled?
|
|
55
|
+
perform_caching && sitemap_max_age.to_i.positive?
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
module Admin
|
|
5
|
+
# Applies Alchemy's own Content Security Policy to the including controller.
|
|
6
|
+
#
|
|
7
|
+
# Include it into any controller that renders the Alchemy admin but does not
|
|
8
|
+
# inherit from +Alchemy::Admin::BaseController+, so that its responses carry
|
|
9
|
+
# the same policy as the rest of the admin:
|
|
10
|
+
#
|
|
11
|
+
# class Alchemy::Admin::PasswordsController < Devise::PasswordsController
|
|
12
|
+
# include Alchemy::Admin::CspProtection
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# Including it is opting in, so the policy applies to every action. Override
|
|
16
|
+
# +apply_content_security_policy?+ to narrow that down.
|
|
17
|
+
#
|
|
18
|
+
# The policy itself comes from +Alchemy.config.admin_content_security_policy+
|
|
19
|
+
# and is left alone when the host application configured one of its own.
|
|
20
|
+
module CspProtection
|
|
21
|
+
extend ActiveSupport::Concern
|
|
22
|
+
|
|
23
|
+
included do
|
|
24
|
+
before_action :set_content_security_policy, if: :apply_content_security_policy?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def apply_content_security_policy? = true
|
|
30
|
+
|
|
31
|
+
def set_content_security_policy
|
|
32
|
+
policy_class = Alchemy.config.admin_content_security_policy
|
|
33
|
+
return unless policy_class
|
|
34
|
+
return if request.content_security_policy
|
|
35
|
+
|
|
36
|
+
policy = policy_class.new(request)
|
|
37
|
+
request.content_security_policy_nonce_generator ||= policy.nonce_generator
|
|
38
|
+
request.content_security_policy = policy.call
|
|
39
|
+
request.content_security_policy_report_only = policy.report_only?
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -21,6 +21,8 @@ module Alchemy
|
|
|
21
21
|
# add_alchemy_filter :by_location, type: :select, options: ->(query) { Location.pluck(:name, :id) }
|
|
22
22
|
# add_alchemy_filter :future, type: :checkbox
|
|
23
23
|
# add_alchemy_filter :by_timeframe, type: :select, options: ["today", "tomorrow"]
|
|
24
|
+
# add_alchemy_filter :by_tag, type: :select, options: ->(query) { Tag.pluck(:name) }, multiple: true, include_blank: false
|
|
25
|
+
# add_alchemy_filter :by_tag, type: :select, options: ->(query) { Tag.pluck(:name) }, multiple: ->(params) { params[:only].present? }
|
|
24
26
|
def add_alchemy_filter(name, type:, **args)
|
|
25
27
|
alchemy_filters << "Alchemy::Admin::Filters::#{type.to_s.camelize}".constantize.new(
|
|
26
28
|
name:,
|
|
@@ -74,7 +76,7 @@ module Alchemy
|
|
|
74
76
|
def permitted_ransack_search_fields
|
|
75
77
|
[
|
|
76
78
|
resource_handler.search_field_name
|
|
77
|
-
] + alchemy_filters.
|
|
79
|
+
] + alchemy_filters.flat_map(&:permitted_search_params)
|
|
78
80
|
end
|
|
79
81
|
|
|
80
82
|
def resource_has_filters
|
|
@@ -7,6 +7,7 @@ module Alchemy
|
|
|
7
7
|
included do
|
|
8
8
|
before_action :set_current_alchemy_site
|
|
9
9
|
before_action :set_alchemy_language
|
|
10
|
+
before_action :set_current_alchemy_user
|
|
10
11
|
|
|
11
12
|
helper "alchemy/pages"
|
|
12
13
|
|
|
@@ -59,6 +60,18 @@ module Alchemy
|
|
|
59
60
|
Current.site = current_alchemy_site
|
|
60
61
|
end
|
|
61
62
|
|
|
63
|
+
# Stores the current user in +Current.user+ so models and services can
|
|
64
|
+
# access the acting user without threading it through every call.
|
|
65
|
+
#
|
|
66
|
+
# Apps without a configured +current_user_method+ leave it +nil+ instead
|
|
67
|
+
# of raising, so the public frontend keeps working without authentication.
|
|
68
|
+
#
|
|
69
|
+
def set_current_alchemy_user
|
|
70
|
+
Current.user = current_alchemy_user
|
|
71
|
+
rescue NoCurrentUserFoundError
|
|
72
|
+
Current.user = nil
|
|
73
|
+
end
|
|
74
|
+
|
|
62
75
|
# Sets the current language for Alchemy.
|
|
63
76
|
#
|
|
64
77
|
def set_alchemy_language(lang = nil)
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
module Alchemy
|
|
4
4
|
# Handles Legacy page redirects
|
|
5
5
|
#
|
|
6
|
-
# If the page could not be found via its urlname we try to find
|
|
7
|
-
# a legacy page url for requested url to redirect to.
|
|
6
|
+
# If the page could not be found via its urlname or is not public we try to find
|
|
7
|
+
# a legacy page url of a published page for requested url to redirect to.
|
|
8
8
|
#
|
|
9
9
|
module LegacyPageRedirects
|
|
10
10
|
extend ActiveSupport::Concern
|
|
@@ -22,7 +22,7 @@ module Alchemy
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def redirect_to_legacy_url?
|
|
25
|
-
(@page.nil? || request.format.nil?) && last_legacy_url
|
|
25
|
+
(@page.nil? || !@page.public? || request.format.nil?) && last_legacy_url
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
# Use the bare minimum to redirect to legacy page
|
|
@@ -48,7 +48,7 @@ module Alchemy
|
|
|
48
48
|
Page.table_name => {
|
|
49
49
|
language_id: Current.language.id
|
|
50
50
|
}
|
|
51
|
-
)
|
|
51
|
+
).where(page_id: Page.published.select(:id))
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def last_legacy_url
|
|
@@ -272,8 +272,11 @@ module Alchemy
|
|
|
272
272
|
end
|
|
273
273
|
|
|
274
274
|
# Returns the regular expression used for external url validation in link dialog.
|
|
275
|
+
#
|
|
276
|
+
# Anchored with ^ instead of \A because the link dialog receives this as a
|
|
277
|
+
# JavaScript literal, where Ruby's anchors are a syntax error.
|
|
275
278
|
def link_url_regexp
|
|
276
|
-
Alchemy.config.format_matchers.link_url ||
|
|
279
|
+
Alchemy.config.format_matchers.link_url || Alchemy::Configurations::FormatMatchers::LINK_URL
|
|
277
280
|
end
|
|
278
281
|
|
|
279
282
|
# Renders a hint with tooltip
|
|
@@ -10,13 +10,11 @@ module Alchemy
|
|
|
10
10
|
# == Defaults
|
|
11
11
|
#
|
|
12
12
|
# * It uses Alchemy::Forms::Builder as builder
|
|
13
|
-
# * It makes a remote request, if the request was XHR request.
|
|
14
13
|
# * It adds the alchemy class to form
|
|
15
14
|
#
|
|
16
15
|
def alchemy_form_for(object, *args, &block)
|
|
17
16
|
options = args.extract_options!
|
|
18
17
|
options[:builder] = Alchemy::Forms::Builder
|
|
19
|
-
options.key?(:remote) || options[:remote] = request.xhr?
|
|
20
18
|
options[:html] = {
|
|
21
19
|
id: options.delete(:id),
|
|
22
20
|
class: ["alchemy", options.delete(:class)].compact.join(" "),
|
|
@@ -11,212 +11,41 @@ module Alchemy
|
|
|
11
11
|
# The Hash representing a Alchemy module
|
|
12
12
|
#
|
|
13
13
|
def alchemy_main_navigation_entry(alchemy_module)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"alchemy/admin/partials/main_navigation_entry",
|
|
17
|
-
alchemy_module: alchemy_module,
|
|
18
|
-
navigation: alchemy_module["navigation"]
|
|
14
|
+
render Alchemy::Admin::MainNavigation::Entry.new(
|
|
15
|
+
item: Alchemy::Configurations::MenuItem.from_legacy_hash(alchemy_module)
|
|
19
16
|
)
|
|
20
17
|
end
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
#
|
|
24
|
-
# To let your module be navigatable by the user you have to provide an Ability for it.
|
|
25
|
-
#
|
|
26
|
-
# === Example:
|
|
27
|
-
#
|
|
28
|
-
# # module.yml
|
|
29
|
-
# name: 'my_module'
|
|
30
|
-
# navigation: {
|
|
31
|
-
# controller: 'my/admin/posts'
|
|
32
|
-
# action: 'index'
|
|
33
|
-
# }
|
|
34
|
-
#
|
|
35
|
-
# # ability.rb
|
|
36
|
-
# can :index, :my_admin_posts
|
|
37
|
-
#
|
|
38
|
-
def navigate_module(navigation)
|
|
39
|
-
[
|
|
40
|
-
navigation["action"].to_sym,
|
|
41
|
-
navigation["controller"].to_s.gsub(/\A\//, "").tr("/", "_").to_sym
|
|
42
|
-
]
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# CSS classes for main navigation entry.
|
|
46
|
-
#
|
|
47
|
-
def main_navigation_css_classes(navigation)
|
|
48
|
-
[
|
|
49
|
-
"main_navi_entry",
|
|
50
|
-
admin_mainnavi_active?(navigation) ? "active" : nil,
|
|
51
|
-
navigation.key?("sub_navigation") ? "has_sub_navigation" : nil
|
|
52
|
-
].compact
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Returns true if given navi entry is in params controller and action
|
|
56
|
-
#
|
|
57
|
-
# == Example
|
|
58
|
-
# <%= entry_active?({controller: 'admin/users', action: 'index'}) %>
|
|
59
|
-
#
|
|
60
|
-
# @param [Hash]
|
|
61
|
-
# A Alchemy module definition navigation entry
|
|
62
|
-
#
|
|
63
|
-
def entry_active?(entry)
|
|
64
|
-
is_entry_controller_active?(entry) && is_entry_action_active?(entry)
|
|
65
|
-
end
|
|
18
|
+
deprecate alchemy_main_navigation_entry: "Render `Alchemy::Admin::MainNavigation::Entry` instead",
|
|
19
|
+
deprecator: Alchemy::Deprecation
|
|
66
20
|
|
|
67
21
|
# Returns url for given Alchemy module.
|
|
68
22
|
#
|
|
69
|
-
# If the module is inside an engine it calls the +url_for+ helper on the engines routing proxy.
|
|
70
|
-
#
|
|
71
|
-
# If the module is inside the host rails app it calls the +url_for+ helper on the main_app routing proxy.
|
|
72
|
-
#
|
|
73
23
|
# @param [Hash]
|
|
74
24
|
# A Alchemy module definition
|
|
75
25
|
#
|
|
76
26
|
def url_for_module(alchemy_module)
|
|
77
|
-
|
|
78
|
-
alchemy_module["engine_name"],
|
|
79
|
-
url_options_for_module(alchemy_module)
|
|
80
|
-
)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Returns url for given Alchemy module sub navigation entry.
|
|
84
|
-
#
|
|
85
|
-
# If the module is inside an engine it calls the +url_for+ helper on the engines routing proxy.
|
|
86
|
-
#
|
|
87
|
-
# If the module is inside the host rails app it calls the +url_for+ helper on the main_app routing proxy.
|
|
88
|
-
#
|
|
89
|
-
# @param [Hash]
|
|
90
|
-
# A Alchemy module sub navigation definition
|
|
91
|
-
#
|
|
92
|
-
def url_for_module_sub_navigation(navigation)
|
|
93
|
-
alchemy_module = module_definition_for(navigation)
|
|
94
|
-
return if alchemy_module.nil?
|
|
95
|
-
|
|
96
|
-
route_from_engine_or_main_app(
|
|
97
|
-
alchemy_module["engine_name"],
|
|
98
|
-
url_options_for_navigation_entry(navigation)
|
|
99
|
-
)
|
|
27
|
+
Alchemy::Configurations::MenuItem.from_legacy_hash(alchemy_module).path(self)
|
|
100
28
|
end
|
|
29
|
+
deprecate url_for_module: "Use `Alchemy::Configurations::MenuItem#path` instead",
|
|
30
|
+
deprecator: Alchemy::Deprecation
|
|
101
31
|
|
|
102
32
|
# Alchemy modules for main navigation.
|
|
103
33
|
#
|
|
104
34
|
# Sorted by position attribute, if given.
|
|
105
35
|
#
|
|
106
36
|
def sorted_alchemy_modules
|
|
107
|
-
|
|
108
|
-
not_sorted = []
|
|
109
|
-
alchemy_modules.map do |m|
|
|
110
|
-
if m["position"].blank?
|
|
111
|
-
not_sorted << m
|
|
112
|
-
else
|
|
113
|
-
sorted << m
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
sorted.sort_by { |m| m["position"] } + not_sorted
|
|
37
|
+
Alchemy::Admin::MainNavigation::Menu.new.items.map { |menu_item| Alchemy::Modules.to_legacy_hash(menu_item) }
|
|
117
38
|
end
|
|
39
|
+
deprecate sorted_alchemy_modules: "Use `Alchemy.config.menu` instead",
|
|
40
|
+
deprecator: Alchemy::Deprecation
|
|
118
41
|
|
|
119
42
|
private
|
|
120
43
|
|
|
121
|
-
# Calls +url_for+ helper on engine if present or on host app.
|
|
122
|
-
#
|
|
123
|
-
# @param [String]
|
|
124
|
-
# A name of an engine
|
|
125
|
-
# @param [Hash]
|
|
126
|
-
# url options hash passed to +url_for+ helper
|
|
127
|
-
#
|
|
128
|
-
def route_from_engine_or_main_app(engine_name, url_options)
|
|
129
|
-
if engine_name.present?
|
|
130
|
-
send(engine_name).url_for(url_options)
|
|
131
|
-
else
|
|
132
|
-
main_app.url_for(url_options)
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# Returns a url options hash for given Alchemy module.
|
|
137
|
-
#
|
|
138
|
-
# @param [Hash]
|
|
139
|
-
# A Alchemy module definition
|
|
140
|
-
#
|
|
141
|
-
def url_options_for_module(alchemy_module)
|
|
142
|
-
url_options_for_navigation_entry(alchemy_module["navigation"] || {})
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# Validates the existence of a given controller configuration.
|
|
146
|
-
#
|
|
147
|
-
# @param String
|
|
148
|
-
# The controller name
|
|
149
|
-
def validate_controller_existence!(definition_hash)
|
|
150
|
-
controllers = [definition_hash["navigation"]["controller"]]
|
|
151
|
-
|
|
152
|
-
if definition_hash["navigation"]["sub_navigation"].is_a?(Array)
|
|
153
|
-
controllers.concat(definition_hash["navigation"]["sub_navigation"].map { |x| x["controller"] })
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
controllers.each do |controller|
|
|
157
|
-
controller_const_name = "#{controller.camelize}Controller"
|
|
158
|
-
controller_const_name.constantize
|
|
159
|
-
rescue NameError
|
|
160
|
-
raise "Error in AlchemyCMS module definition: '#{definition_hash["name"]}'. Could not find the " \
|
|
161
|
-
"matching controller class #{controller_const_name} for the specified controller: '#{controller}'"
|
|
162
|
-
end
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
# Returns a url options hash for given navigation entry.
|
|
166
|
-
#
|
|
167
|
-
# @param [Hash]
|
|
168
|
-
# A Alchemy module navigation entry
|
|
169
|
-
#
|
|
170
|
-
def url_options_for_navigation_entry(entry)
|
|
171
|
-
{
|
|
172
|
-
controller: entry["controller"],
|
|
173
|
-
action: entry["action"],
|
|
174
|
-
only_path: true,
|
|
175
|
-
params: entry["params"]
|
|
176
|
-
}.delete_if { |_k, v| v.nil? }
|
|
177
|
-
end
|
|
178
|
-
|
|
179
44
|
# Retrieves the current Alchemy module from controller and index action.
|
|
180
45
|
#
|
|
181
46
|
def current_alchemy_module
|
|
182
47
|
module_definition_for(controller: params[:controller], action: "index")
|
|
183
48
|
end
|
|
184
|
-
|
|
185
|
-
# Returns true if the current controller and action is in a modules navigation definition.
|
|
186
|
-
#
|
|
187
|
-
def admin_mainnavi_active?(navigation)
|
|
188
|
-
# Has the given navigation entry a active sub navigation?
|
|
189
|
-
has_active_entry?(navigation["sub_navigation"] || []) ||
|
|
190
|
-
# Has the given navigation entry a active nested navigation?
|
|
191
|
-
has_active_entry?(navigation["nested"] || []) ||
|
|
192
|
-
# Is the navigation entry active?
|
|
193
|
-
entry_active?(navigation || {})
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
# Returns true if the given entry's controller is current controller
|
|
197
|
-
#
|
|
198
|
-
def is_entry_controller_active?(entry)
|
|
199
|
-
entry["controller"].gsub(/\A\//, "") == params[:controller] ||
|
|
200
|
-
entry.fetch("nested_controllers", []).include?(params[:controller])
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
# Returns true if the given entry's action is current controllers action
|
|
204
|
-
#
|
|
205
|
-
# Also checks if given entry has a +nested_actions+ key, if so it checks if one of them is current controller's action
|
|
206
|
-
#
|
|
207
|
-
def is_entry_action_active?(entry)
|
|
208
|
-
entry["action"] == params[:action] ||
|
|
209
|
-
entry.fetch("nested_actions", []).include?(params[:action])
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
# Returns true if an entry of given entries is active.
|
|
213
|
-
#
|
|
214
|
-
# @param [Array]
|
|
215
|
-
# Alchemy module navigation entries.
|
|
216
|
-
#
|
|
217
|
-
def has_active_entry?(entries)
|
|
218
|
-
entries.any? { |entry| entry_active?(entry) }
|
|
219
|
-
end
|
|
220
49
|
end
|
|
221
50
|
end
|
|
222
51
|
end
|
|
@@ -58,6 +58,20 @@ module Alchemy
|
|
|
58
58
|
element.ingredient_by_role(role)
|
|
59
59
|
end
|
|
60
60
|
alias_method :ingredient, :ingredient_by_role
|
|
61
|
+
|
|
62
|
+
# The element's visible nested children.
|
|
63
|
+
#
|
|
64
|
+
# Reads them from the page version's shared in-memory element repository
|
|
65
|
+
# instead of the +nested_elements+ association, so rendering a deeply
|
|
66
|
+
# nested element tree does not issue one query per parent.
|
|
67
|
+
#
|
|
68
|
+
# Returns an +Alchemy::ElementsRepository+ so callers can keep chaining
|
|
69
|
+
# scopes (e.g. +nested_elements.named(:slide)+) while still rendering it
|
|
70
|
+
# directly as a collection (+render el.nested_elements+).
|
|
71
|
+
#
|
|
72
|
+
def nested_elements
|
|
73
|
+
element.page_version.element_repository.children_of(element).visible
|
|
74
|
+
end
|
|
61
75
|
end
|
|
62
76
|
|
|
63
77
|
# Block-level helper for element views. Constructs a DOM element wrapping
|
|
@@ -15,6 +15,20 @@ module Alchemy
|
|
|
15
15
|
@resource_window_size ||= "480x#{100 + resource_handler.attributes.length * 40}"
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
# Returns the translated label for the resource dialog title and button text
|
|
19
|
+
# Add a scoped label by adding a "Create <resource-name>" translation key
|
|
20
|
+
# or rely on the generic "Create" translation key with interpolated `%{resource_name}`
|
|
21
|
+
# generated from `resouce_model.model_name.human`.
|
|
22
|
+
def create_resource_label
|
|
23
|
+
Alchemy.t(
|
|
24
|
+
"Create #{resource_name}",
|
|
25
|
+
default: Alchemy.t(
|
|
26
|
+
"Create",
|
|
27
|
+
resource_name: resource_model.model_name.human
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
18
32
|
def resource_instance_variable
|
|
19
33
|
instance_variable_get(:"@#{resource_name}")
|
|
20
34
|
end
|
|
@@ -84,7 +98,7 @@ module Alchemy
|
|
|
84
98
|
else
|
|
85
99
|
options[:time_format] || :"alchemy.time"
|
|
86
100
|
end
|
|
87
|
-
value = l(attribute_value, format: localization_format)
|
|
101
|
+
value = Alchemy.l(attribute_value, format: localization_format)
|
|
88
102
|
elsif attribute[:type] == :boolean
|
|
89
103
|
value = attribute_value ? '<alchemy-icon name="check"></alchemy-icon>'.html_safe : nil
|
|
90
104
|
else
|
|
@@ -1,27 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
RemoteSelect,
|
|
3
|
-
escapeHtml
|
|
4
|
-
} from "alchemy_admin/components/remote_select"
|
|
1
|
+
import { RemoteSelect } from "alchemy_admin/components/remote_select"
|
|
5
2
|
|
|
6
3
|
export class AttachmentSelect extends RemoteSelect {
|
|
7
|
-
|
|
8
|
-
return
|
|
4
|
+
_entry(attachment, term) {
|
|
5
|
+
return {
|
|
6
|
+
icon: attachment.icon_css_class,
|
|
7
|
+
primary: this._hightlightTerm(attachment.name, term)
|
|
8
|
+
}
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* @private
|
|
17
|
-
*/
|
|
18
|
-
_renderListEntry(attachment, term) {
|
|
19
|
-
return `
|
|
20
|
-
<div class="attachment-select--attachment">
|
|
21
|
-
<alchemy-icon name="${escapeHtml(attachment.icon_css_class)}"></alchemy-icon>
|
|
22
|
-
<span class="attachment-select--attachment-name">${this._hightlightTerm(attachment.name, term)}</span>
|
|
23
|
-
</div>
|
|
24
|
-
`
|
|
11
|
+
_selectedEntry(attachment) {
|
|
12
|
+
return {
|
|
13
|
+
icon: attachment.icon_css_class,
|
|
14
|
+
primary: attachment.name
|
|
15
|
+
}
|
|
25
16
|
}
|
|
26
17
|
}
|
|
27
18
|
|
|
@@ -2,13 +2,11 @@
|
|
|
2
2
|
// contained in a form, so that Turbo can submit the form.
|
|
3
3
|
class AutoSubmit extends HTMLElement {
|
|
4
4
|
connectedCallback() {
|
|
5
|
-
|
|
6
|
-
// the event from the original select element.
|
|
7
|
-
$(this).on("change", this.#onChange)
|
|
5
|
+
this.addEventListener("change", this.#onChange)
|
|
8
6
|
}
|
|
9
7
|
|
|
10
8
|
disconnectedCallback() {
|
|
11
|
-
|
|
9
|
+
this.removeEventListener("change", this.#onChange)
|
|
12
10
|
}
|
|
13
11
|
|
|
14
12
|
#onChange = (event) => {
|
|
@@ -4,11 +4,6 @@ class Button extends HTMLButtonElement {
|
|
|
4
4
|
connectedCallback() {
|
|
5
5
|
if (this.form) {
|
|
6
6
|
this.form.addEventListener("submit", this)
|
|
7
|
-
|
|
8
|
-
if (this.form.dataset.remote == "true") {
|
|
9
|
-
this.form.addEventListener("ajax:complete", this)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
7
|
this.form.addEventListener("turbo:submit-end", this)
|
|
13
8
|
} else {
|
|
14
9
|
console.warn("No form for button found!", this)
|
|
@@ -27,7 +22,6 @@ class Button extends HTMLButtonElement {
|
|
|
27
22
|
this.disable()
|
|
28
23
|
}
|
|
29
24
|
break
|
|
30
|
-
case "ajax:complete":
|
|
31
25
|
case "turbo:submit-end":
|
|
32
26
|
this.enable()
|
|
33
27
|
break
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class ClearScheduleButton extends HTMLButtonElement {
|
|
2
|
+
connectedCallback() {
|
|
3
|
+
this.addEventListener("click", this)
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
disconnectedCallback() {
|
|
7
|
+
this.removeEventListener("click", this)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
handleEvent() {
|
|
11
|
+
this.form
|
|
12
|
+
?.querySelectorAll('input[type="datetime-local"]')
|
|
13
|
+
.forEach((input) => {
|
|
14
|
+
input.value = ""
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
customElements.define("alchemy-clear-schedule-button", ClearScheduleButton, {
|
|
20
|
+
extends: "button"
|
|
21
|
+
})
|