alchemy_cms 8.3.11 → 8.4.1
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
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "alchemy/configuration"
|
|
4
|
+
require "alchemy/configurations/admin_components"
|
|
4
5
|
require "alchemy/configurations/dashboard"
|
|
5
6
|
require "alchemy/configurations/default_language"
|
|
6
7
|
require "alchemy/configurations/default_site"
|
|
7
8
|
require "alchemy/configurations/importmap"
|
|
8
9
|
require "alchemy/configurations/format_matchers"
|
|
9
10
|
require "alchemy/configurations/mailer"
|
|
11
|
+
require "alchemy/configurations/menu"
|
|
12
|
+
require "alchemy/configurations/menu_item"
|
|
10
13
|
require "alchemy/configurations/page_cache"
|
|
11
14
|
require "alchemy/configurations/preview"
|
|
12
15
|
require "alchemy/configurations/sitemap"
|
|
@@ -50,6 +53,7 @@ module Alchemy
|
|
|
50
53
|
#
|
|
51
54
|
# show_root [Boolean] # Show language root page in sitemap?
|
|
52
55
|
# show_flag [Boolean] # Enables the Checkbox in Page#update overlay. So your customer can set the visibility of pages in the sitemap.
|
|
56
|
+
# max_age [Integer] # How long the sitemap is cached, in seconds. Set to 0 to disable sitemap caching.
|
|
53
57
|
configuration :sitemap, Sitemap
|
|
54
58
|
|
|
55
59
|
# === Default items per page in admin views
|
|
@@ -81,6 +85,14 @@ module Alchemy
|
|
|
81
85
|
#
|
|
82
86
|
configuration :preview, Preview
|
|
83
87
|
|
|
88
|
+
# === Preview load timeout
|
|
89
|
+
#
|
|
90
|
+
# The number of seconds the admin preview window waits for the preview
|
|
91
|
+
# frame to load before it stops the loading spinner and warns the user
|
|
92
|
+
# that the preview failed to load.
|
|
93
|
+
#
|
|
94
|
+
option :preview_load_timeout, :integer, default: 5
|
|
95
|
+
|
|
84
96
|
# === Picture rendering settings
|
|
85
97
|
#
|
|
86
98
|
# Alchemy uses Dragonfly to render images. Settings for image rendering are specific to elements and are defined in elements.yml
|
|
@@ -177,17 +189,42 @@ module Alchemy
|
|
|
177
189
|
|
|
178
190
|
# === Link Target Options
|
|
179
191
|
#
|
|
180
|
-
# Values for the link target selectbox inside the
|
|
181
|
-
#
|
|
192
|
+
# Values for the link target selectbox inside the link dialog.
|
|
193
|
+
#
|
|
194
|
+
# Each value is stored with a leading underscore, so that it can be used
|
|
195
|
+
# as the link's `target` attribute right away: `blank` is stored as
|
|
196
|
+
# `_blank` and rendered as `target="_blank"`. Views rendering a link
|
|
197
|
+
# themselves should pass the stored value through
|
|
198
|
+
# `Alchemy::Ingredients::LinkTarget#link_target_value`, which also maps
|
|
199
|
+
# the legacy `blank` (stored before the underscore was added) to
|
|
200
|
+
# `_blank`.
|
|
182
201
|
#
|
|
183
202
|
# == Example:
|
|
184
203
|
#
|
|
185
|
-
# Open all links set to overlay
|
|
204
|
+
# Open all links set to overlay in a dialog window in your frontend code,
|
|
205
|
+
# based on the `target="_overlay"` attribute. Such a target has no meaning
|
|
206
|
+
# to the browser, so your code needs to handle the click itself.
|
|
186
207
|
#
|
|
187
|
-
#
|
|
208
|
+
# config.link_target_options = %w[blank overlay]
|
|
188
209
|
#
|
|
189
210
|
option :link_target_options, :collection, item_type: :string, default: %w[blank]
|
|
190
211
|
|
|
212
|
+
# === Allowed url schemes
|
|
213
|
+
#
|
|
214
|
+
# Url schemes editors are allowed to store in menu nodes and linkable
|
|
215
|
+
# ingredients. Any other scheme is rejected, because a scheme like
|
|
216
|
+
# `javascript:` turns an editable url into script execution in your
|
|
217
|
+
# site's own origin.
|
|
218
|
+
#
|
|
219
|
+
# Urls without a scheme (absolute and relative paths, anchors, queries
|
|
220
|
+
# and protocol relative urls) are always allowed.
|
|
221
|
+
#
|
|
222
|
+
# == Example:
|
|
223
|
+
#
|
|
224
|
+
# config.allowed_url_schemes = %w[http https mailto]
|
|
225
|
+
#
|
|
226
|
+
option :allowed_url_schemes, :collection, item_type: :string, default: %w[http https mailto tel ftp]
|
|
227
|
+
|
|
191
228
|
# === Format matchers
|
|
192
229
|
#
|
|
193
230
|
# Named aliases for regular expressions that can be used in various places.
|
|
@@ -291,6 +328,34 @@ module Alchemy
|
|
|
291
328
|
#
|
|
292
329
|
option :admin_importmaps, :collection, collection_class: Set, item_type: :configuration, config_class: Alchemy::Configurations::Importmap, default: []
|
|
293
330
|
|
|
331
|
+
# The entries of the admin main navigation
|
|
332
|
+
#
|
|
333
|
+
# +url+ and +condition+ are evaluated in the view context.
|
|
334
|
+
#
|
|
335
|
+
# Entries without a +condition+ (and without a legacy +controller+ and
|
|
336
|
+
# +action+) are shown to every admin user.
|
|
337
|
+
#
|
|
338
|
+
# == Example
|
|
339
|
+
#
|
|
340
|
+
# Alchemy.configure do |config|
|
|
341
|
+
# config.menu.add(
|
|
342
|
+
# name: "events",
|
|
343
|
+
# title: "Events",
|
|
344
|
+
# icon: "calendar",
|
|
345
|
+
# url: -> { main_app.admin_events_path },
|
|
346
|
+
# condition: -> { can?(:index, :admin_events) },
|
|
347
|
+
# children: [
|
|
348
|
+
# {
|
|
349
|
+
# title: "Locations",
|
|
350
|
+
# url: -> { main_app.admin_locations_path },
|
|
351
|
+
# condition: -> { can?(:index, :admin_locations) }
|
|
352
|
+
# }
|
|
353
|
+
# ]
|
|
354
|
+
# )
|
|
355
|
+
# end
|
|
356
|
+
#
|
|
357
|
+
option :menu, :collection, item_type: :configuration, config_class: Alchemy::Configurations::MenuItem, default: Alchemy::Configurations::Menu::DEFAULT
|
|
358
|
+
|
|
294
359
|
# Additional stylesheets to be included in the Alchemy admin UI
|
|
295
360
|
#
|
|
296
361
|
# == Example
|
|
@@ -468,6 +533,40 @@ module Alchemy
|
|
|
468
533
|
# === Dashboard configuration
|
|
469
534
|
#
|
|
470
535
|
configuration :dashboard, Dashboard
|
|
536
|
+
|
|
537
|
+
# === Admin component registry
|
|
538
|
+
#
|
|
539
|
+
# Overridable admin ViewComponents keyed by role, so extensions can swap
|
|
540
|
+
# a component for their own.
|
|
541
|
+
#
|
|
542
|
+
configuration :admin_components, AdminComponents
|
|
543
|
+
|
|
544
|
+
# === Admin Content Security Policy
|
|
545
|
+
#
|
|
546
|
+
# The class building the Content Security Policy Alchemy sends with its
|
|
547
|
+
# own admin responses. Defaults to +nil+, which sends no policy at all.
|
|
548
|
+
# It will be enabled by default in Alchemy 9.0.
|
|
549
|
+
#
|
|
550
|
+
# It never replaces a policy the host application has configured, and it
|
|
551
|
+
# only applies to Alchemy's own controllers.
|
|
552
|
+
#
|
|
553
|
+
# Alchemy.config.admin_content_security_policy = "Alchemy::Admin::ContentSecurityPolicy"
|
|
554
|
+
#
|
|
555
|
+
option :admin_content_security_policy, :class, default: nil
|
|
556
|
+
|
|
557
|
+
# === Publishable resolver
|
|
558
|
+
#
|
|
559
|
+
# The class used to resolve publication state for Publishable records
|
|
560
|
+
# (currently +Alchemy::Element+ and +Alchemy::PageVersion+).
|
|
561
|
+
#
|
|
562
|
+
# The default resolver uses the +public_on+ / +public_until+ timestamp
|
|
563
|
+
# columns. Apps or gems can provide a custom resolver that implements
|
|
564
|
+
# the same interface — see +Alchemy::Publishable::TimestampResolver+
|
|
565
|
+
# for the interface.
|
|
566
|
+
#
|
|
567
|
+
# == Example
|
|
568
|
+
# Alchemy.config.publishable_resolver = "MyApp::CustomResolver"
|
|
569
|
+
option :publishable_resolver, :class, default: "Alchemy::Publishable::TimestampResolver"
|
|
471
570
|
end
|
|
472
571
|
end
|
|
473
572
|
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
module Configurations
|
|
5
|
+
module Menu
|
|
6
|
+
DEFAULT = [
|
|
7
|
+
{
|
|
8
|
+
name: "dashboard",
|
|
9
|
+
title: "Home",
|
|
10
|
+
position: 1,
|
|
11
|
+
icon: "home-2",
|
|
12
|
+
url: -> { alchemy.admin_dashboard_path },
|
|
13
|
+
condition: -> { can?(:index, :alchemy_admin_dashboard) }
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: "pages",
|
|
17
|
+
title: "modules.pages",
|
|
18
|
+
position: 2,
|
|
19
|
+
icon: "pages",
|
|
20
|
+
url: -> { alchemy.admin_pages_path },
|
|
21
|
+
condition: -> { can?(:index, :alchemy_admin_pages) },
|
|
22
|
+
children: [
|
|
23
|
+
{
|
|
24
|
+
title: "modules.pages",
|
|
25
|
+
url: -> { alchemy.admin_pages_path },
|
|
26
|
+
condition: -> { can?(:index, :alchemy_admin_pages) }
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
title: "modules.layoutpages",
|
|
30
|
+
url: -> { alchemy.admin_layoutpages_path },
|
|
31
|
+
condition: -> { can?(:index, :alchemy_admin_layoutpages) }
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "menus",
|
|
37
|
+
title: "modules.menus",
|
|
38
|
+
position: 3,
|
|
39
|
+
icon: "menu-2",
|
|
40
|
+
url: -> { alchemy.admin_nodes_path },
|
|
41
|
+
condition: -> { can?(:index, :alchemy_admin_nodes) }
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "languages",
|
|
45
|
+
title: "modules.languages",
|
|
46
|
+
position: 4,
|
|
47
|
+
icon: "translate-2",
|
|
48
|
+
icon_style: "none",
|
|
49
|
+
url: -> { alchemy.admin_languages_path },
|
|
50
|
+
condition: -> { can?(:index, :alchemy_admin_languages) }
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "sites",
|
|
54
|
+
title: "modules.sites",
|
|
55
|
+
position: 5,
|
|
56
|
+
icon: "global",
|
|
57
|
+
url: -> { alchemy.admin_sites_path },
|
|
58
|
+
condition: -> { can?(:index, :alchemy_admin_sites) }
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "tags",
|
|
62
|
+
title: "modules.tags",
|
|
63
|
+
position: 6,
|
|
64
|
+
icon: "price-tag-3",
|
|
65
|
+
url: -> { alchemy.admin_tags_path },
|
|
66
|
+
condition: -> { can?(:index, :alchemy_admin_tags) }
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "archive",
|
|
70
|
+
title: "modules.library",
|
|
71
|
+
position: 7,
|
|
72
|
+
icon: "archive-drawer",
|
|
73
|
+
url: -> { alchemy.admin_pictures_path },
|
|
74
|
+
condition: -> { can?(:index, :alchemy_admin_pictures) },
|
|
75
|
+
children: [
|
|
76
|
+
{
|
|
77
|
+
title: "modules.pictures",
|
|
78
|
+
url: -> { alchemy.admin_pictures_path },
|
|
79
|
+
condition: -> { can?(:index, :alchemy_admin_pictures) }
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
title: "modules.attachments",
|
|
83
|
+
url: -> { alchemy.admin_attachments_path },
|
|
84
|
+
condition: -> { can?(:index, :alchemy_admin_attachments) }
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
].freeze
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Alchemy
|
|
4
|
+
module Configurations
|
|
5
|
+
class MenuItem < Alchemy::Configuration
|
|
6
|
+
option :name, :string
|
|
7
|
+
option :title, :string
|
|
8
|
+
option :engine_name, :string
|
|
9
|
+
option :position, :number
|
|
10
|
+
option :icon, :string
|
|
11
|
+
option :icon_style, :string
|
|
12
|
+
option :image, :string
|
|
13
|
+
option :inline_image, :string
|
|
14
|
+
option :turbo, :boolean, default: true
|
|
15
|
+
option :url, :proc
|
|
16
|
+
option :condition, :proc
|
|
17
|
+
option :match_path, :regexp
|
|
18
|
+
option :children, :collection, item_type: :configuration, config_class: MenuItem
|
|
19
|
+
|
|
20
|
+
# Legacy options of entries registered with +Alchemy::Modules.register_module+
|
|
21
|
+
option :controller, :string
|
|
22
|
+
option :action, :string
|
|
23
|
+
option :nested_controllers, :collection, item_type: :string
|
|
24
|
+
option :nested_actions, :collection, item_type: :string
|
|
25
|
+
option :nested, :collection, item_type: :configuration, config_class: MenuItem
|
|
26
|
+
|
|
27
|
+
def self.from_legacy_hash(hash)
|
|
28
|
+
hash = hash.to_h.deep_stringify_keys
|
|
29
|
+
new(
|
|
30
|
+
from_legacy_navigation(hash["navigation"] || {}, engine_name: hash["engine_name"]).merge(
|
|
31
|
+
"name" => hash["name"],
|
|
32
|
+
"position" => hash["position"]
|
|
33
|
+
).compact
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.from_legacy_navigation(navigation, engine_name:)
|
|
38
|
+
{
|
|
39
|
+
"title" => navigation["name"],
|
|
40
|
+
"engine_name" => engine_name,
|
|
41
|
+
"controller" => navigation["controller"],
|
|
42
|
+
"action" => navigation["action"],
|
|
43
|
+
"url" => navigation["url"],
|
|
44
|
+
"condition" => navigation["condition"],
|
|
45
|
+
"icon" => navigation["icon"],
|
|
46
|
+
"icon_style" => navigation["icon-style"],
|
|
47
|
+
"image" => navigation["image"],
|
|
48
|
+
"inline_image" => navigation["inline_image"],
|
|
49
|
+
"turbo" => navigation.dig("data", "turbo").to_s != "false",
|
|
50
|
+
"nested_controllers" => navigation["nested_controllers"],
|
|
51
|
+
"nested_actions" => navigation["nested_actions"],
|
|
52
|
+
"children" => navigation["sub_navigation"]&.map { |entry| new(from_legacy_navigation(entry, engine_name:)) },
|
|
53
|
+
"nested" => navigation["nested"]&.map { |entry| new(from_legacy_navigation(entry, engine_name:)) }
|
|
54
|
+
}.compact
|
|
55
|
+
end
|
|
56
|
+
private_class_method :from_legacy_navigation
|
|
57
|
+
|
|
58
|
+
def path(view_context)
|
|
59
|
+
if url
|
|
60
|
+
view_context.instance_exec(&url)
|
|
61
|
+
elsif controller
|
|
62
|
+
routes = engine_name.present? ? view_context.public_send(engine_name) : view_context.main_app
|
|
63
|
+
routes.url_for(controller:, action:, only_path: true)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def visible?(view_context)
|
|
68
|
+
if condition
|
|
69
|
+
view_context.instance_exec(&condition)
|
|
70
|
+
elsif controller
|
|
71
|
+
view_context.can?(action.to_sym, controller.delete_prefix("/").tr("/", "_").to_sym)
|
|
72
|
+
else
|
|
73
|
+
true
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def active?(view_context)
|
|
78
|
+
current?(view_context) || children.any? { |child| child.active?(view_context) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def current?(view_context)
|
|
82
|
+
current_path = view_context.request.path
|
|
83
|
+
path_matches = if match_path
|
|
84
|
+
match_path.match?(current_path)
|
|
85
|
+
elsif (own_path = local_path(view_context))
|
|
86
|
+
current_path == own_path || current_path.start_with?("#{own_path.chomp("/")}/")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
path_matches || [self, *nested].any? do |entry|
|
|
90
|
+
entry.matches_controller?(controller: view_context.params[:controller], action: view_context.params[:action])
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def matches_controller?(controller:, action:)
|
|
95
|
+
return false unless self.controller
|
|
96
|
+
|
|
97
|
+
controller = controller.to_s.delete_prefix("/")
|
|
98
|
+
action = action.to_s
|
|
99
|
+
(self.controller.delete_prefix("/") == controller || nested_controllers.include?(controller)) &&
|
|
100
|
+
(self.action == action || nested_actions.include?(action))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def validate_controller_existence!
|
|
104
|
+
[controller, *children.map(&:controller)].compact.each do |controller|
|
|
105
|
+
controller_const_name = "#{controller.camelize}Controller"
|
|
106
|
+
controller_const_name.constantize
|
|
107
|
+
rescue NameError
|
|
108
|
+
raise "Error in AlchemyCMS module definition: '#{name}'. Could not find the " \
|
|
109
|
+
"matching controller class #{controller_const_name} for the specified controller: '#{controller}'"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def local_path(view_context)
|
|
116
|
+
uri = URI.parse(path(view_context).to_s)
|
|
117
|
+
uri.path.presence if uri.host.nil? || uri.host == view_context.request.host
|
|
118
|
+
rescue URI::InvalidURIError
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -4,7 +4,21 @@ module Alchemy
|
|
|
4
4
|
module Configurations
|
|
5
5
|
class Uploader < Alchemy::Configuration
|
|
6
6
|
class AllowedFileTypes < Alchemy::Configuration
|
|
7
|
-
|
|
7
|
+
# Document, media and archive formats Alchemy knows how to display.
|
|
8
|
+
# Executables and other active content are deliberately absent.
|
|
9
|
+
DEFAULT_ATTACHMENT_FILE_TYPES = %w[
|
|
10
|
+
pdf
|
|
11
|
+
doc docx odt rtf txt
|
|
12
|
+
xls xlsx ods csv
|
|
13
|
+
ppt pptx odp
|
|
14
|
+
vcf
|
|
15
|
+
zip
|
|
16
|
+
avif gif jpeg jpg png psd svg tif tiff webp
|
|
17
|
+
aac flac m4a mp3 oga ogg wav weba
|
|
18
|
+
avi flv m4v mov mp4 mpeg mpg ogv webm wmv
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
option :alchemy_attachments, :collection, item_type: :string, default: DEFAULT_ATTACHMENT_FILE_TYPES
|
|
8
22
|
option :alchemy_pictures, :collection, item_type: :string, default: %w[jpg jpeg gif png svg webp]
|
|
9
23
|
|
|
10
24
|
def set(configuration_hash)
|
data/lib/alchemy/engine.rb
CHANGED
|
@@ -159,12 +159,6 @@ module Alchemy
|
|
|
159
159
|
Alchemy.config.user_class.stampable(stamper_class_name: Alchemy.config.user_class_name)
|
|
160
160
|
end
|
|
161
161
|
end
|
|
162
|
-
|
|
163
|
-
if defined?(RailsLiveReload) && Rails.env.development?
|
|
164
|
-
require "alchemy/dev_support/live_reload_watcher"
|
|
165
|
-
|
|
166
|
-
Alchemy::LiveReloadWatcher.init
|
|
167
|
-
end
|
|
168
162
|
end
|
|
169
163
|
|
|
170
164
|
initializer "alchemy.webp-mime_type" do |app|
|
data/lib/alchemy/i18n.rb
CHANGED
|
@@ -15,6 +15,20 @@ module Alchemy
|
|
|
15
15
|
def t(msg, **kwargs)
|
|
16
16
|
Alchemy::I18n.translate(msg, **kwargs)
|
|
17
17
|
end
|
|
18
|
+
|
|
19
|
+
# Alchemy shortcut localization method
|
|
20
|
+
#
|
|
21
|
+
# Instead of having to call:
|
|
22
|
+
#
|
|
23
|
+
# Alchemy::I18n.localize(datetime)
|
|
24
|
+
#
|
|
25
|
+
# You can use this shortcut method:
|
|
26
|
+
#
|
|
27
|
+
# Alchemy.l(datetime)
|
|
28
|
+
#
|
|
29
|
+
def l(datetime, **kwargs)
|
|
30
|
+
Alchemy::I18n.localize(datetime, **kwargs)
|
|
31
|
+
end
|
|
18
32
|
end
|
|
19
33
|
|
|
20
34
|
module I18n
|
|
@@ -53,6 +67,18 @@ module Alchemy
|
|
|
53
67
|
::I18n.t(msg, **options.merge(scope: scope))
|
|
54
68
|
end
|
|
55
69
|
|
|
70
|
+
# Returns a localized time string for the given time and format.
|
|
71
|
+
#
|
|
72
|
+
# @param datetime [Time|Date|nil]
|
|
73
|
+
# @param format [Symbol] (:"alchemy.default")
|
|
74
|
+
#
|
|
75
|
+
# @return [String|nil]
|
|
76
|
+
def localize(datetime, format: :"alchemy.default")
|
|
77
|
+
return nil if datetime.nil?
|
|
78
|
+
|
|
79
|
+
::I18n.l(datetime, format:)
|
|
80
|
+
end
|
|
81
|
+
|
|
56
82
|
def available_locales
|
|
57
83
|
Rails.application.config.i18n.available_locales || begin
|
|
58
84
|
@@available_locales ||= nil
|
data/lib/alchemy/modules.rb
CHANGED
|
@@ -2,10 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
module Alchemy
|
|
4
4
|
module Modules
|
|
5
|
-
mattr_accessor :alchemy_modules
|
|
6
|
-
|
|
7
|
-
@@alchemy_modules = YAML.load_file(File.expand_path("../../config/alchemy/modules.yml", __dir__))
|
|
8
|
-
|
|
9
5
|
class << self
|
|
10
6
|
def included(base)
|
|
11
7
|
base.send :helper_method, :alchemy_modules, :module_definition_for
|
|
@@ -25,68 +21,74 @@ module Alchemy
|
|
|
25
21
|
# }
|
|
26
22
|
#
|
|
27
23
|
def register_module(module_definition)
|
|
28
|
-
|
|
24
|
+
menu_item = Alchemy::Configurations::MenuItem.from_legacy_hash(module_definition)
|
|
25
|
+
return if menu_item.name.present? && Alchemy.config.menu.any? { |item| item.name == menu_item.name }
|
|
26
|
+
|
|
27
|
+
Alchemy.config.menu.add(menu_item)
|
|
28
|
+
end
|
|
29
|
+
deprecate register_module: "Use `Alchemy.config.menu.add` instead", deprecator: Alchemy::Deprecation
|
|
30
|
+
|
|
31
|
+
def alchemy_modules
|
|
32
|
+
Alchemy.config.menu.map { |menu_item| to_legacy_hash(menu_item) }
|
|
33
|
+
end
|
|
34
|
+
deprecate alchemy_modules: "Use `Alchemy.config.menu` instead", deprecator: Alchemy::Deprecation
|
|
35
|
+
|
|
36
|
+
def to_legacy_hash(menu_item)
|
|
37
|
+
{
|
|
38
|
+
"name" => menu_item.name,
|
|
39
|
+
"engine_name" => menu_item.engine_name,
|
|
40
|
+
"position" => menu_item.position,
|
|
41
|
+
"navigation" => to_legacy_navigation(menu_item)
|
|
42
|
+
}.compact
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
29
46
|
|
|
30
|
-
|
|
47
|
+
def to_legacy_navigation(menu_item)
|
|
48
|
+
{
|
|
49
|
+
"name" => menu_item.title,
|
|
50
|
+
"controller" => menu_item.controller,
|
|
51
|
+
"action" => menu_item.action,
|
|
52
|
+
"url" => menu_item.url,
|
|
53
|
+
"condition" => menu_item.condition,
|
|
54
|
+
"icon" => menu_item.icon,
|
|
55
|
+
"icon-style" => menu_item.icon_style,
|
|
56
|
+
"image" => menu_item.image,
|
|
57
|
+
"inline_image" => menu_item.inline_image,
|
|
58
|
+
"data" => menu_item.turbo ? nil : {turbo: false},
|
|
59
|
+
"nested_controllers" => menu_item.nested_controllers.to_a.presence,
|
|
60
|
+
"nested_actions" => menu_item.nested_actions.to_a.presence,
|
|
61
|
+
"sub_navigation" => menu_item.children.map { |child| to_legacy_navigation(child) }.presence,
|
|
62
|
+
"nested" => menu_item.nested.map { |entry| to_legacy_navigation(entry) }.presence
|
|
63
|
+
}.compact
|
|
31
64
|
end
|
|
32
65
|
end
|
|
33
66
|
|
|
67
|
+
def alchemy_modules
|
|
68
|
+
Alchemy::Modules.alchemy_modules
|
|
69
|
+
end
|
|
70
|
+
|
|
34
71
|
# Get the module definition for given module name
|
|
35
72
|
#
|
|
36
73
|
# You can also pass a hash of an module definition.
|
|
37
74
|
# It then tries to find the module defintion from controller name and action name
|
|
38
75
|
#
|
|
39
76
|
def module_definition_for(name_or_params)
|
|
40
|
-
case name_or_params
|
|
77
|
+
menu_item = case name_or_params
|
|
41
78
|
when String
|
|
42
|
-
|
|
79
|
+
Alchemy.config.menu.detect { |item| item.name == name_or_params }
|
|
43
80
|
when Hash
|
|
44
|
-
name_or_params.stringify_keys
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
definition_from_nested(module_navi, name_or_params)
|
|
81
|
+
params = name_or_params.stringify_keys
|
|
82
|
+
Alchemy.config.menu.detect do |item|
|
|
83
|
+
[item, *item.children, *item.nested].any? do |entry|
|
|
84
|
+
entry.matches_controller?(controller: params["controller"], action: params["action"])
|
|
85
|
+
end
|
|
50
86
|
end
|
|
51
87
|
else
|
|
52
88
|
raise ArgumentError, "Could not find module definition for #{name_or_params}"
|
|
53
89
|
end
|
|
90
|
+
Alchemy::Modules.to_legacy_hash(menu_item) if menu_item
|
|
54
91
|
end
|
|
55
|
-
|
|
56
|
-
private
|
|
57
|
-
|
|
58
|
-
def definition_from_mainnavi(module_navi, params)
|
|
59
|
-
controller_matches?(module_navi, params) && action_matches?(module_navi, params)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def definition_from_subnavi(module_navi, params)
|
|
63
|
-
subnavi = module_navi["sub_navigation"]
|
|
64
|
-
return if subnavi.nil?
|
|
65
|
-
|
|
66
|
-
subnavi.any? do |navi|
|
|
67
|
-
controller_matches?(navi, params) && action_matches?(navi, params)
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def definition_from_nested(module_navi, params)
|
|
72
|
-
nested = module_navi["nested"]
|
|
73
|
-
return if nested.nil?
|
|
74
|
-
|
|
75
|
-
nested.any? do |navi|
|
|
76
|
-
controller_matches?(navi, params) && action_matches?(navi, params)
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def controller_matches?(navi, params)
|
|
81
|
-
remove_slash(navi["controller"]) == remove_slash(params["controller"])
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def action_matches?(navi, params)
|
|
85
|
-
navi["action"] == params["action"]
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def remove_slash(str)
|
|
89
|
-
str.gsub(/^\//, "")
|
|
90
|
-
end
|
|
92
|
+
deprecate module_definition_for: "Use `Alchemy.config.menu` instead", deprecator: Alchemy::Deprecation
|
|
91
93
|
end
|
|
92
94
|
end
|