From bb34c7e8cbfba0343dd96559043b713e37ab49a8 Mon Sep 17 00:00:00 2001 From: Anita Date: Fri, 15 Jul 2022 14:13:51 +0800 Subject: [PATCH 01/74] Tested with Rails 6.1 Ruby 3.1.2 core: TranslationHelper, SiteBarHelper - using Ruby3 keyword args lib/refinery/engine uses new mobility configuration images, resources: Updated examples to pass an explicit block to 'expect' as passing an implicit block is deprecated. images: Use errors.add to add errors to an object - had already been done for resources. Gem versions changed: mobility 1.2.9 rspec-rails 6.0.0.rc1 friendly_id >= 5.4.0 friendly_id-mobility ~> 1.0.3 --- .gitignore | 1 + Gemfile | 14 ++++-- core/app/helpers/refinery/site_bar_helper.rb | 8 ++-- .../helpers/refinery/translation_helper.rb | 2 +- core/app/views/refinery/_site_bar.html.erb | 7 +-- .../refinery/admin/_error_messages.html.erb | 8 ++-- core/lib/refinery/core/engine.rb | 24 ++++++++-- core/lib/refinery/version.rb | 2 +- .../refinery/translation_helper_spec.rb | 5 +- .../translated_field_presenter_spec.rb | 6 +-- .../system/refinery/admin/xhr_paging_spec.rb | 4 +- core/spec/system/refinery/site_bar_spec.rb | 2 +- .../images/validators/image_size_validator.rb | 15 +++--- .../validators/image_update_validator.rb | 2 +- .../support/shared_examples/image_deleter.rb | 17 ++++--- .../support/shared_examples/image_editor.rb | 4 +- .../support/shared_examples/image_indexer.rb | 7 ++- .../support/shared_examples/image_inserter.rb | 2 +- .../shared_examples/image_previewer.rb | 2 +- .../shared_examples/image_translator.rb | 2 +- .../support/shared_examples/image_uploader.rb | 20 ++++---- .../spec/system/refinery/admin/images_spec.rb | 48 +++++++++---------- pages/lib/refinery/pages/finder.rb | 2 +- pages/lib/refinery/pages/instance_methods.rb | 2 +- pages/refinerycms-pages.gemspec | 4 +- refinerycms.gemspec | 2 + .../validators/file_size_validator.rb | 7 ++- .../spec/models/refinery/resource_spec.rb | 3 +- .../system/refinery/admin/resources_spec.rb | 20 ++++---- spec/spec_helper.rb | 1 + testing/refinerycms-testing.gemspec | 2 +- 31 files changed, 131 insertions(+), 114 deletions(-) diff --git a/.gitignore b/.gitignore index d9073f40c3c..b35ceabdd93 100644 --- a/.gitignore +++ b/.gitignore @@ -97,3 +97,4 @@ Gemfile.lock # rspec failures .rspec_failures +Refinery Review.md diff --git a/Gemfile b/Gemfile index 17e0baf8dd4..02dda907921 100644 --- a/Gemfile +++ b/Gemfile @@ -1,15 +1,23 @@ source 'https://rubygems.org' +ruby "3.1.2" gemspec +gem 'rails', "~>6.1" +gem 'net-smtp', require: false +gem 'net-imap', require: false +gem 'net-pop', require: false + + path "./" do gem "refinerycms-core" + gem "refinerycms-dragonfly" gem "refinerycms-images" gem "refinerycms-pages" gem "refinerycms-resources" end -gem 'refinerycms-i18n', github: 'refinery/refinerycms-i18n', branch: 'master' +gem 'refinerycms-i18n', path: '/private/var/www/refinery/refinerycms-i18n' # Add support for refinerycms-acts-as-indexed gem 'refinerycms-acts-as-indexed', ['~> 4.0', '>= 4.0.0'], @@ -43,6 +51,7 @@ group :development, :test do gem 'activejob' gem 'bootsnap', require: false gem 'listen', '~> 3.0' + gem 'rspec-rails', '~> 6.0.0.rc1' end group :test do @@ -53,9 +62,6 @@ group :test do gem 'rspec-retry' gem 'falcon' gem 'falcon-capybara' - - # TODO: Use beta source for Rails 6 support - gem 'rspec-rails', '~> 4.0.0.beta3' end # Load local gems according to Refinery developer preference. diff --git a/core/app/helpers/refinery/site_bar_helper.rb b/core/app/helpers/refinery/site_bar_helper.rb index 6d6519957e8..96d03390779 100644 --- a/core/app/helpers/refinery/site_bar_helper.rb +++ b/core/app/helpers/refinery/site_bar_helper.rb @@ -3,24 +3,24 @@ module SiteBarHelper # Generates the link to determine where the site bar switch button returns to. def site_bar_switch_link - link_to_if(admin?, t('.switch_to_your_website', site_bar_translate_locale_args), + link_to_if(admin?, t('.switch_to_your_website', **site_bar_translate_locale_args), refinery.root_path(site_bar_translate_locale_args), 'data-turbolinks' => false) do - link_to t('.switch_to_your_website_editor', site_bar_translate_locale_args), + link_to t('.switch_to_your_website_editor', **site_bar_translate_locale_args), Refinery::Core.backend_path, 'data-turbolinks' => false end end def site_bar_edit_link return nil if admin? || @page.nil? - link_to t('refinery.admin.pages.edit', site_bar_translate_locale_args), + link_to t('refinery.admin.pages.edit', **site_bar_translate_locale_args), refinery.admin_edit_page_path(@page.nested_url, :switch_locale => (@page.translations.first.locale unless @page.translated_to_default_locale?)), 'data-turbolinks' => false end def site_bar_translate_locale_args - { :locale => Refinery::I18n.current_locale } + { locale: Refinery::I18n.current_locale } end def display_site_bar? diff --git a/core/app/helpers/refinery/translation_helper.rb b/core/app/helpers/refinery/translation_helper.rb index f3d36f3cbbe..d211543bb75 100644 --- a/core/app/helpers/refinery/translation_helper.rb +++ b/core/app/helpers/refinery/translation_helper.rb @@ -3,7 +3,7 @@ module TranslationHelper # Overrides Rails' core I18n.t() function to produce a more helpful error message. # The default one wreaks havoc with CSS and makes it hard to understand the problem. - def t(key, options = {}) + def t(key, **options) if (val = super) =~ /class.+?translation_missing/ val = val.to_s.gsub(/]*>/, 'i18n: ').gsub('', '').gsub(', ', '.') end diff --git a/core/app/views/refinery/_site_bar.html.erb b/core/app/views/refinery/_site_bar.html.erb index e5fcb7993a8..db909de8599 100644 --- a/core/app/views/refinery/_site_bar.html.erb +++ b/core/app/views/refinery/_site_bar.html.erb @@ -17,12 +17,13 @@
- <%= Refinery::Core.site_name %> + <% Rails.logger.debug Refinery::Core.site_name %> + <%#= Refinery::Core.site_name %> - <%= link_to t('.log_out', site_bar_translate_locale_args), + <%= link_to t('.log_out', **site_bar_translate_locale_args), ::Refinery::Core.refinery_logout_path, - :id => 'logout' if ::Refinery::Core.refinery_logout_path.present? %> + id: 'logout' if ::Refinery::Core.refinery_logout_path.present? %>
diff --git a/core/app/views/refinery/admin/_error_messages.html.erb b/core/app/views/refinery/admin/_error_messages.html.erb index febb6a6dab9..41274deb266 100644 --- a/core/app/views/refinery/admin/_error_messages.html.erb +++ b/core/app/views/refinery/admin/_error_messages.html.erb @@ -3,12 +3,12 @@

<%= t('.problems_in_following_fields') %>:

diff --git a/core/lib/refinery/core/engine.rb b/core/lib/refinery/core/engine.rb index ae0b04c84bb..5eb025e1aba 100644 --- a/core/lib/refinery/core/engine.rb +++ b/core/lib/refinery/core/engine.rb @@ -45,11 +45,25 @@ def refinery_inclusion! end initializer "refinery.mobility" do - Mobility.configure do |config| - config.default_backend = :table - config.accessor_method = :translates - config.query_method = :i18n - config.default_options[:dirty] = true + Mobility.configure do + plugins do + backend :table + reader # Explicitly declare readers, + writer # writers, and + backend_reader # backend reader (post.title_backend, etc). + + active_record # You must now also explicitly ask for ActiveRecord (or Sequel) + + query # i18n is the default scope + cache # previously implicit + fallbacks + presence # previously implicit + default + attribute_methods # uncomment this to get methods like `translated_attributes` + dirty + end + # accessor_method not available in v1.0 + # config.accessor_method = :translates #.. not sure if this is covered in the above plugins (AMG. 15/7/22). end end diff --git a/core/lib/refinery/version.rb b/core/lib/refinery/version.rb index 3a6f11e49f8..990c0c0e0cd 100644 --- a/core/lib/refinery/version.rb +++ b/core/lib/refinery/version.rb @@ -13,7 +13,7 @@ def to_s end def required_ruby_version - '>= 2.5.5' + '>= 3.1' end end end diff --git a/core/spec/helpers/refinery/translation_helper_spec.rb b/core/spec/helpers/refinery/translation_helper_spec.rb index 10bbde9634b..5339d9be458 100644 --- a/core/spec/helpers/refinery/translation_helper_spec.rb +++ b/core/spec/helpers/refinery/translation_helper_spec.rb @@ -15,8 +15,9 @@ module Refinery before do Mobility.with_locale(:en) do - page.title = "draft" - page.save! + # page.title = "draft" + # page.save! + page.update!({title: 'draft'}) end Mobility.with_locale(:lv) do diff --git a/core/spec/presenters/refinery/translated_field_presenter_spec.rb b/core/spec/presenters/refinery/translated_field_presenter_spec.rb index bad639bee28..85d80cb353e 100644 --- a/core/spec/presenters/refinery/translated_field_presenter_spec.rb +++ b/core/spec/presenters/refinery/translated_field_presenter_spec.rb @@ -6,13 +6,11 @@ module Refinery before do Mobility.with_locale(:en) do - page.title = "draft" - page.save! + page.update!({ title: "draft" }) end Mobility.with_locale(:lv) do - page.title = "melnraksts" - page.save! + page.update!({title: "melnraksts"}) end end diff --git a/core/spec/system/refinery/admin/xhr_paging_spec.rb b/core/spec/system/refinery/admin/xhr_paging_spec.rb index c1272f48f11..a60996c3290 100644 --- a/core/spec/system/refinery/admin/xhr_paging_spec.rb +++ b/core/spec/system/refinery/admin/xhr_paging_spec.rb @@ -8,8 +8,8 @@ module Refinery # Refinery::Admin::ImagesController specifies :order => 'created_at DESC' in crudify let(:first_image) { Image.order('created_at DESC').first } let(:last_image) { Image.order('created_at DESC').last } - let!(:image_1) { FactoryBot.create :image } - let!(:image_2) { FactoryBot.create :image } + let!(:image_1) { FactoryBot.create(:image) } + let!(:image_2) { FactoryBot.create(:image) } before do allow(Image).to receive(:per_page).and_return(1) diff --git a/core/spec/system/refinery/site_bar_spec.rb b/core/spec/system/refinery/site_bar_spec.rb index 7c5add0ecc9..0d05f6ec00e 100644 --- a/core/spec/system/refinery/site_bar_spec.rb +++ b/core/spec/system/refinery/site_bar_spec.rb @@ -10,10 +10,10 @@ module Refinery context "when set" do before do allow(Refinery::Core).to receive(:refinery_logout_path).and_return(logout_path) - visit Refinery::Core.backend_path end it "is present" do + visit Refinery::Core.backend_path expect(page).to have_selector("a[href='#{logout_path}']") expect(page).to have_content("Log out") end diff --git a/images/lib/refinery/images/validators/image_size_validator.rb b/images/lib/refinery/images/validators/image_size_validator.rb index ecfdd589cca..e6aaecee7b8 100644 --- a/images/lib/refinery/images/validators/image_size_validator.rb +++ b/images/lib/refinery/images/validators/image_size_validator.rb @@ -2,17 +2,16 @@ module Refinery module Images module Validators class ImageSizeValidator < ActiveModel::Validator - def validate(record) - image = record.image - - if image.respond_to?(:length) && image.length > Images.max_image_size - record.errors[:image] << ::I18n.t('too_big', - :scope => 'activerecord.errors.models.refinery/image', - :size => Images.max_image_size) - end + record.errors.add(:image, ::I18n.t('too_big', + scope: 'activerecord.errors.models.refinery/image', + size: Images.max_image_size)) if too_big(record.image) end + private + def too_big(image) + image.respond_to?(:length) && image.length > Images.max_image_size + end end end end diff --git a/images/lib/refinery/images/validators/image_update_validator.rb b/images/lib/refinery/images/validators/image_update_validator.rb index 0ab9fbb7248..a2769bf2fde 100644 --- a/images/lib/refinery/images/validators/image_update_validator.rb +++ b/images/lib/refinery/images/validators/image_update_validator.rb @@ -7,7 +7,7 @@ def validate(record) if record.image_name_changed? record.errors.add :image_name, ::I18n.t("different_file_name", - :scope => "activerecord.errors.models.refinery/image") + scope: "activerecord.errors.models.refinery/image") end end diff --git a/images/spec/support/shared_examples/image_deleter.rb b/images/spec/support/shared_examples/image_deleter.rb index a8ff8cf90b1..6c04e4f8de6 100644 --- a/images/spec/support/shared_examples/image_deleter.rb +++ b/images/spec/support/shared_examples/image_deleter.rb @@ -1,27 +1,26 @@ -shared_examples_for 'deletes an image' do +shared_examples_for 'Delete' do before do raise "please set let(:initial_path)" if initial_path.blank? ensure_on(initial_path) end - let(:image_count) {[Refinery::Image.count, Refinery::Images.pages_per_admin_index].min} - let(:deleting_an_image) { - -> { - first("#records li").click_link(::I18n.t('delete', scope: 'refinery.admin.images')) - } - } + def deleting_an_image + first("#records li").click_link(::I18n.t('delete', scope: 'refinery.admin.images')) + end + + let(:image_count) { [Refinery::Image.count, Refinery::Images.pages_per_admin_index].min } it 'has a delete image link for each image' do expect(page).to have_selector('#records.images li a.confirm-delete', count: image_count) end it "removes an image" do - expect(deleting_an_image).to change(Refinery::Image, :count).by(-1) + expect { deleting_an_image }.to change(Refinery::Image, :count).by(-1) end it 'says the image has been removed' do image_title = page.has_selector?('#image_grid') ? find("#image_grid li:first").first("img")[:title] : - find("#image_list li:first").first("span.title").text + find("#image_list li:first").first("span.title").text expect(image_title).to be_present diff --git a/images/spec/support/shared_examples/image_editor.rb b/images/spec/support/shared_examples/image_editor.rb index 9a643a06cd7..6e272ccd4ba 100644 --- a/images/spec/support/shared_examples/image_editor.rb +++ b/images/spec/support/shared_examples/image_editor.rb @@ -1,3 +1,3 @@ -shared_examples 'edits an image' do +shared_examples 'Edit' do pending -end \ No newline at end of file +end diff --git a/images/spec/support/shared_examples/image_indexer.rb b/images/spec/support/shared_examples/image_indexer.rb index 0dc60b06e63..216dcbf1914 100644 --- a/images/spec/support/shared_examples/image_indexer.rb +++ b/images/spec/support/shared_examples/image_indexer.rb @@ -1,4 +1,4 @@ -shared_examples_for 'indexes images' do +shared_examples_for 'Index' do let(:image_count) {[Refinery::Image.count, Refinery::Images.pages_per_admin_index].min} @@ -20,7 +20,7 @@ end # image index -shared_examples_for 'shows list and grid views' do +shared_examples_for 'Index Views' do let(:image_count) {[Refinery::Image.count, Refinery::Images.pages_per_admin_index].min} @@ -70,11 +70,10 @@ expect(page).to have_content(::I18n.t('switch_to', view_name: 'grid', scope: 'refinery.admin.images.index.view')) end - end # list view end -shared_examples_for 'paginates the list of images' do +shared_examples_for 'Index Pagination' do let(:image_count) {[Refinery::Image.count, Refinery::Images.pages_per_admin_index].min} diff --git a/images/spec/support/shared_examples/image_inserter.rb b/images/spec/support/shared_examples/image_inserter.rb index 60d6e002ba6..d4e22c6aec9 100644 --- a/images/spec/support/shared_examples/image_inserter.rb +++ b/images/spec/support/shared_examples/image_inserter.rb @@ -1,5 +1,5 @@ -shared_examples 'inserts images' do +shared_examples 'Insert' do before do raise "please set let(:initial_path)" if initial_path.blank? ensure_on(initial_path) diff --git a/images/spec/support/shared_examples/image_previewer.rb b/images/spec/support/shared_examples/image_previewer.rb index 9fe47e8ebc7..2cf3a65e9fe 100644 --- a/images/spec/support/shared_examples/image_previewer.rb +++ b/images/spec/support/shared_examples/image_previewer.rb @@ -9,7 +9,7 @@ def preview_image preview_window.close end -shared_examples 'shows an image preview' do +shared_examples 'Preview' do before do raise "please set let(:initial_path)" if initial_path.blank? ensure_on(initial_path) diff --git a/images/spec/support/shared_examples/image_translator.rb b/images/spec/support/shared_examples/image_translator.rb index 8341544fd56..4ab18c40494 100644 --- a/images/spec/support/shared_examples/image_translator.rb +++ b/images/spec/support/shared_examples/image_translator.rb @@ -1,4 +1,4 @@ -shared_examples 'translates an image' do +shared_examples 'Translate' do before do allow(Refinery::I18n).to receive(:frontend_locales).and_return([:en, :fr]) ensure_on(initial_path) diff --git a/images/spec/support/shared_examples/image_uploader.rb b/images/spec/support/shared_examples/image_uploader.rb index a55f6d45832..b752babc2cb 100644 --- a/images/spec/support/shared_examples/image_uploader.rb +++ b/images/spec/support/shared_examples/image_uploader.rb @@ -1,34 +1,32 @@ -shared_examples 'uploads images' do +shared_examples 'Upload' do before do raise "please set let(:initial_path)" if initial_path.blank? ensure_on(initial_path) initialize_context end - let(:uploading_an_image) { - ->{ + def uploading_an_image open_upload_dialog page.within_frame(dialog_frame_id) do select_upload attach_file 'image_image', image_path - fill_in 'image_image_title', with: 'Image With Dashes' - fill_in 'image_image_alt', with: "Alt description for image" + fill_in 'image_image_title', with: 'Image With Dashes' + fill_in 'image_image_alt', with: "Alt description for image" click_button ::I18n.t('save', scope: 'refinery.admin.form_actions') end - } - } + end context 'when the image type is acceptable' do - let(:image_path) {Refinery.roots('refinery/images').join("spec/fixtures/image-with-dashes.jpg")} + let(:image_path) { Refinery.roots('refinery/images').join("spec/fixtures/image-with-dashes.jpg") } it 'the image is uploaded', :js => true do - expect(uploading_an_image).to change(Refinery::Image, :count).by(1) + expect { uploading_an_image }.to change(Refinery::Image, :count).by(1) end end context 'when the image type is not acceptable' do - let(:image_path) {Refinery.roots('refinery/images').join("spec/fixtures/cape-town-tide-table.pdf")} + let(:image_path) { Refinery.roots('refinery/images').join("spec/fixtures/cape-town-tide-table.pdf") } it 'the image is rejected', :js => true do - expect(uploading_an_image).to_not change(Refinery::Image, :count) + expect { uploading_an_image }.to_not change(Refinery::Image, :count) page.within_frame(dialog_frame_id) do expect(page).to have_content(::I18n.t('incorrect_format', :scope => 'activerecord.errors.models.refinery/image', diff --git a/images/spec/system/refinery/admin/images_spec.rb b/images/spec/system/refinery/admin/images_spec.rb index 1dc1b269452..6afa06bd673 100644 --- a/images/spec/system/refinery/admin/images_spec.rb +++ b/images/spec/system/refinery/admin/images_spec.rb @@ -13,31 +13,31 @@ module Refinery expect(page).to have_content(::I18n.t('no_images_yet', scope: 'refinery.admin.images.records')) end - it_has_behaviour 'uploads images' + it_has_behaviour 'Upload' end context 'When there is one image' do include_context 'one image' - it_has_behaviour 'indexes images' - it_has_behaviour 'shows list and grid views' - it_has_behaviour 'shows an image preview' - it_has_behaviour 'deletes an image' - it_has_behaviour 'edits an image' - it_has_behaviour 'uploads images' - it_has_behaviour 'translates an image' + it_has_behaviour 'Index' + it_has_behaviour 'Index Views' + it_has_behaviour 'Preview' + it_has_behaviour 'Delete' + it_has_behaviour 'Edit' + it_has_behaviour 'Upload' + it_has_behaviour 'Translate' end context 'When there are many images' do include_context 'many images' - it_has_behaviour 'indexes images' - it_has_behaviour 'shows list and grid views' - it_has_behaviour 'paginates the list of images' - it_has_behaviour 'shows an image preview' - it_has_behaviour 'deletes an image' - it_has_behaviour 'uploads images' - it_has_behaviour 'edits an image' + it_has_behaviour 'Index' + it_has_behaviour 'Index Views' + it_has_behaviour 'Index Pagination' + it_has_behaviour 'Preview' + it_has_behaviour 'Delete' + it_has_behaviour 'Upload' + it_has_behaviour 'Edit' end end @@ -49,25 +49,25 @@ module Refinery context 'When there are no images' do include_context 'no existing images' - it_has_behaviour 'uploads images' + it_has_behaviour 'Upload' end context 'When there is one image' do include_context 'one image' - it_has_behaviour 'indexes images' - it_has_behaviour 'paginates the list of images' - it_has_behaviour 'uploads images' - it_has_behaviour 'inserts images' + it_has_behaviour 'Index' + it_has_behaviour 'Index Pagination' + it_has_behaviour 'Upload' + it_has_behaviour 'Insert' end context 'When there are many images' do include_context 'many images' - it_has_behaviour 'indexes images' - it_has_behaviour 'paginates the list of images' - it_has_behaviour 'uploads images' - it_has_behaviour 'inserts images' + it_has_behaviour 'Index' + it_has_behaviour 'Index Pagination' + it_has_behaviour 'Upload' + it_has_behaviour 'Insert' end end diff --git a/pages/lib/refinery/pages/finder.rb b/pages/lib/refinery/pages/finder.rb index fa030d7d962..7ff20695718 100644 --- a/pages/lib/refinery/pages/finder.rb +++ b/pages/lib/refinery/pages/finder.rb @@ -44,7 +44,7 @@ def with_mobility attr_accessor :conditions def translated_attributes - Page.translated_attribute_names.map(&:to_s) | %w(locale) + [*Page.mobility_attributes, 'locale'] end def translations_conditions(original_conditions) diff --git a/pages/lib/refinery/pages/instance_methods.rb b/pages/lib/refinery/pages/instance_methods.rb index 1ff334ad961..8b2263e611c 100644 --- a/pages/lib/refinery/pages/instance_methods.rb +++ b/pages/lib/refinery/pages/instance_methods.rb @@ -18,7 +18,7 @@ def error_404(exception = nil) end end - def render(*args) + def render(*args, &block) present @page unless admin? || @meta super end diff --git a/pages/refinerycms-pages.gemspec b/pages/refinerycms-pages.gemspec index c67ff1b60e0..f61c19ffabe 100644 --- a/pages/refinerycms-pages.gemspec +++ b/pages/refinerycms-pages.gemspec @@ -22,8 +22,8 @@ Gem::Specification.new do |s| s.add_dependency 'awesome_nested_set', '~> 3.1', '>= 3.1.0' s.add_dependency 'babosa', '~> 1.0' s.add_dependency 'diffy', '~> 3.1', '>= 3.1.0' - s.add_dependency 'friendly_id', ['>= 5.1.0', '< 5.3'] - s.add_dependency 'friendly_id-mobility', '~> 0.5' + s.add_dependency 'friendly_id', '>= 5.4.0' + s.add_dependency 'friendly_id-mobility', '~> 1.0.3' s.add_dependency 'refinerycms-core', version s.add_dependency 'seo_meta', '~> 3.0', '>= 3.0.0' s.add_dependency 'speakingurl-rails', '~> 8.0', '>= 8.0.0' diff --git a/refinerycms.gemspec b/refinerycms.gemspec index 6a88a5b7a55..3418b4f8f27 100644 --- a/refinerycms.gemspec +++ b/refinerycms.gemspec @@ -24,6 +24,8 @@ Gem::Specification.new do |s| s.add_dependency 'refinerycms-images', version s.add_dependency 'refinerycms-pages', version s.add_dependency 'refinerycms-resources', version + + s.add_development_dependency 'rspec-rails' s.required_ruby_version = Refinery::Version.required_ruby_version s.cert_chain = [File.expand_path('certs/parndt.pem', __dir__)] diff --git a/resources/lib/refinery/resources/validators/file_size_validator.rb b/resources/lib/refinery/resources/validators/file_size_validator.rb index 9570c6253de..f881c53bfa5 100644 --- a/resources/lib/refinery/resources/validators/file_size_validator.rb +++ b/resources/lib/refinery/resources/validators/file_size_validator.rb @@ -7,12 +7,11 @@ def validate(record) file = record.file if file.respond_to?(:length) && file.length > Resources.max_file_size - record.errors[:file] << ::I18n.t('too_big', - :scope => 'activerecord.errors.models.refinery/resource', - :size => Resources.max_file_size) + record.errors.add :file, ::I18n.t('too_big', + scope: 'activerecord.errors.models.refinery/resource', + size: Resources.max_file_size) end end - end end end diff --git a/resources/spec/models/refinery/resource_spec.rb b/resources/spec/models/refinery/resource_spec.rb index 2774c817094..5afc72d3ee7 100644 --- a/resources/spec/models/refinery/resource_spec.rb +++ b/resources/spec/models/refinery/resource_spec.rb @@ -61,6 +61,7 @@ module Refinery expect(resource.title).to eq('Cape Town Tide Table') end end + context 'when a specific title has been given' do it 'returns that title' do expect(titled_resource.title).to eq('Resource Title') @@ -103,7 +104,7 @@ module Refinery end end - specify 'each returned array item should be passed form parameters' do + specify 'each returned array item should should be processed with other form parameters' do params = { file: [file, file, file], fake_param: 'blah' } expect(Resource).to receive(:create).exactly(3).times.with(file: file, fake_param: 'blah') diff --git a/resources/spec/system/refinery/admin/resources_spec.rb b/resources/spec/system/refinery/admin/resources_spec.rb index 65bc50b01ab..a7443ea2678 100644 --- a/resources/spec/system/refinery/admin/resources_spec.rb +++ b/resources/spec/system/refinery/admin/resources_spec.rb @@ -21,17 +21,15 @@ module Admin end context 'new/create' do - let(:uploading_a_file) do - lambda do - visit refinery.admin_resources_path - find('a', text: 'Upload new file').click + def uploading_a_file + visit refinery.admin_resources_path + find('a', text: 'Upload new file').click - expect(page).to have_selector 'iframe#dialog_iframe' + expect(page).to have_selector 'iframe#dialog_iframe' - page.within_frame('dialog_iframe') do - attach_file 'resource_file', file_path - click_button ::I18n.t('save', scope: 'refinery.admin.form_actions') - end + page.within_frame('dialog_iframe') do + attach_file 'resource_file', file_path + click_button ::I18n.t('save', scope: 'refinery.admin.form_actions') end end @@ -39,7 +37,7 @@ module Admin let(:file_path) { Refinery.roots('refinery/resources').join('spec/fixtures/cape-town-tide-table.pdf') } it 'the file is uploaded', js: true do - expect(uploading_a_file).to change(Refinery::Resource, :count).by(1) + expect { uploading_a_file }.to change(Refinery::Resource, :count).by(1) end end @@ -47,7 +45,7 @@ module Admin let(:file_path) { Refinery.roots('refinery/resources').join('spec/fixtures/refinery_is_secure.html') } it 'the file is rejected', js: true do - expect(uploading_a_file).to_not change(Refinery::Resource, :count) + expect { uploading_a_file }.to_not change(Refinery::Resource, :count) page.within_frame('dialog_iframe') do expect(page).to have_content(::I18n.t('incorrect_format', scope: 'activerecord.errors.models.refinery/resource')) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ffb8cbb115c..f989c5c436a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -41,6 +41,7 @@ config.run_all_when_everything_filtered = true config.include ActionView::TestCase::Behavior, :file_path => %r{spec/presenters} config.infer_spec_type_from_file_location! + # config.raise_errors_for_deprecations! config.use_transactional_fixtures = true diff --git a/testing/refinerycms-testing.gemspec b/testing/refinerycms-testing.gemspec index 908cafcb7f1..de4938727b4 100644 --- a/testing/refinerycms-testing.gemspec +++ b/testing/refinerycms-testing.gemspec @@ -22,7 +22,7 @@ Gem::Specification.new do |s| s.add_dependency 'factory_bot_rails', '~> 4.8' s.add_dependency 'rails-controller-testing', '>= 0.1.1' s.add_dependency 'refinerycms-core', version - s.add_dependency 'rspec-rails', '~> 4.0.0.beta2' + s.add_dependency 'rspec-rails', '~> 6.0.0.rc1' s.add_dependency 'webdrivers', '~> 4.0' s.required_ruby_version = Refinery::Version.required_ruby_version From 76ea092031ef3256767b0cb6bbd794b338aa42f8 Mon Sep 17 00:00:00 2001 From: Anita Date: Mon, 18 Jul 2022 12:25:39 +0800 Subject: [PATCH 02/74] Use matching ruby3 branch of refinerycms-i18n while still a work in progress --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 02dda907921..f7acecdaf9a 100644 --- a/Gemfile +++ b/Gemfile @@ -17,7 +17,7 @@ path "./" do gem "refinerycms-resources" end -gem 'refinerycms-i18n', path: '/private/var/www/refinery/refinerycms-i18n' +gem 'refinerycms-i18n', git: 'https://github.com/anitagraham/refinerycms-i18n', branch: ruby3 # Add support for refinerycms-acts-as-indexed gem 'refinerycms-acts-as-indexed', ['~> 4.0', '>= 4.0.0'], From 404b7ddb1471ac6c5add20f1e953191bc82ea795 Mon Sep 17 00:00:00 2001 From: Anita Date: Mon, 18 Jul 2022 12:39:54 +0800 Subject: [PATCH 03/74] Change workflow to test only Ruby 3.1. Correct reference to refinerycms-i18n in Gemfile' --- .github/workflows/main.yml | 2 +- Gemfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7525e5d51b3..d7a190d4eb7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,7 +12,7 @@ jobs: matrix: database: [ mysql, postgresql ] extension: [ core, dragonfly, images, pages, resources ] - ruby: [ 2.7, 2.6 ] + ruby: [ 3.1] fail-fast: false max-parallel: 20 runs-on: ubuntu-latest diff --git a/Gemfile b/Gemfile index f7acecdaf9a..01b5bd58054 100644 --- a/Gemfile +++ b/Gemfile @@ -17,7 +17,7 @@ path "./" do gem "refinerycms-resources" end -gem 'refinerycms-i18n', git: 'https://github.com/anitagraham/refinerycms-i18n', branch: ruby3 +gem 'refinerycms-i18n', git: 'https://github.com/anitagraham/refinerycms-i18n', branch: 'ruby3' # Add support for refinerycms-acts-as-indexed gem 'refinerycms-acts-as-indexed', ['~> 4.0', '>= 4.0.0'], From c06f6402b60cd3c365fbbd253c2e5fb9c7d60b13 Mon Sep 17 00:00:00 2001 From: Anita Date: Thu, 21 Jul 2022 12:46:50 +0800 Subject: [PATCH 04/74] Use Ruby 3 syntax callling Resource.create. Add a description for File and Directory matchers. --- resources/spec/models/refinery/resource_spec.rb | 8 +++++--- spec/support/refinery/generator_spec_describer.rb | 11 +++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 spec/support/refinery/generator_spec_describer.rb diff --git a/resources/spec/models/refinery/resource_spec.rb b/resources/spec/models/refinery/resource_spec.rb index 5afc72d3ee7..ff647bf9080 100644 --- a/resources/spec/models/refinery/resource_spec.rb +++ b/resources/spec/models/refinery/resource_spec.rb @@ -98,16 +98,18 @@ module Refinery end end - specify 'each returned array item should be an instance of resource' do + specify 'each returned item should be an instance of resource' do Resource.create_resources(file: [file, file, file]).each do |resource| expect(resource).to be_an_instance_of(Resource) end end specify 'each returned array item should should be processed with other form parameters' do - params = { file: [file, file, file], fake_param: 'blah' } + params = { file: [file, file, file], file_mime_type: 'application/pdf' } - expect(Resource).to receive(:create).exactly(3).times.with(file: file, fake_param: 'blah') + expect(Resource).to receive(:create).exactly(3).times.with({file: file, + file_mime_type: 'application/pdf' + }) Resource.create_resources(params) end end diff --git a/spec/support/refinery/generator_spec_describer.rb b/spec/support/refinery/generator_spec_describer.rb new file mode 100644 index 00000000000..bdab89ac3a1 --- /dev/null +++ b/spec/support/refinery/generator_spec_describer.rb @@ -0,0 +1,11 @@ +class GeneratorSpec::Matcher::File + def description + 'Matches a file' + end +end + +class GeneratorSpec::Matcher::Directory + def description + 'Matches a directory' + end +end From fd22de0bc28a15c9afc77ac6090c07424fec8c24 Mon Sep 17 00:00:00 2001 From: Anita Date: Thu, 21 Jul 2022 13:30:53 +0800 Subject: [PATCH 05/74] Tidy up configurations for mobility 1.2.9 Tidy up configuration block for Mobility 1.2.9 Revert some changes for single argument keyword args Add descriptions for Generator spec matchers (thereby avoiding a very long message when there are no descriptions). --- Gemfile | 6 ++---- core/lib/refinery/core/engine.rb | 3 --- .../presenters/refinery/translated_field_presenter_spec.rb | 4 ++-- pages/spec/controllers/refinery/pages_controller_spec.rb | 2 +- spec/support/refinery/generator_spec_describer.rb | 4 ++-- 5 files changed, 7 insertions(+), 12 deletions(-) diff --git a/Gemfile b/Gemfile index 01b5bd58054..164020956a2 100644 --- a/Gemfile +++ b/Gemfile @@ -17,12 +17,10 @@ path "./" do gem "refinerycms-resources" end -gem 'refinerycms-i18n', git: 'https://github.com/anitagraham/refinerycms-i18n', branch: 'ruby3' +gem 'refinerycms-i18n', git: 'https://github.com/refinery/refinerycms-i18n', branch: 'ruby3' # Add support for refinerycms-acts-as-indexed -gem 'refinerycms-acts-as-indexed', ['~> 4.0', '>= 4.0.0'], - git: 'https://github.com/refinery/refinerycms-acts-as-indexed', - branch: 'master' +gem 'refinerycms-acts-as-indexed', ['~> 4.0', '>= 4.0.0'], git: 'https://github.com/refinery/refinerycms-acts-as-indexed', branch: 'master' # Add the default visual editor, for now. gem 'refinerycms-wymeditor', ['~> 3.0', '>= 3.0.0'] diff --git a/core/lib/refinery/core/engine.rb b/core/lib/refinery/core/engine.rb index 5eb025e1aba..efe00dce909 100644 --- a/core/lib/refinery/core/engine.rb +++ b/core/lib/refinery/core/engine.rb @@ -51,9 +51,7 @@ def refinery_inclusion! reader # Explicitly declare readers, writer # writers, and backend_reader # backend reader (post.title_backend, etc). - active_record # You must now also explicitly ask for ActiveRecord (or Sequel) - query # i18n is the default scope cache # previously implicit fallbacks @@ -63,7 +61,6 @@ def refinery_inclusion! dirty end # accessor_method not available in v1.0 - # config.accessor_method = :translates #.. not sure if this is covered in the above plugins (AMG. 15/7/22). end end diff --git a/core/spec/presenters/refinery/translated_field_presenter_spec.rb b/core/spec/presenters/refinery/translated_field_presenter_spec.rb index 85d80cb353e..6c9d51b18b1 100644 --- a/core/spec/presenters/refinery/translated_field_presenter_spec.rb +++ b/core/spec/presenters/refinery/translated_field_presenter_spec.rb @@ -6,11 +6,11 @@ module Refinery before do Mobility.with_locale(:en) do - page.update!({ title: "draft" }) + page.update!( title: "draft") end Mobility.with_locale(:lv) do - page.update!({title: "melnraksts"}) + page.update!(title: "melnraksts") end end diff --git a/pages/spec/controllers/refinery/pages_controller_spec.rb b/pages/spec/controllers/refinery/pages_controller_spec.rb index 2e9d86a4df8..252f0782fc7 100644 --- a/pages/spec/controllers/refinery/pages_controller_spec.rb +++ b/pages/spec/controllers/refinery/pages_controller_spec.rb @@ -3,7 +3,7 @@ module Refinery describe PagesController, :type => :controller do before do - FactoryBot.create(:page, link_url: "/") + FactoryBot.create(:page, { link_url: "/" }) FactoryBot.create(:page, title: "testing") FactoryBot.create(:page, link_url: "/items") end diff --git a/spec/support/refinery/generator_spec_describer.rb b/spec/support/refinery/generator_spec_describer.rb index bdab89ac3a1..ff91c8b1391 100644 --- a/spec/support/refinery/generator_spec_describer.rb +++ b/spec/support/refinery/generator_spec_describer.rb @@ -1,11 +1,11 @@ class GeneratorSpec::Matcher::File def description - 'Matches a file' + 'match a file' end end class GeneratorSpec::Matcher::Directory def description - 'Matches a directory' + 'match a directory stucture' end end From 16e23dd8d7dbf5c2fd79f339f6518a5fd71b6944 Mon Sep 17 00:00:00 2001 From: Anita Date: Sat, 20 Aug 2022 11:32:03 +0800 Subject: [PATCH 06/74] WIP --- .../assets/javascripts/refinery/admin.js.erb | 2 +- .../translated_field_presenter_spec.rb | 2 +- .../system/refinery/admin/xhr_paging_spec.rb | 4 +- .../shared_contexts/admin_images_tab.rb | 2 +- .../shared_examples/image_previewer.rb | 2 +- .../support/shared_examples/image_uploader.rb | 10 +-- .../spec/system/refinery/admin/pages_spec.rb | 2 + .../system/refinery/admin/resources_spec.rb | 14 +--- routes.txt | 66 +++++++++++++++++++ spec/spec_helper.rb | 34 +++++++--- spec/support/capybara.rb | 62 +++++++++++++++++ testing/refinerycms-testing.gemspec | 2 +- 12 files changed, 171 insertions(+), 31 deletions(-) create mode 100644 routes.txt create mode 100644 spec/support/capybara.rb diff --git a/core/app/assets/javascripts/refinery/admin.js.erb b/core/app/assets/javascripts/refinery/admin.js.erb index 1a86df70e3a..f594b4daebd 100644 --- a/core/app/assets/javascripts/refinery/admin.js.erb +++ b/core/app/assets/javascripts/refinery/admin.js.erb @@ -636,7 +636,7 @@ var page_options = { if( typeof(visual_editor_init) == "function" ) { visual_editor_init(); } - + // Wipe the title and increment the index counter by one. $('#new_page_part_index').val(parseInt($('#new_page_part_index').val(), 10) + 1); diff --git a/core/spec/presenters/refinery/translated_field_presenter_spec.rb b/core/spec/presenters/refinery/translated_field_presenter_spec.rb index 6c9d51b18b1..da9136b8c20 100644 --- a/core/spec/presenters/refinery/translated_field_presenter_spec.rb +++ b/core/spec/presenters/refinery/translated_field_presenter_spec.rb @@ -6,7 +6,7 @@ module Refinery before do Mobility.with_locale(:en) do - page.update!( title: "draft") + page.update!({ title: "draft" }) end Mobility.with_locale(:lv) do diff --git a/core/spec/system/refinery/admin/xhr_paging_spec.rb b/core/spec/system/refinery/admin/xhr_paging_spec.rb index a60996c3290..1a91f81a5ae 100644 --- a/core/spec/system/refinery/admin/xhr_paging_spec.rb +++ b/core/spec/system/refinery/admin/xhr_paging_spec.rb @@ -2,6 +2,9 @@ module Refinery describe "Crudify", type: :system do + before do + driven_by(:selenium_chrome_headless) + end refinery_login describe "xhr_paging", :js do @@ -37,5 +40,4 @@ module Refinery end end end - end diff --git a/images/spec/support/shared_contexts/admin_images_tab.rb b/images/spec/support/shared_contexts/admin_images_tab.rb index 61fc059c116..0f337286873 100644 --- a/images/spec/support/shared_contexts/admin_images_tab.rb +++ b/images/spec/support/shared_contexts/admin_images_tab.rb @@ -9,7 +9,7 @@ let(:index_item_selector) {'#records li'} let(:gridview_img_selector) {' > img'} - let(:gridview_title_selector) {'[tooltip]'} + let(:gridview_title_selector) {'[title]'} let(:gridview_alt_selector) {'[alt]'} let(:listview_title_selector) {' > span.title'} let(:listview_filename_selector) {' > span.preview'} diff --git a/images/spec/support/shared_examples/image_previewer.rb b/images/spec/support/shared_examples/image_previewer.rb index 2cf3a65e9fe..7656d7fdbcb 100644 --- a/images/spec/support/shared_examples/image_previewer.rb +++ b/images/spec/support/shared_examples/image_previewer.rb @@ -1,5 +1,5 @@ def preview_image - preview_window = window_opened_by do + preview_window = Capybara.window_opened_by do find(:linkhref, image_url).click end diff --git a/images/spec/support/shared_examples/image_uploader.rb b/images/spec/support/shared_examples/image_uploader.rb index b752babc2cb..74eb90ccaea 100644 --- a/images/spec/support/shared_examples/image_uploader.rb +++ b/images/spec/support/shared_examples/image_uploader.rb @@ -7,29 +7,29 @@ def uploading_an_image open_upload_dialog - page.within_frame(dialog_frame_id) do + # page.within_frame(dialog_frame_id) do select_upload attach_file 'image_image', image_path fill_in 'image_image_title', with: 'Image With Dashes' fill_in 'image_image_alt', with: "Alt description for image" click_button ::I18n.t('save', scope: 'refinery.admin.form_actions') - end + # end end context 'when the image type is acceptable' do let(:image_path) { Refinery.roots('refinery/images').join("spec/fixtures/image-with-dashes.jpg") } - it 'the image is uploaded', :js => true do + it 'the image is uploaded', js: true do expect { uploading_an_image }.to change(Refinery::Image, :count).by(1) end end context 'when the image type is not acceptable' do let(:image_path) { Refinery.roots('refinery/images').join("spec/fixtures/cape-town-tide-table.pdf") } - it 'the image is rejected', :js => true do + it 'the image is rejected', js: true do expect { uploading_an_image }.to_not change(Refinery::Image, :count) page.within_frame(dialog_frame_id) do expect(page).to have_content(::I18n.t('incorrect_format', - :scope => 'activerecord.errors.models.refinery/image', + scope: 'activerecord.errors.models.refinery/image', actual: 'application/pdf', valid_types: 'image/jpeg, image/png, image/gif or image/tiff')) end diff --git a/pages/spec/system/refinery/admin/pages_spec.rb b/pages/spec/system/refinery/admin/pages_spec.rb index fb13b476563..f658b42f7db 100644 --- a/pages/spec/system/refinery/admin/pages_spec.rb +++ b/pages/spec/system/refinery/admin/pages_spec.rb @@ -153,7 +153,9 @@ module Admin describe "new/create" do it "Creates a page", js: true do + Rails.logger.debug "#{Time.current} about to visit #{refinery.admin_pages_path}" visit refinery.admin_pages_path + Rails.logger.debug "#{Time.current} at #{refinery.admin_pages_path}" find('a', text: 'Add new page').click diff --git a/resources/spec/system/refinery/admin/resources_spec.rb b/resources/spec/system/refinery/admin/resources_spec.rb index a7443ea2678..709f19efdf6 100644 --- a/resources/spec/system/refinery/admin/resources_spec.rb +++ b/resources/spec/system/refinery/admin/resources_spec.rb @@ -24,13 +24,8 @@ module Admin def uploading_a_file visit refinery.admin_resources_path find('a', text: 'Upload new file').click - - expect(page).to have_selector 'iframe#dialog_iframe' - - page.within_frame('dialog_iframe') do - attach_file 'resource_file', file_path - click_button ::I18n.t('save', scope: 'refinery.admin.form_actions') - end + attach_file 'resource_file', file_path + click_button ::I18n.t('save', scope: 'refinery.admin.form_actions') end context 'when the file mime_type is acceptable' do @@ -46,10 +41,7 @@ def uploading_a_file it 'the file is rejected', js: true do expect { uploading_a_file }.to_not change(Refinery::Resource, :count) - - page.within_frame('dialog_iframe') do - expect(page).to have_content(::I18n.t('incorrect_format', scope: 'activerecord.errors.models.refinery/resource')) - end + expect(page).to have_content(::I18n.t('incorrect_format', scope: 'activerecord.errors.models.refinery/resource')) end end diff --git a/routes.txt b/routes.txt new file mode 100644 index 00000000000..24f6bbcebae --- /dev/null +++ b/routes.txt @@ -0,0 +1,66 @@ +Usage: + rails new APP_PATH [options] + +Options: + [--skip-namespace], [--no-skip-namespace] # Skip namespace (affects only isolated engines) + [--skip-collision-check], [--no-skip-collision-check] # Skip collision check + -r, [--ruby=PATH] # Path to the Ruby binary of your choice + # Default: /Users/anita/.rbenv/versions/3.1.2/bin/ruby + -m, [--template=TEMPLATE] # Path to some application template (can be a filesystem path or URL) + -d, [--database=DATABASE] # Preconfigure for selected database (options: mysql/postgresql/sqlite3/oracle/sqlserver/jdbcmysql/jdbcsqlite3/jdbcpostgresql/jdbc) + # Default: sqlite3 + -G, [--skip-git], [--no-skip-git] # Skip .gitignore file + [--skip-keeps], [--no-skip-keeps] # Skip source control .keep files + -M, [--skip-action-mailer], [--no-skip-action-mailer] # Skip Action Mailer files + [--skip-action-mailbox], [--no-skip-action-mailbox] # Skip Action Mailbox gem + [--skip-action-text], [--no-skip-action-text] # Skip Action Text gem + -O, [--skip-active-record], [--no-skip-active-record] # Skip Active Record files + [--skip-active-job], [--no-skip-active-job] # Skip Active Job + [--skip-active-storage], [--no-skip-active-storage] # Skip Active Storage files + -C, [--skip-action-cable], [--no-skip-action-cable] # Skip Action Cable files + -A, [--skip-asset-pipeline], [--no-skip-asset-pipeline] # Indicates when to generate skip asset pipeline + -a, [--asset-pipeline=ASSET_PIPELINE] # Choose your asset pipeline [options: sprockets (default), propshaft] + # Default: sprockets + -J, [--skip-javascript], [--no-skip-javascript] # Skip JavaScript files + [--skip-hotwire], [--no-skip-hotwire] # Skip Hotwire integration + [--skip-jbuilder], [--no-skip-jbuilder] # Skip jbuilder gem + -T, [--skip-test], [--no-skip-test] # Skip test files + [--skip-system-test], [--no-skip-system-test] # Skip system test files + [--skip-bootsnap], [--no-skip-bootsnap] # Skip bootsnap gem + [--dev], [--no-dev] # Set up the application with Gemfile pointing to your Rails checkout + [--edge], [--no-edge] # Set up the application with Gemfile pointing to Rails repository + --master, [--main], [--no-main] # Set up the application with Gemfile pointing to Rails repository main branch + [--rc=RC] # Path to file containing extra configuration options for rails command + [--no-rc], [--no-no-rc] # Skip loading of extra configuration options from .railsrc file + [--api], [--no-api] # Preconfigure smaller stack for API only apps + [--minimal], [--no-minimal] # Preconfigure a minimal rails app + -j, [--javascript=JAVASCRIPT] # Choose JavaScript approach [options: importmap (default), webpack, esbuild, rollup] + # Default: importmap + -c, [--css=CSS] # Choose CSS processor [options: tailwind, bootstrap, bulma, postcss, sass... check https://github.com/rails/cssbundling-rails] + -B, [--skip-bundle], [--no-skip-bundle] # Don't run bundle install + +Runtime options: + -f, [--force] # Overwrite files that already exist + -p, [--pretend], [--no-pretend] # Run but do not make any changes + -q, [--quiet], [--no-quiet] # Suppress status output + -s, [--skip], [--no-skip] # Skip files that already exist + +Rails options: + -h, [--help], [--no-help] # Show this help message and quit + -v, [--version], [--no-version] # Show Rails version number and quit + +Description: + The 'rails new' command creates a new Rails application with a default + directory structure and configuration at the path you specify. + + You can specify extra command-line arguments to be used every time + 'rails new' runs in the .railsrc configuration file in your home directory, + or in $XDG_CONFIG_HOME/rails/railsrc if XDG_CONFIG_HOME is set. + + Note that the arguments specified in the .railsrc file don't affect the + defaults values shown above in this help message. + +Example: + rails new ~/Code/Ruby/weblog + + This generates a skeletal Rails installation in ~/Code/Ruby/weblog. diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f989c5c436a..56c87e91a14 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -14,11 +14,27 @@ require File.expand_path("../dummy/config/environment", __FILE__) +require 'rack/test' require 'rspec/rails' require 'capybara/rspec' -require 'webdrivers/chromedriver' +require 'falcon' require 'falcon/capybara' -Capybara.server = :falcon +require 'webdrivers/chromedriver' +require 'selenium/webdriver' +# +# Capybara.server = :falcon_https +# Capybara.default_driver = :selenium_chrome_https +# Capybara.javascript_driver = :selenium_chrome_headless_https + +# if testing on localhost +Capybara.configure do |config| + # config.app = Rack::Builder.parse_file( + # File.expand_path('../config.ru', __dir__) + # ).first + + # For HTTPS: + # For not headless, use `:selenium_chrome_https` +end if ENV['RETRY_COUNT'] require 'rspec/retry' @@ -58,13 +74,13 @@ ::I18n.default_locale = I18n.locale = Mobility.locale = :en end - config.before(:each, type: :system) do - driven_by :rack_test - end - - config.before(:each, type: :system, js: true) do - driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1080] - end + # config.before(:each, type: :system) do + # driven_by :selenium_chrome_headless + # end + # + # config.before(:each, type: :system, js: true) do + # driven_by :selenium_chrome_headless, using: :headless_chrome, screen_size: [1400, 1080] + # end unless ENV['FULL_BACKTRACE'] config.backtrace_exclusion_patterns = %w( diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb new file mode 100644 index 00000000000..3c7ab220027 --- /dev/null +++ b/spec/support/capybara.rb @@ -0,0 +1,62 @@ +require 'rack/test' +require 'rspec/rails' +require 'capybara/rspec' +require 'falcon' +require 'falcon/capybara' +require 'webdrivers/chromedriver' +require 'selenium/webdriver' + +Capybara.register_driver :local_selenium do |app| + options = Selenium::WebDriver::Chrome::Options.new + options.add_argument("--window-size=1400,1080") + + Capybara::Selenium::Driver.new(app, browser: :chrome, options: options) +end + +Capybara.register_driver :local_selenium_headless do |app| + options = Selenium::WebDriver::Chrome::Options.new + options.add_argument("--headless") + options.add_argument("--window-size=1400,1400") + + Capybara::Selenium::Driver.new(app, browser: :chrome, options: options) +end + +selenium_app_host = ENV.fetch("SELENIUM_APP_HOST") do + Socket.ip_address_list + .find(&:ipv4_private?) + .ip_address +end + +Capybara.configure do |config| + # config.server = :puma, { Silent: true } + # config.app = Rack::Builder.parse_file( + # File.expand_path('../config.ru', __dir__) + # ).first + config.server = :falcon + config.server = :falcon_https + config.default_driver = :selenium_chrome_https + config.javascript_driver = :selenium_chrome_headless_https + config.server_host = selenium_app_host +end + +RSpec.configure do |config| + config.before(:each, type: :system) do |example| + # `Capybara.app_host` is reset in the RSpec before_setup callback defined + # in `ActionDispatch::SystemTesting::TestHelpers::SetupAndTeardown`, which + # is annoying as hell, but not easy to "fix". Just set it manually every + # test run. + Capybara.app_host = "http://#{Capybara.server_host}:#{Capybara.server_port}" + + # Allow Capybara and WebDrivers to access network if necessary + driver = if example.metadata[:js] + locality = ENV["SELENIUM_HOST"].present? ? :remote : :local + headless = "_headless" if ENV["DISABLE_HEADLESS"].blank? + + "#{locality}_selenium#{headless}".to_sym + else + :rack_test + end + + driven_by driver + end +end diff --git a/testing/refinerycms-testing.gemspec b/testing/refinerycms-testing.gemspec index de4938727b4..853c74b181a 100644 --- a/testing/refinerycms-testing.gemspec +++ b/testing/refinerycms-testing.gemspec @@ -23,7 +23,7 @@ Gem::Specification.new do |s| s.add_dependency 'rails-controller-testing', '>= 0.1.1' s.add_dependency 'refinerycms-core', version s.add_dependency 'rspec-rails', '~> 6.0.0.rc1' - s.add_dependency 'webdrivers', '~> 4.0' + s.add_dependency 'webdrivers', '~> 5.0' s.required_ruby_version = Refinery::Version.required_ruby_version From 0067df3f07833d6eaf3fd1a3d3c67c54d0461a3a Mon Sep 17 00:00:00 2001 From: Anita Date: Thu, 19 Dec 2024 14:17:07 +0800 Subject: [PATCH 07/74] remove all capybara configuration from spec/spec_helper to already existing file spec/support/capybara.rb. remove requirement for 'webdrivers/chromedriver' as this is all handled by Selenium consistent errors in Selenium chrome webdriver, temporarily switched to firefox. --- spec/spec_helper.rb | 33 ++++++++++++--------------------- spec/support/capybara.rb | 21 ++++++++++++--------- 2 files changed, 24 insertions(+), 30 deletions(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 56c87e91a14..3c327f77de9 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -16,24 +16,14 @@ require 'rack/test' require 'rspec/rails' -require 'capybara/rspec' require 'falcon' -require 'falcon/capybara' -require 'webdrivers/chromedriver' -require 'selenium/webdriver' -# -# Capybara.server = :falcon_https -# Capybara.default_driver = :selenium_chrome_https -# Capybara.javascript_driver = :selenium_chrome_headless_https + # if testing on localhost Capybara.configure do |config| - # config.app = Rack::Builder.parse_file( - # File.expand_path('../config.ru', __dir__) - # ).first - - # For HTTPS: - # For not headless, use `:selenium_chrome_https` + config.app = Rack::Builder.parse_file( + File.expand_path('../config.ru', __dir__) + ).first end if ENV['RETRY_COUNT'] @@ -74,13 +64,13 @@ ::I18n.default_locale = I18n.locale = Mobility.locale = :en end - # config.before(:each, type: :system) do - # driven_by :selenium_chrome_headless - # end - # - # config.before(:each, type: :system, js: true) do - # driven_by :selenium_chrome_headless, using: :headless_chrome, screen_size: [1400, 1080] - # end + config.before(:each, type: :system) do + driven_by :selenium_chrome_headless + end + + config.before(:each, type: :system, js: true) do + driven_by :selenium_chrome_headless, using: :headless_chrome, screen_size: [1400, 1080] + end unless ENV['FULL_BACKTRACE'] config.backtrace_exclusion_patterns = %w( @@ -98,5 +88,6 @@ ([ENGINE_RAILS_ROOT, Rails.root.to_s].uniq | Refinery::Plugins.registered.pathnames).map{ |p| Dir[File.join(p, 'spec', 'support', '**', '*.rb').to_s] }.flatten.sort.each do |support_file| + # puts "Requiring #{support_file}" require support_file end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 3c7ab220027..8161d769228 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -1,24 +1,24 @@ require 'rack/test' require 'rspec/rails' require 'capybara/rspec' -require 'falcon' require 'falcon/capybara' -require 'webdrivers/chromedriver' require 'selenium/webdriver' Capybara.register_driver :local_selenium do |app| - options = Selenium::WebDriver::Chrome::Options.new + options = Selenium::WebDriver::Options.firefox + # options = Selenium::WebDriver::Chrome::Options.new options.add_argument("--window-size=1400,1080") - Capybara::Selenium::Driver.new(app, browser: :chrome, options: options) + Capybara::Selenium::Driver.new(app, browser: :firefox, options: options) end Capybara.register_driver :local_selenium_headless do |app| - options = Selenium::WebDriver::Chrome::Options.new + options = Selenium::WebDriver::Options.firefox + # options = Selenium::WebDriver::Chrome::Options.new options.add_argument("--headless") options.add_argument("--window-size=1400,1400") - Capybara::Selenium::Driver.new(app, browser: :chrome, options: options) + Capybara::Selenium::Driver.new(app, browser: :firefox, options: options) end selenium_app_host = ENV.fetch("SELENIUM_APP_HOST") do @@ -33,10 +33,13 @@ # File.expand_path('../config.ru', __dir__) # ).first config.server = :falcon - config.server = :falcon_https - config.default_driver = :selenium_chrome_https - config.javascript_driver = :selenium_chrome_headless_https + # config.server = :falcon_https + config.default_driver = :local_selenium + # config.default_driver = :selenium_chrome_https + config.javascript_driver = :local_selenium_headless + # config.javascript_driver = :selenium_chrome_headless_https config.server_host = selenium_app_host + Selenium::WebDriver.logger.ignore(:clear_local_storage, :clear_session_storage) end RSpec.configure do |config| From 19eb40d9e4153f0b5594e23030700dfbf742f118 Mon Sep 17 00:00:00 2001 From: Anita Date: Thu, 19 Dec 2024 14:21:12 +0800 Subject: [PATCH 08/74] remove reliance on www.example.com in tests, use attribute selectors to target the part of the url which should change. --- pages/spec/system/refinery/admin/pages_spec.rb | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pages/spec/system/refinery/admin/pages_spec.rb b/pages/spec/system/refinery/admin/pages_spec.rb index f658b42f7db..faf49a85224 100644 --- a/pages/spec/system/refinery/admin/pages_spec.rb +++ b/pages/spec/system/refinery/admin/pages_spec.rb @@ -890,17 +890,16 @@ module Admin before { Refinery::Pages.absolute_page_links = true } it "shows Russian pages if we're editing the Russian locale" do - visit refinery.link_to_admin_pages_dialogs_path(:visual_editor => true, :switch_locale => :ru) - + visit refinery.link_to_admin_pages_dialogs_path(visual_editor: true, switch_locale: :ru) expect(page).to have_content("About Ru") - expect(page).to have_selector("a[href='http://www.example.com/ru/about-ru']") + expect(page).to have_selector("a[href$='ru/about-ru']") end it "shows default to the default locale if no query string is added" do - visit refinery.link_to_admin_pages_dialogs_path(:visual_editor => true) + visit refinery.link_to_admin_pages_dialogs_path(visual_editor: true) expect(page).to have_content("About") - expect(page).to have_selector("a[href='http://www.example.com/about']") + expect(page).to have_selector("a[href$='about']") end end end From b6b34e2117109606d3bdf58b11d46c73a3dbc714 Mon Sep 17 00:00:00 2001 From: Anita Date: Thu, 19 Dec 2024 14:26:44 +0800 Subject: [PATCH 09/74] previous usage of 'Pages' as a constant referring to Refinery::Pages was failing in Refinery::Admin tests, so now using Refinery::Pages(*) --- .../controllers/refinery/admin/pages_controller.rb | 12 ++++++------ .../refinery/pages/admin/pages_helper_spec.rb | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pages/app/controllers/refinery/admin/pages_controller.rb b/pages/app/controllers/refinery/admin/pages_controller.rb index f94c708d490..42a42cc20a3 100644 --- a/pages/app/controllers/refinery/admin/pages_controller.rb +++ b/pages/app/controllers/refinery/admin/pages_controller.rb @@ -1,17 +1,17 @@ module Refinery module Admin class PagesController < Refinery::AdminController - prepend Pages::InstanceMethods + prepend Refinery::Pages::InstanceMethods crudify :'refinery/page', - :include => [:translations, :children], - :paging => false + include: [:translations, :children], + paging: false helper_method :valid_layout_templates, :valid_view_templates def new @page = Page.new(new_page_params) - Pages.default_parts_for(@page).each_with_index do |page_part, index| + Refinery::Pages.default_parts_for(@page).each_with_index do |page_part, index| @page.parts << PagePart.new(:title => page_part[:title], :slug => page_part[:slug], :position => index) end end @@ -79,11 +79,11 @@ def mobility! end def valid_layout_templates - Pages.layout_template_whitelist & Pages.valid_templates(*Pages.layout_templates_pattern) + Refinery::Pages.layout_template_whitelist & Refinery::Pages.valid_templates(*Refinery::Pages.layout_templates_pattern) end def valid_view_templates - Pages.valid_templates(*Pages.view_templates_pattern) + Refinery::Pages.valid_templates(*Refinery::Pages.view_templates_pattern) end def page_params diff --git a/pages/spec/helpers/refinery/pages/admin/pages_helper_spec.rb b/pages/spec/helpers/refinery/pages/admin/pages_helper_spec.rb index cbe89a03929..5cb97c52835 100644 --- a/pages/spec/helpers/refinery/pages/admin/pages_helper_spec.rb +++ b/pages/spec/helpers/refinery/pages/admin/pages_helper_spec.rb @@ -18,13 +18,13 @@ module Admin context "when page layout template is set using symbols" do before do - allow(Pages.config).to receive(:layout_template_whitelist).and_return [:three, :one, :two] + allow(Refinery::Pages.config).to receive(:layout_template_whitelist).and_return [:three, :one, :two] end - it "works as expected" do - page = FactoryBot.create(:page, :layout_template => "three") + it "page layout template is set correctly" do + page = FactoryBot.create(:page, layout_template: "three") - expect(helper.template_options(:layout_template, page)).to eq(:selected => 'three') + expect(helper.template_options(:layout_template, page)).to eq(selected: 'three') end end From febcc903ecf15a21b1ba671b618ea53a15fe68f5 Mon Sep 17 00:00:00 2001 From: Anita Date: Thu, 19 Dec 2024 14:30:35 +0800 Subject: [PATCH 10/74] saving generator options before switching to 'pretend=true' inside the options hash, as options[:original_options] --- .../generators/refinery/cms/cms_generator.rb | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/core/lib/generators/refinery/cms/cms_generator.rb b/core/lib/generators/refinery/cms/cms_generator.rb index b4a86e645d1..1b37d643066 100644 --- a/core/lib/generators/refinery/cms/cms_generator.rb +++ b/core/lib/generators/refinery/cms/cms_generator.rb @@ -5,18 +5,18 @@ module Refinery class CmsGenerator < Rails::Generators::Base source_root Pathname.new(File.expand_path('../templates', __FILE__)) - class_option :update, :type => :boolean, :aliases => nil, :group => :runtime, - :desc => "Update an existing Refinery CMS based application" - class_option :fresh_installation, :type => :boolean, :aliases => nil, :group => :runtime, :default => false, - :desc => "Allow Refinery to remove default Rails files in a fresh installation" - class_option :heroku, :type => :string, :default => nil, :group => :runtime, :banner => 'APP_NAME', - :desc => "Deploy to Heroku after the generator has run." - class_option :stack, :type => :string, :default => 'cedar-14', :group => :runtime, - :desc => "Specify which Heroku stack you want to use. Requires --heroku option to function." - class_option :skip_db, :type => :boolean, :default => false, :aliases => nil, :group => :runtime, - :desc => "Skip over any database creation, migration or seeding." - class_option :skip_migrations, :type => :boolean, :default => false, :aliases => nil, :group => :runtime, - :desc => "Skip over installing or running migrations." + class_option :update, type: :boolean, aliases: nil, group: :runtime, + desc: "Update an existing Refinery CMS based application" + class_option :fresh_installation, type: :boolean, aliases: nil, group: :runtime, default: false, + desc: "Allow Refinery to remove default Rails files in a fresh installation" + class_option :heroku, type: :string, default: nil, group: :runtime, banner: 'APP_NAME', + desc: "Deploy to Heroku after the generator has run." + class_option :stack, type: :string, default: 'cedar-14', group: :runtime, + desc: "Specify which Heroku stack you want to use. Requires --heroku option to function." + class_option :skip_db, type: :boolean, default: false, aliases: nil, group: :runtime, + desc: "Skip over any database creation, migration or seeding." + class_option :skip_migrations, type: :boolean, default: false, aliases: nil, group: :runtime, + desc: "Skip over installing or running migrations." def generate start_pretending? @@ -55,7 +55,7 @@ def append_asset_pipeline! if destination_path.join(application_css).file? insert_into_file application_css, %q{*= require refinery/formatting *= require refinery/theme - }, :before => "*= require_self" + }, before: "*= require_self" end end @@ -215,10 +215,10 @@ def ensure_environments_are_sane! " end" ].join("\n") - gsub_file env, current_mailer_config, new_mailer_config, :verbose => false + gsub_file env, current_mailer_config, new_mailer_config, verbose: false end - gsub_file env, "config.assets.compile = false", "config.assets.compile = true", :verbose => false + gsub_file env, "config.assets.compile = false", "config.assets.compile = true", verbose: false end end @@ -253,7 +253,7 @@ def manage_roadblocks! %w(public/index.html app/views/layouts/application.html.erb).each do |roadblock| if (roadblock_path = destination_path.join(roadblock)).file? if self.options[:fresh_installation] - remove_file roadblock_path, :verbose => true + remove_file roadblock_path, verbose: true else say_status :"-- You may need to remove '#{roadblock}' for Refinery to function properly --", nil, :yellow end @@ -329,10 +329,8 @@ def start_pretending? # Only pretend to do the next actions if this is Refinery to stay DRY if destination_path == Refinery.root say_status :'-- pretending to make changes that happen in an actual installation --', nil, :yellow - old_pretend = self.options[:pretend] - new_options = self.options.dup - new_options[:pretend] = true - self.options = new_options + original_pretend = self.options[:pretend] + self.options.merge(pretend: true, original_pretend: original_pretend) end end @@ -340,9 +338,8 @@ def stop_pretending? # Stop pretending if destination_path == Refinery.root say_status :'-- finished pretending --', nil, :yellow - new_options = self.options.dup - new_options[:pretend] = old_pretend - self.options = new_options + original_pretend = self.options[:original_pretend] + self.options.merge(pretend: original_pretend).remove(:original_pretend) end end end From 5394a97748ee8327f7e2a7198dd2f3359bb54322 Mon Sep 17 00:00:00 2001 From: Anita Date: Thu, 19 Dec 2024 14:33:55 +0800 Subject: [PATCH 11/74] remove another instance of 'www.example.com' --- pages/spec/system/refinery/pages_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/spec/system/refinery/pages_spec.rb b/pages/spec/system/refinery/pages_spec.rb index 63212ca0150..97ed958c661 100644 --- a/pages/spec/system/refinery/pages_spec.rb +++ b/pages/spec/system/refinery/pages_spec.rb @@ -161,7 +161,7 @@ def standard_page_menu_items_exist? it 'should have a canonical url' do visit '/about-us' - expect(page).to have_selector('head link[rel="canonical"][href^="http://www.example.com/about-us"]', visible: false) + expect(page).to have_selector('head link[rel="canonical"][href$="about-us"]', visible: false) end end From fe6899356317805fee92fd2207dcaf69dee5ca84 Mon Sep 17 00:00:00 2001 From: Anita Date: Wed, 25 Dec 2024 10:24:11 +0800 Subject: [PATCH 12/74] refinery_help_tag will always show the 'tooltip' message --- .../stylesheets/refinery/mixins/_labels.scss | 17 ++++++++++ .../assets/stylesheets/refinery/refinery.scss | 3 +- .../refinery/sections/_layout.scss | 31 ++++++++++--------- core/app/helpers/refinery/tag_helper.rb | 2 +- 4 files changed, 36 insertions(+), 17 deletions(-) create mode 100644 core/app/assets/stylesheets/refinery/mixins/_labels.scss diff --git a/core/app/assets/stylesheets/refinery/mixins/_labels.scss b/core/app/assets/stylesheets/refinery/mixins/_labels.scss new file mode 100644 index 00000000000..a77c95504ce --- /dev/null +++ b/core/app/assets/stylesheets/refinery/mixins/_labels.scss @@ -0,0 +1,17 @@ +@mixin label_with_help { + margin-bottom: 5px; + font-weight: bold; + margin-top: 20px; + display: block; + vertical-align: middle; + + > label, > .help, { + display: inline; + vertical-align: middle; + } + > .help { + font-weight: normal; + font-style: italic; + margin-left: 3px; + } +} diff --git a/core/app/assets/stylesheets/refinery/refinery.scss b/core/app/assets/stylesheets/refinery/refinery.scss index 15ed8a80b75..408382438ef 100644 --- a/core/app/assets/stylesheets/refinery/refinery.scss +++ b/core/app/assets/stylesheets/refinery/refinery.scss @@ -8,6 +8,7 @@ @import 'mixins/icon_base'; @import 'mixins/icon'; @import 'mixins/rounded'; +@import 'mixins/labels'; @import 'plugins/ui'; @@ -19,4 +20,4 @@ @import 'sections/layout'; -@import 'site_bar'; \ No newline at end of file +@import 'site_bar'; diff --git a/core/app/assets/stylesheets/refinery/sections/_layout.scss b/core/app/assets/stylesheets/refinery/sections/_layout.scss index cfd137e04a8..ed05e1446c6 100644 --- a/core/app/assets/stylesheets/refinery/sections/_layout.scss +++ b/core/app/assets/stylesheets/refinery/sections/_layout.scss @@ -644,7 +644,8 @@ textarea { width: auto; height: 25px; } -label, .label_with_help { +//label, .label_with_help { +label { margin-bottom: 5px; font-weight: bold; margin-top: 20px; @@ -1371,20 +1372,20 @@ input.button.close_dialog:active, a.button.close_dialog:active, #content a.butto width: 98%; } .label_with_help { - vertical-align: middle; -} -.label_with_help label, .label_with_help span.help, .label_with_help img.help { - display: inline; -} -.label_with_help img.help { - vertical-align: middle; -} -.label_with_help span.help { - font-weight: normal; - margin-left: 3px; - border-bottom: 1px dotted #727272; - cursor: help; -} + @include label_with_help +} +//.label_with_help label, .label_with_help span.help, .label_with_help img.help { +// display: inline; +//} +//.label_with_help img.help { +// vertical-align: middle; +//} +//.label_with_help span.help { +// font-weight: normal; +// margin-left: 3px; +// border-bottom: 1px dotted #727272; +// cursor: help; +//} #more_options_field { position: relative; } diff --git a/core/app/helpers/refinery/tag_helper.rb b/core/app/helpers/refinery/tag_helper.rb index 20acff05f4a..db6842b9b50 100644 --- a/core/app/helpers/refinery/tag_helper.rb +++ b/core/app/helpers/refinery/tag_helper.rb @@ -6,7 +6,7 @@ module TagHelper # Remember to wrap your block with if you're using a label next to the help tag. def refinery_help_tag(title='Tip') title = title.html_safe? ? title : h(title) - action_icon(:info, '#', title, {tooltip: title}) + tag.span title, class: :help end # This is just a quick wrapper to render an image tag that lives inside refinery/icons. From 6e78d70c4d30bbd6490170c4c035c10598df6640 Mon Sep 17 00:00:00 2001 From: Anita Date: Sun, 29 Dec 2024 12:24:50 +0800 Subject: [PATCH 13/74] css cleanup: remove units from unitless values. (0px -> 0) --- .../refinery/sections/_layout.scss | 178 +++++++++--------- 1 file changed, 89 insertions(+), 89 deletions(-) diff --git a/core/app/assets/stylesheets/refinery/sections/_layout.scss b/core/app/assets/stylesheets/refinery/sections/_layout.scss index ed05e1446c6..3124bd9e0c7 100644 --- a/core/app/assets/stylesheets/refinery/sections/_layout.scss +++ b/core/app/assets/stylesheets/refinery/sections/_layout.scss @@ -1,6 +1,6 @@ $admin_width: 1080px; $menu_width: 120px; -$standard_rounding: 0px; +$standard_rounding: 0; p { font-size: 14px; @@ -28,7 +28,7 @@ label[for] { #admin_container, #login_container { font-size: 13px; - margin: 0px auto 15px auto; + margin: 0 auto 15px auto; text-align: left; position: relative; margin-top: 15px; @@ -39,7 +39,7 @@ label[for] { } #menu { a:hover { - border-bottom: 0px; + border-bottom: 0; } } .hidden { @@ -52,7 +52,7 @@ label[for] { } #admin_container { width: $admin_width; - margin: -5px auto 0px auto; + margin: -5px auto 0 auto; height: 100%; @include faux_columns_2($menu_width, transparent, white); } @@ -84,9 +84,9 @@ select.widest { float: right; width: 30%; padding-left: 15px; - padding-top: 0px; + padding-top: 0; ul { - margin: 0px 0px 18px 0px; + margin: 0 0 18px 0; padding: 0; list-style: none; &#current_locale { @@ -105,7 +105,7 @@ select.widest { } } #sort-status { - padding: 5px 5px 5px 0px; + padding: 5px 5px 5px 0; } hr { border: 0; @@ -114,7 +114,7 @@ hr { .errorExplanation { background-color: #FFB1B1; - padding: 0px 5px 5px 30px; + padding: 0 5px 5px 30px; font-weight: bold; margin-top: 5px; margin-bottom: 5px; @@ -208,7 +208,7 @@ noscript .flash.flash_alert a:hover { } #content .visual_editor_box a, #content .ui-tabs a { - border-bottom: 0px none; + border-bottom: 0 none; } .index #content, .splash #content { @@ -216,7 +216,7 @@ noscript .flash.flash_alert a:hover { background-repeat: repeat-y; } #content { - padding: 0px; + padding: 0; background-color: white; a { border-bottom: 1px dotted #727272; @@ -341,7 +341,7 @@ nav { } } label { - margin-top: 0px; + margin-top: 0; } label, a { @@ -351,8 +351,8 @@ nav { margin-bottom: 20px; &.remember_me, &.forgot_password { - margin-bottom: 0px; - margin-top: 0px; + margin-bottom: 0; + margin-top: 0; } &.forgot_password { text-align: right; @@ -371,10 +371,10 @@ nav { height: 31px; } input.larger:focus { - background-position: 0px -41px; + background-position: 0 -41px; } .fieldWithErrors input.larger { - background-position: 0px -82px; + background-position: 0 -82px; border: 1px solid red; } } @@ -389,17 +389,17 @@ div.field.checkbox_access { } #menu { display: block; - margin: 45px 0px 0px 0px; - padding: 0px; + margin: 45px 0 0 0; + padding: 0; position: absolute; background: transparent; } header { float: left; - margin-bottom: 0px; + margin-bottom: 0; width: $menu_width; a, a:hover { - border-bottom: 0px none; + border-bottom: 0 none; } #logo { position: absolute; @@ -495,7 +495,7 @@ pre { > ul li, > #recent_activity > ul li, > #recent_inquiries > ul li, .pagination_container > ul li, .pagination_frame > ul { list-style: none; - padding: 0px 5px; + padding: 0 5px; vertical-align: top; margin-bottom: 2px; line-height: 35px; @@ -521,15 +521,15 @@ pre { ul { &.clickable { li { - padding: 0px; - margin-bottom: 0px; + padding: 0; + margin-bottom: 0; a { - padding: 0px 5px; + padding: 0 5px; vertical-align: top; margin-bottom: 2px; line-height: 35px; display: block; - border-bottom: 0px none; + border-bottom: 0 none; } } } @@ -578,15 +578,15 @@ pre { padding: 0; } #records.tree ul li, .tree ul li { - margin: 0px; + margin: 0; padding: 4px 0 0 40px; - background: image_url('refinery/branch.gif') no-repeat 15px 0px; + background: image_url('refinery/branch.gif') no-repeat 15px 0; } #records.tree li.record ul { margin-left: 0; } #records.tree .on-hover, #pagination ul.tree a:hover, #pagination .tree .on { - background: image_url('refinery/branch.gif') no-repeat 15px 0px; + background: image_url('refinery/branch.gif') no-repeat 15px 0; } #records.tree ul li.branch_start, .tree ul li.branch_start { background-image: image_url('refinery/branch-start.gif'); @@ -607,16 +607,16 @@ pre { cursor: move; } #records h2, #actions h2 { - margin-top: 0px; + margin-top: 0; } .pagination { background-color: #C9DAE2; padding: 5px 5px 4px 5px; - margin: 10px 0px; + margin: 10px 0; em { font-weight: bold; font-style: normal; - padding: 0px 6px; + padding: 0 6px; } } .pagination { @@ -626,7 +626,7 @@ pre { a, #content a, .current, .disabled, em { padding: 7px; line-height: 20px; - border-bottom: 0px none; + border-bottom: 0 none; } .current, a:hover, em { background: #A8B9C1; @@ -668,8 +668,8 @@ label.stripped { display: inline; font-weight: normal; font-size: 1em; - margin: 0px; - padding: 0px; + margin: 0; + padding: 0; } #body_field { float: left; @@ -735,8 +735,8 @@ label.stripped { } #records .actions { position: absolute; - right: 0px; - top: 0px; + right: 0; + top: 0; width: 120px; text-align: right; display: block; @@ -759,7 +759,7 @@ label.stripped { text-align: center; } #content #records.tree > ul li .actions a { - margin: 3px 3px 0px 3px; + margin: 3px 3px 0 3px; line-height: 24px; } .actions a * { @@ -809,7 +809,7 @@ label.stripped { li { margin: 0; list-style: none; - padding: 5px 0px 5px 0px; + padding: 5px 0 5px 0; } a { font-weight: bold; @@ -830,7 +830,7 @@ label.stripped { padding: 7px; } #inquiry tr:last-child td, .inquiry tr:last-child td { - border-bottom: 0px; + border-bottom: 0; } #inquiry td label.stripped, .inquiry td label.stripped { font-weight: bold; @@ -858,7 +858,7 @@ body.dialog form { span.radio { display: block; line-height: 18px; - padding: 6px 0px; + padding: 6px 0; } span.radio * { cursor: pointer; @@ -878,12 +878,12 @@ body.dialog form { #existing_image_area_content, #existing_image_area_crops { margin-top: 28px; ul { - margin: 0px; - padding: 0px; + margin: 0; + padding: 0; li { list-style: none; - padding: 0px; - margin: 0px 2px 0px 0px; + padding: 0; + margin: 0 2px 0 0; float: left; height: 114px; max-height: 114px; @@ -906,12 +906,12 @@ body.dialog form { margin-top: 18px; margin-bottom: 50px; ul { - margin: 0px; - padding: 10px 0px 0px 0px; + margin: 0; + padding: 10px 0 0 0; li { float: left; list-style: none; - margin: 0px 18px 0px 0px; + margin: 0 18px 0 0; text-align:center; a { display: block; @@ -931,12 +931,12 @@ body.dialog form { } #existing_image_size_area { #image_dialog_size_0 a { height: 30px; width: 30px; line-height: 30px; margin-top: 10px } - #image_dialog_size_1 a { height: 50px; width: 50px; line-height: 50px; margin-top: 0px } + #image_dialog_size_1 a { height: 50px; width: 50px; line-height: 50px; margin-top: 0 } #image_dialog_size_2 a { height: 70px; width: 70px; line-height: 70px; margin-top:-10px } #image_dialog_size_3 a { height: 90px; width: 90px; line-height: 90px; margin-top:-20px } } #content { - padding: 0px; + padding: 0; } } #upload_image_area, #upload_resource_area { @@ -948,7 +948,7 @@ body.dialog form { } #dialog_main { .pagination { - margin: 0px; + margin: 0; position: fixed; bottom: 5px; right: 12px; @@ -958,8 +958,8 @@ body.dialog form { width: 100%; padding-bottom: 40px; ul { - margin: 0px 12px 0px 12px; - padding: 0px; + margin: 0 12px 0 12px; + padding: 0; li { cursor: pointer; line-height: 24px; @@ -1010,7 +1010,7 @@ body.dialog form { #link_title { margin-top: 12px; label { - margin: 3px 0px 0px 0px !important; + margin: 3px 0 0 0 !important; width: 130px; display: block; float: left; @@ -1042,7 +1042,7 @@ ul#menu.reordering_menu li a { position: relative; .button, .button-wrapper { position: absolute; - right: 0px; + right: 0; top: 2px; } } @@ -1068,7 +1068,7 @@ a:focus { outline: none; } a img { - border: 0px none; + border: 0 none; } @@ -1097,7 +1097,7 @@ ul#page_parts_controls { margin: 0 3px; a { - border: 0px none; + border: 0 none; } } } @@ -1119,8 +1119,8 @@ ul#page_parts_controls { #dialog_frame { width:952px; height:460px; - padding: 0px; - border: 0px solid #F2F1ED; + padding: 0; + border: 0 solid #F2F1ED; } .visual_editor_hideables { display: none; @@ -1132,7 +1132,7 @@ ul#page_parts_controls { margin-bottom: 45px; } .dialog .dialog_area > div > .field label:first-child, .dialog .dialog_area > div > label:first-child { - margin-top: 0px; + margin-top: 0; } .visual_editor_dialog .form-actions, #content.form-actions.dialog-form-actions, .ui-dialog .form-actions { margin-top: 16px; @@ -1155,7 +1155,7 @@ ul#page_parts_controls { top: 4px; } .visual_editor_dialog .form-actions, .ui-dialog .form-actions { - border: 0px none; + border: 0 none; border-top: 1px solid #E8E8E8; } #dialog_iframe { @@ -1163,12 +1163,12 @@ ul#page_parts_controls { } #dialog_container #content .form-actions, .ui-dialog .form-actions, .dialog_container .form-actions { position: absolute; - bottom: 0px; - left: 0px; - right: 0px; - border-right: 0px; - border-left: 0px; - border-bottom: 0px; + bottom: 0; + left: 0; + right: 0; + border-right: 0; + border-left: 0; + border-bottom: 0; width: auto; } #dialog_container.dialog.iframed #content .form-actions { @@ -1178,16 +1178,16 @@ body.dialog #content .search_form { float: right; width: auto !important; min-width: 300px; - margin-bottom: 0px; + margin-bottom: 0; } #existing_image_area { padding-top:6px; } .ui-dialog div.field { - margin: 0px 10px; + margin: 0 10px; } #dialog_container, .dialog_container { - margin: 0px; + margin: 0; padding: 12px; } ul#image_grid, .pagination_container > ul#image_grid { @@ -1232,8 +1232,8 @@ ul#image_grid li span.actions { width: 100%; } ul.checkboxes { - margin: 0px; - padding: 0px; + margin: 0; + padding: 0; } ul.checkboxes li { list-style: none; @@ -1243,7 +1243,7 @@ ul.checkboxes li { margin-right: 6px; } .label_inline_with_link a { - border: 0px none; + border: 0 none; margin-top: 19px; line-height: 17px; float: left; @@ -1257,7 +1257,7 @@ ul.checkboxes li { width:auto; } #new_page_part_dialog .field { - padding: 0px 10px; + padding: 0 10px; } .hide-overflow { overflow: hidden; @@ -1271,25 +1271,25 @@ ul.checkboxes li { width: 98%; } .ui-dialog .visual_editor_dialog_paste .field, .ui-dialog .visual_editor_dialog_paste .field textarea { - margin: 0px 0px 45px 0px; + margin: 0 0 45px 0; height: 300px; } input.button, a.button, #content a.button, span.button-wrapper, span.button-wrapper input { cursor:pointer; background: #22a7f2; color: white; - padding: 0px 14px 0px 14px; + padding: 0 14px 0 14px; font-size: 14px; line-height: 24px; height: 24px; display: inline-block; - border: 0px none; - margin-top: 0px; - margin-bottom: 0px; + border: 0 none; + margin-top: 0; + margin-bottom: 0; } /* for those pesky IE browsers */ span.button-wrapper { - padding: 0px; + padding: 0; } span.button-wrapper input { display: inherit; @@ -1322,7 +1322,7 @@ input.button.close_dialog:active, a.button.close_dialog:active, #content a.butto .form-actions a.confirm-delete, #content .form-actions a.confirm-delete { background: #ee1100; position: absolute; - right: 0px; + right: 0; } .form-actions a.confirm-delete:hover, #content .form-actions a.confirm-delete:hover { background: #ff3322; @@ -1344,7 +1344,7 @@ input.button.close_dialog:active, a.button.close_dialog:active, #content a.butto } .hemisquare { padding: 10px; - margin: 0px 0px; + margin: 0 0; margin-left: 10px; float: left; width: 45%; @@ -1352,23 +1352,23 @@ input.button.close_dialog:active, a.button.close_dialog:active, #content a.butto float: right; } input, textarea, select { - margin: 7px 0px; + margin: 7px 0; } textarea { - margin-bottom: 0px; + margin-bottom: 0; } label { - margin: 0px; + margin: 0; } small { font-size: 0.9em; } } #content .hemisquare h2 { - margin-top: 0px; + margin-top: 0; } #content .hemisquare .field { - margin: 0 0 20px 0px; + margin: 0 0 20px 0; width: 98%; } .label_with_help { @@ -1406,11 +1406,11 @@ input.button.close_dialog:active, a.button.close_dialog:active, #content a.butto /* Locale picker */ #switch_locale_picker { - margin: 0px; - padding: 0px; + margin: 0; + padding: 0; li { float: left; - padding: 0px; + padding: 0; margin: 0; list-style: none; a { @@ -1507,7 +1507,7 @@ input.button.close_dialog:active, a.button.close_dialog:active, #content a.butto } #content #records .title .preview a.locale { - border-bottom: 0px none; + border-bottom: 0 none; } /* AJAX pagination */ .pagination_container { From 75afa2a3dc94f6abadcfc5a344f469d1e54035ff Mon Sep 17 00:00:00 2001 From: Anita Date: Sun, 29 Dec 2024 12:28:24 +0800 Subject: [PATCH 14/74] css cleanup: remove recent_activities and recent_inquiries from stylesheets. --- .../refinery/sections/_layout.scss | 25 +++---------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/core/app/assets/stylesheets/refinery/sections/_layout.scss b/core/app/assets/stylesheets/refinery/sections/_layout.scss index 3124bd9e0c7..fcae58b117b 100644 --- a/core/app/assets/stylesheets/refinery/sections/_layout.scss +++ b/core/app/assets/stylesheets/refinery/sections/_layout.scss @@ -486,14 +486,12 @@ pre { float: right; } #records { - > ul, > #recent_activity > ul, > #recent_inquiries > ul, - .pagination_container > ul, .pagination_frame > ul { + > ul, .pagination_container > ul, .pagination_frame > ul { margin-left: 0; margin-top: 0; padding-left: 0; } - > ul li, > #recent_activity > ul li, > #recent_inquiries > ul li, - .pagination_container > ul li, .pagination_frame > ul { + > ul li, .pagination_container > ul li, .pagination_frame > ul { list-style: none; padding: 0 5px; vertical-align: top; @@ -505,13 +503,7 @@ pre { max-height: 35px; } } -#recent_activity li a, #recent_inquiries li a { - overflow: hidden; - white-space: nowrap; - -o-text-overflow: ellipsis; - -ms-text-overflow: ellipsis; - text-overflow: ellipsis; -} + #content #records { > ul li .actions a, .pagination_container > ul li .actions a { line-height: 29px; @@ -560,17 +552,8 @@ pre { ul#sortable_list, ul.sortable_list { margin-top: 6px; } - > #recent_activity, > #recent_inquiries { - float: left; - width: 48%; - } - > #recent_inquiries { - margin-left: 21px; - } -} -#records.one_list > #recent_activity, #records.one_list > #recent_inquiries { - width: 100%; } + #pagination ul a:hover, #pagination .on { background: image_url('refinery/hover-gradient.jpg') repeat-x bottom #D4D4C6; } From b38d27750bbaf0e91c9db8439c7fad9b0929c622 Mon Sep 17 00:00:00 2001 From: Anita Date: Sun, 5 Jan 2025 11:37:15 +0800 Subject: [PATCH 15/74] use mixins for common styles between image views --- .../stylesheets/refinery/mixins/_images.scss | 52 ++++++++++++++++ .../assets/stylesheets/refinery/refinery.scss | 2 + .../refinery/sections/_layout.scss | 62 +++++-------------- 3 files changed, 69 insertions(+), 47 deletions(-) create mode 100644 core/app/assets/stylesheets/refinery/mixins/_images.scss diff --git a/core/app/assets/stylesheets/refinery/mixins/_images.scss b/core/app/assets/stylesheets/refinery/mixins/_images.scss new file mode 100644 index 00000000000..a2c67bebbbb --- /dev/null +++ b/core/app/assets/stylesheets/refinery/mixins/_images.scss @@ -0,0 +1,52 @@ +@mixin indexImage { + .alt {font-style: italic} + .preview {color: #A3A093} // the filename + .title {font-weight: bold;} +} + +@mixin gridImage { + @include indexImage; + padding: 0; + margin: 0; + line-height: 1.2; + + img ~ * { + display: inline-block; + margin-right: 2px; + } +} + +@mixin listImage { + @include indexImage; + margin: 0 12px 12px 0; + padding: 0; + overflow: hidden; + .actions { + top: auto; + bottom: 0; + right: 0; + width: 100%; + } +} + +@mixin imageIndexLayout { + width: 100%; + padding: 0; + @content; +} + +// apply to #image_grid +@mixin imageGridLayout { + @include imageIndexLayout { + display: grid; + grid-template-columns: repeat(4, 150px); + grid-template-rows: auto; + gap: 15px;} +} + +// apply to #image_list (note: 1. id, 2. not plural +@mixin imageListLayout { + @include imageIndexLayout { + margin: 10px 0 15px 0; + } +} diff --git a/core/app/assets/stylesheets/refinery/refinery.scss b/core/app/assets/stylesheets/refinery/refinery.scss index 408382438ef..e96de19f354 100644 --- a/core/app/assets/stylesheets/refinery/refinery.scss +++ b/core/app/assets/stylesheets/refinery/refinery.scss @@ -9,6 +9,7 @@ @import 'mixins/icon'; @import 'mixins/rounded'; @import 'mixins/labels'; +@import 'mixins/images'; @import 'plugins/ui'; @@ -20,4 +21,5 @@ @import 'sections/layout'; + @import 'site_bar'; diff --git a/core/app/assets/stylesheets/refinery/sections/_layout.scss b/core/app/assets/stylesheets/refinery/sections/_layout.scss index fcae58b117b..de52f249622 100644 --- a/core/app/assets/stylesheets/refinery/sections/_layout.scss +++ b/core/app/assets/stylesheets/refinery/sections/_layout.scss @@ -285,7 +285,7 @@ noscript .flash.flash_alert a:hover { float: right; margin: 3px 3px; line-height: inherit; - border-bottom: 0px none; + border-bottom: 0 none; } } } @@ -499,9 +499,6 @@ pre { line-height: 35px; position: relative; } - > #recent_activity > ul li, > #recent_inquiries > ul li { - max-height: 35px; - } } #content #records { @@ -774,10 +771,6 @@ label.stripped { #records.tree li.loading > div > span.icon { @include icon('spinner'); } -#image_grid .actions a img { - padding: 4px 4px 1px 4px; - vertical-align: top; -} .actions a.reorder_anchor:hover { background: inherit !important; @@ -1173,47 +1166,19 @@ body.dialog #content .search_form { margin: 0; padding: 12px; } -ul#image_grid, .pagination_container > ul#image_grid { - width: 100%; - padding: 0px; - margin: 10px 0px 15px 0px; -} -ul#image_grid li, .pagination_container > ul#image_grid li { - position: relative; - float: left; - margin: 0px 12px 12px 0px; - padding: 0px; - width: 149px; - height: 186px; - max-width: 149px; - max-height: 186px; - text-align: center; - overflow: hidden; -} -ul#image_grid li.image_3 { - margin-left: 0px; - margin-right: 0px; -} -#records ul#image_grid li .actions { - top: auto; - bottom: 0px; -} -#records ul#image_grid li .actions a { - line-height: 24px; -} -ul#image_grid li.row-end { - margin-right: 0px; - float: right; -} -ul#image_grid li a { - border: 0 none; -} -ul#image_grid li p { - margin: 3px 0; +#records ul#image_grid { + @include imageGridLayout; + li { + @include gridImage + } } -ul#image_grid li span.actions { - width: 100%; +#records ul#images_list { + @include imageListLayout; + li { + @include listImage + } } + ul.checkboxes { margin: 0; padding: 0; @@ -1489,6 +1454,9 @@ input.button.close_dialog:active, a.button.close_dialog:active, #content a.butto .fa-stack .fa-comment {color: $icon_locale_colour; font-size: 24px;} } +#content .locales a { + border-bottom: none; +} #content #records .title .preview a.locale { border-bottom: 0 none; } From 2ed8b6385803bdf88e43dbfba3a7f9ae9cfd07fa Mon Sep 17 00:00:00 2001 From: Anita Date: Sun, 5 Jan 2025 11:46:59 +0800 Subject: [PATCH 16/74] define ImageObject for use in Admin image views return ImageObjects from Refinery::Admin::ImagesController#index action unify code for grid_view and list_view into _image.html.erb add view_objects directory to engine autoload_path --- .../refinery/admin/images_controller.rb | 8 +++ images/app/viewobjects/image_object.rb | 56 +++++++++++++++++++ .../refinery/admin/images/_grid_view.html.erb | 16 ------ .../refinery/admin/images/_image.html.erb | 31 ++++++++++ .../refinery/admin/images/_images.html.erb | 21 ++++++- .../admin/images/_list_view_image.html.erb | 30 ---------- images/lib/refinery/images/engine.rb | 2 +- 7 files changed, 115 insertions(+), 49 deletions(-) create mode 100644 images/app/viewobjects/image_object.rb delete mode 100644 images/app/views/refinery/admin/images/_grid_view.html.erb create mode 100644 images/app/views/refinery/admin/images/_image.html.erb delete mode 100644 images/app/views/refinery/admin/images/_list_view_image.html.erb diff --git a/images/app/controllers/refinery/admin/images_controller.rb b/images/app/controllers/refinery/admin/images_controller.rb index 3caf6a93a8e..958199a4b8d 100644 --- a/images/app/controllers/refinery/admin/images_controller.rb +++ b/images/app/controllers/refinery/admin/images_controller.rb @@ -16,6 +16,14 @@ def new @url_override = refinery.admin_images_path(dialog: from_dialog?) end + def index + search_all_images if searching? + paginate_all_images + + @images = @images.map { |image| ImageObject.new(image, view_context) } + end + + # This renders the image insert dialog def insert self.new if @image.nil? diff --git a/images/app/viewobjects/image_object.rb b/images/app/viewobjects/image_object.rb new file mode 100644 index 00000000000..fa5cdebe7e9 --- /dev/null +++ b/images/app/viewobjects/image_object.rb @@ -0,0 +1,56 @@ +class ImageObject + include Refinery::TranslationHelper + include Refinery::ImageHelper + + attr_reader :image, :context, :i18n_scope + attr_writer :title, :alt, :filename, :translations, :edit_attributes, :delete_attributes, :preview_attributes + delegate_missing_to :image + + Action = Struct.new('Action', :href, :title, :text, :data, :options, keyword_init: true) + + def initialize(image, context, scope=nil) + @image = image + @context = context + @i18n_scope = scope || 'refinery.admin.images' + end + + def title + translated_field(image, :title) + end + + def alt + translated_field(image, :alt) + end + + def img_element + image_fu image, '149x149#c', title: ::I18n.t('edit_title', scope: i18n_scope, title: image.image_title || image.image_alt) + end + + def filename + image.image_name + end + def locales_with_titles + image.translations + .reject { |i| i.image_title.blank? } + .map(&:locale).sort_by { |t| Refinery::I18n.frontend_locales.index(t.to_sym) } + end + + def edit_action(options = {}) + Action.new(href: context.edit_admin_image_path(image, switch_locale: options.delete(:switch_locale)), + title: I18n.t('.edit', scope: i18n_scope), + text: I18n.t('.edit_title', title: image.title, scope: i18n_scope), + options: options) + end + + def preview_action(options = {}) + Action.new(href: image.url, title: I18n.t('.view_live_html', scope: i18n_scope), options: options) + end + + def delete_action(options = {}) + Action.new(href: context.admin_image_path(image), + title: I18n.t('.delete', scope: i18n_scope), + data:{ confirm: I18n.t('message', scope: 'refinery.admin.delete', title: image.title) }, + options: { class: [:delete, 'confirm-delete'], **options }) + end + +end diff --git a/images/app/views/refinery/admin/images/_grid_view.html.erb b/images/app/views/refinery/admin/images/_grid_view.html.erb deleted file mode 100644 index eaf40f540ba..00000000000 --- a/images/app/views/refinery/admin/images/_grid_view.html.erb +++ /dev/null @@ -1,16 +0,0 @@ -
    - <% @images.each_with_index do |image, index| -%> -
  • - <%= image_fu image, '149x149#c', title: image.title %> - - - <%= action_icon :preview, image.url, t('view_live_html', scope: 'refinery.admin.images') %> - <%= action_icon :edit, refinery.edit_admin_image_path(image), t('edit', scope: 'refinery.admin.images') %> - <%= action_icon :delete, refinery.admin_image_path(image, params.to_unsafe_h.slice(:page)), t('delete', scope: 'refinery.admin.images'), class: 'confirm-delete', - data: { confirm: t('message', scope: 'refinery.admin.delete', title: image.title) } %> - <%= action_icon :info, '#', "Title: #{image.title} Alt text: #{image.alt}" %> - - -
  • - <% end -%> -
diff --git a/images/app/views/refinery/admin/images/_image.html.erb b/images/app/views/refinery/admin/images/_image.html.erb new file mode 100644 index 00000000000..07c5f26e9a3 --- /dev/null +++ b/images/app/views/refinery/admin/images/_image.html.erb @@ -0,0 +1,31 @@ +
  • + <% edit_action = image.edit_action %> + <% preview_action = image.preview_action %> + <% delete_action = image.delete_action %> + + <% view_identity_elements = [] %> + <% case Refinery::Images.preferred_image_view %> + <% when 'list' %> + <% view_identity_elements = [ + link_to(image.title, edit_action.href, class: :title, title: edit_action.text), + tag.span(image.filename, class: :filename) + ] %> + <% when 'grid' %> + <% view_identity_elements = [ + link_to(image.img_element, edit_action.href, title: edit_action.text), + tag.span(image.title, class: "title", title: "t('.title')") + ] %> + <% else %> + <% Rails.logger.error "Undefined image view #{Refinery::Images.preferred_image_view} " %> + <% end %> + <%= view_identity_elements.compact.join(' ').html_safe %> + + <%= image.alt unless image.alt == image.title %> + <%= edit_in_locales(image, image.locales_with_titles) %> + + + <%= action_icon :preview, preview_action.href, preview_action.title, preview_action.options %> + <%= action_icon :edit, edit_action.href, edit_action.title, edit_action.options %> + <%= action_icon :delete, delete_action.href, delete_action.title, **delete_action.options %> + +
  • diff --git a/images/app/views/refinery/admin/images/_images.html.erb b/images/app/views/refinery/admin/images/_images.html.erb index 64ed047d1f7..7b5faf6bcfe 100644 --- a/images/app/views/refinery/admin/images/_images.html.erb +++ b/images/app/views/refinery/admin/images/_images.html.erb @@ -1,2 +1,19 @@ -<%= will_paginate @images if Refinery::Admin::ImagesController.pageable? %> -<%= render "#{Refinery::Images.preferred_image_view}_view" %> +<%#= will_paginate @images if Refinery::Admin::ImagesController.pageable? %> +<% images = @images %> +<% view = Refinery::Images.preferred_image_view %> +<% if view == 'grid' + groups = [[Date.today, images]] + group_header = nil + else + groups = group_by_date(images, :updated_at) + + end +%> + +<%= tag.ul id: "images_#{view}", classes: [*%w(clearfix pagination_frame images_list), pagination_selector] %> +<% groups.each do |(date, group)| %> +

    <%= l(Date.parse(date.to_s), format: :long ) %>

    + <%= tag.ul class: :images_list do %> + <%= render partial: 'refinery/admin/images/image', collection: group, view: "#{view}_view" %> + <% end %> +<% end %> diff --git a/images/app/views/refinery/admin/images/_list_view_image.html.erb b/images/app/views/refinery/admin/images/_list_view_image.html.erb deleted file mode 100644 index cbe228c612b..00000000000 --- a/images/app/views/refinery/admin/images/_list_view_image.html.erb +++ /dev/null @@ -1,30 +0,0 @@ -
  • - - <%= translated_field(list_view_image, :title) %> - - - <% if Refinery::I18n.frontend_locales.many? %> - - <% list_view_image.translations.sort_by{ |t| Refinery::I18n.frontend_locales.index(t.locale)}.each do |translation| %> - <% if translation.image_title.present? %> - <%= link_to refinery.edit_admin_image_path(list_view_image, switch_locale: translation.locale), - class: 'locale', title: translation.locale.upcase do %> - -
    - <%= locale_text_icon(translation.locale.upcase) %> -
    - <% end %> - <% end %> - <% end %> -
    - <% end %> - - <%= list_view_image.image_name %> - - - <%= action_icon :preview, list_view_image.url, t('view_live_html', scope: 'refinery.admin.images') %> - <%= action_icon :edit, refinery.edit_admin_image_path(list_view_image), t('edit', scope: 'refinery.admin.images') %> - <%= action_icon :delete, refinery.admin_image_path(list_view_image), t('delete', scope: 'refinery.admin.images'), class: 'confirm-delete', - data: { confirm: t('message', scope: 'refinery.admin.delete', title: list_view_image.title)} %> - -
  • diff --git a/images/lib/refinery/images/engine.rb b/images/lib/refinery/images/engine.rb index c910402d0df..b3d9fe624fe 100644 --- a/images/lib/refinery/images/engine.rb +++ b/images/lib/refinery/images/engine.rb @@ -6,7 +6,7 @@ class Engine < ::Rails::Engine isolate_namespace Refinery engine_name :refinery_images - config.autoload_paths += %W( #{config.root}/lib ) + config.autoload_paths += %W( #{config.root}/lib #{config.root}/refinery/images/view_objects ) initializer 'attach-refinery-images-with-dragonfly', :before => :finisher_hook do |app| ::Refinery::Dragonfly.configure!(::Refinery::Images) From c4a4bccf3255e735a2391230ae696becfe9b83fc Mon Sep 17 00:00:00 2001 From: Anita Date: Sun, 5 Jan 2025 11:50:00 +0800 Subject: [PATCH 17/74] cleanup stylesheets: change 0px to unitless 0. --- core/app/assets/stylesheets/refinery/plugins/_ui.scss | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/core/app/assets/stylesheets/refinery/plugins/_ui.scss b/core/app/assets/stylesheets/refinery/plugins/_ui.scss index 6921d0d6ee0..c1774ea135a 100644 --- a/core/app/assets/stylesheets/refinery/plugins/_ui.scss +++ b/core/app/assets/stylesheets/refinery/plugins/_ui.scss @@ -52,7 +52,10 @@ span { display: block; margin: 1px; - background: transparent image_url('refinery/cross.png') top left no-repeat; + background-image: image_url('refinery/cross.png'); + background-repeat: no-repeat; + background-position: top left; + background-color: transparent; text-indent:-10000px; } } @@ -84,9 +87,9 @@ } } .ie7 .ui-tabs-hide { - display: none !important !important; - height: auto !important !important; - width: auto !important !important; + display: none !important; + height: auto !important; + width: auto !important; position: static; } .ui-tabs li.ui-state-default { From 7be64ac4b602c279dc95dfefc88baa4519047d73 Mon Sep 17 00:00:00 2001 From: Anita Date: Mon, 6 Jan 2025 10:17:15 +0800 Subject: [PATCH 18/74] use recommended dragonfly call for stripping images instead of deprecated "process.convert" --- images/lib/refinery/images/configuration.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/images/lib/refinery/images/configuration.rb b/images/lib/refinery/images/configuration.rb index 440a7ea4cdc..e9ea01750c3 100644 --- a/images/lib/refinery/images/configuration.rb +++ b/images/lib/refinery/images/configuration.rb @@ -6,7 +6,7 @@ module Images config_accessor :max_image_size, :pages_per_dialog, :pages_per_admin_index, :pages_per_dialog_that_have_size_options, :user_image_sizes, :user_image_ratios, - :image_views, :preferred_image_view, + :image_views, :preferred_image_view, :admin_image_sizes, :allowed_mime_types, :allowed_mime_types_msg self.max_image_size = 5_242_880 @@ -23,6 +23,10 @@ module Images '4/3': '1.333', '1:1': 1 } + + self.admin_image_sizes = { + grid: '149x149#c' + } self.allowed_mime_types = %w[image/jpeg image/png image/gif image/tiff] self.allowed_mime_types_msg = self.allowed_mime_types.to_sentence(last_word_connector: ' or ') self.image_views = [:grid, :list] @@ -35,7 +39,7 @@ module Images # Dragonfly processor to strip image of all profiles and comments (imagemagick conversion -strip) self.dragonfly_processors = [{ name: :strip, - block: -> (content) { content.process!(:convert, '-strip') } + block: -> (content) { ::Dragonfly::ImageMagick::Commands.convert(content, '-strip') } }] end From 6257bd2811b2f11fbadc5cc2d17682b35b5ba16f Mon Sep 17 00:00:00 2001 From: Anita Date: Tue, 7 Jan 2025 11:27:25 +0800 Subject: [PATCH 19/74] scss style reformatted all over to add blank lines and spaces. --- .../refinery/sections/_layout.scss | 408 +++++++++++++++--- 1 file changed, 356 insertions(+), 52 deletions(-) diff --git a/core/app/assets/stylesheets/refinery/sections/_layout.scss b/core/app/assets/stylesheets/refinery/sections/_layout.scss index de52f249622..82c5080f44d 100644 --- a/core/app/assets/stylesheets/refinery/sections/_layout.scss +++ b/core/app/assets/stylesheets/refinery/sections/_layout.scss @@ -6,9 +6,11 @@ p { font-size: 14px; line-height: 18px; } + html { height: 100%; } + body { height: 100%; margin: -52px 0; @@ -19,12 +21,15 @@ body { color: #41403C; background: rgb(217, 217, 217); } + acronym { cursor: help; } + label[for] { cursor: pointer; } + #admin_container, #login_container { font-size: 13px; @@ -33,29 +38,35 @@ label[for] { position: relative; margin-top: 15px; @include rounded($standard_rounding); + a { color: #41403C; text-decoration: none; } + #menu { a:hover { border-bottom: 0; } } + .hidden { display: none; } } + #login_container { background: white; width: 620px; } + #admin_container { width: $admin_width; margin: -5px auto 0 auto; height: 100%; @include faux_columns_2($menu_width, transparent, white); } + #page_container { background-color: white; padding: 1.5em 1em; @@ -65,48 +76,59 @@ label[for] { @include box-sizing(border-box); @include right-rounded($standard_rounding); } + #content .field, #content .form-actions { position: relative; } + input.widest, textarea.widest { width: 99%; } + select.widest { max-width: 99%; } + #records { float: left; width: 67.7%; } + #actions { float: right; width: 30%; padding-left: 15px; padding-top: 0; + ul { margin: 0 0 18px 0; padding: 0; list-style: none; + &#current_locale { margin-top: 30px; } } + li { margin-bottom: 10px; background-color: $action-background-colour; border: 1px solid $action-border-colour; padding: 9px; display: block; + &:empty { display: none } } } + #sort-status { padding: 5px 5px 5px 0; } + hr { border: 0; border-top: 1px solid #484743; @@ -119,77 +141,92 @@ hr { margin-top: 5px; margin-bottom: 5px; border: 1px solid red; + h2 { color: red; text-transform: none; display: none; } + p { font-weight: normal; } } + .fieldWithErrors input, .fieldWithErrors textarea { border: 1px solid red !important; background-color: #FFECF0 !important; } + .fieldWithErrors iframe, .fieldWithErrors .visual_editor_box { border-color: red !important; } + #message, .flash { padding: 8px 8px 8px 30px; margin-bottom: 15px; position: relative; } + .flash_notice, .flash_message { border: 1px solid #00A017; color: #00A017; @include icon('check-circle', green) } + .flash_notice, .flash_notice * { color: #00A017; } + .flash_error, .flash_alert { border: 1px solid #A00027; color: #A00027; @include icon('question-circle', red) } + .flash.flash_notice #flash_close, .flash.flash_error #flash_close, .flash.flash_alert #flash_close { text-transform: lowercase; @include icon('times-circle', $icon_done_colour, 1.2em) } + .flash.flash_message { background: #E0F5E0; padding: 9px; position: relative; margin-bottom: 32px; + h2 { margin-top: 12px; } } + .flash.flash_message.flash_message, .flash_message * { color: #262719; font-size: 14px; } + .flash a, .flash a:hover { color: #e20003; border-bottom-color: #e20003; } + .flash.flash_error a, .flash.flash_error a:hover, .flash.flash_alert a, .flash.flash_alert a:hover { display: none; } + noscript .flash.flash_error a, noscript .flash.flash_error a:hover, noscript .flash.flash_alert a, @@ -197,6 +234,7 @@ noscript .flash.flash_alert a:hover { display: inline; font-weight: bold; } + .flash #flash_close { background: none; border: none; @@ -206,25 +244,31 @@ noscript .flash.flash_alert a:hover { font-size: 1em; margin-top: 3px; } + #content .visual_editor_box a, #content .ui-tabs a { border-bottom: 0 none; } + .index #content, .splash #content { background-color: white; background-repeat: repeat-y; } + #content { padding: 0; background-color: white; + a { border-bottom: 1px dotted #727272; + &.locale { border-bottom: 0; - display:inline; + display: inline; } } + h1 { font-size: 18px; font-weight: lighter; @@ -232,18 +276,21 @@ noscript .flash.flash_alert a:hover { border-bottom: 1px solid #99998B; padding-bottom: 10px; } + h2 { font-size: 18px; - color:#41403c; + color: #41403c; margin-bottom: 15px; margin-top: 10px; font-weight: bold; } + form.edit_image { width: 30%; float: right; margin-top: 3em } + #existing_image { float: left; width: 60%; @@ -279,6 +326,7 @@ noscript .flash.flash_alert a:hover { } } } + .actions { a { display: block; @@ -289,46 +337,56 @@ noscript .flash.flash_alert a:hover { } } } + .less-important { color: #727272; } + header, footer, nav { display: block; } + #page_container .login #page h1 { margin: 0; padding: 5px; font-size: 20px; - line-height:22px + line-height: 22px } + #login_container { #page_container { background: transparent; width: 100%; + div.remember_me label, label.inline { display: inline; } + div.actions { margin-top: 12px; } + div.remember_me { width: 300px; float: left; } + div.forgot_password { float: right; width: 250px; } } + header { background: #eaeaea; height: auto; float: none; width: 100%; @include top-rounded($standard_rounding); + h1 { color: #41403c; vertical-align: middle; @@ -340,53 +398,67 @@ nav { padding-top: 15px; } } + label { margin-top: 0; } + label, a { font-size: 14px; } + .field { margin-bottom: 20px; + &.remember_me, &.forgot_password { margin-bottom: 0; margin-top: 0; } + &.forgot_password { text-align: right; } + &.remember_me label { margin-top: 20px; } } + #flash_container, .errorExplanation { margin-bottom: 12px; } + /* Works in Firefox, Safari, Chrome, IE8+ */ input.larger { background: image_url('refinery/text_field_background.png') repeat-x white; height: 31px; } + input.larger:focus { background-position: 0 -41px; } + .fieldWithErrors input.larger { background-position: 0 -82px; border: 1px solid red; } } + div.field.checkbox_access { margin-top: 20px; + ul.checkboxes li { margin-top: 3px; } + .label_with_help a { font-weight: normal; } } + #menu { display: block; margin: 45px 0 0 0; @@ -394,18 +466,22 @@ div.field.checkbox_access { position: absolute; background: transparent; } + header { float: left; margin-bottom: 0; width: $menu_width; + a, a:hover { border-bottom: 0 none; } + #logo { position: absolute; right: 20px; top: 25px; } + h1, h1 a { color: white; font-size: 20px; @@ -414,6 +490,7 @@ header { padding-bottom: 4px; margin-top: 0; } + p { color: white; font-size: 90%; @@ -421,15 +498,19 @@ header { margin: 0; } } + #menu { display: block; position: relative; + &.ui-sortable a { cursor: move; } + &.ui-sortable-disabled a { cursor: pointer; } + a { @include box-sizing(border-box); display: block; @@ -440,10 +521,12 @@ header { color: rgba(7, 112, 173, 1); font-weight: normal; position: relative; + &.active, &:hover, &:focus { background-color: rgba(37, 169, 244, 1); color: white; } + &.active { margin-left: -0.5em; font-weight: bold; @@ -462,6 +545,7 @@ header { float: right; @include box-sizing(content-box); } + a#menu_reorder_done { background: white; } @@ -471,26 +555,32 @@ pre { margin: 0; padding: 0; } + .preview { color: #A3A093; } + #site_link { display: block; color: #C2C2B3; float: left; } + #site_link:hover { text-decoration: underline; } + .filter { float: right; } + #records { - > ul, .pagination_container > ul, .pagination_frame > ul { + > ul, .pagination_container > ul, .pagination_frame > ul { margin-left: 0; margin-top: 0; padding-left: 0; } + > ul li, .pagination_container > ul li, .pagination_frame > ul { list-style: none; padding: 0 5px; @@ -506,12 +596,14 @@ pre { line-height: 29px; } } + #records { ul { &.clickable { li { padding: 0; margin-bottom: 0; + a { padding: 0 5px; vertical-align: top; @@ -523,29 +615,36 @@ pre { } } } + .left-column { float: left; width: 65%; + img { vertical-align: bottom; margin-top: 1px; margin-right: 5px; } } + .right-column { float: right; width: 34%; text-align: right; } + .on { background-color: #EAEAEA; } + .off, .on-hover { background-color: white; } + ul.empty { display: none; } + ul#sortable_list, ul.sortable_list { margin-top: 6px; } @@ -554,76 +653,96 @@ pre { #pagination ul a:hover, #pagination .on { background: image_url('refinery/hover-gradient.jpg') repeat-x bottom #D4D4C6; } + #records.tree ul li ul, .tree ul li ul { padding: 0; } + #records.tree ul li, .tree ul li { margin: 0; padding: 4px 0 0 40px; background: image_url('refinery/branch.gif') no-repeat 15px 0; } + #records.tree li.record ul { margin-left: 0; } + #records.tree .on-hover, #pagination ul.tree a:hover, #pagination .tree .on { background: image_url('refinery/branch.gif') no-repeat 15px 0; } + #records.tree ul li.branch_start, .tree ul li.branch_start { background-image: image_url('refinery/branch-start.gif'); } + #records.tree ul li.branch_end, .tree ul li.branch_end { background-image: image_url('refinery/branch-end.gif'); } + #records.tree li { line-height: 25px; } + #records.tree li span.spacing, .tree li span.spacing { display: none; } + #records.tree ul li > div:hover, .tree ul li > div:hover { background-color: #EAEAEA; } + #sortable_list.reordering > li, .sortable_list.reordering > li { cursor: move; } + #records h2, #actions h2 { margin-top: 0; } + .pagination { background-color: #C9DAE2; padding: 5px 5px 4px 5px; margin: 10px 0; + em { font-weight: bold; font-style: normal; padding: 0 6px; } } + .pagination { .disabled { color: #A8B9C1; } - a, #content a, .current, .disabled, em { + + a, #content a, .current, .disabled, em { padding: 7px; line-height: 20px; border-bottom: 0 none; } + .current, a:hover, em { background: #A8B9C1; } } + textarea { line-height: 20px; padding: 5px; } + .field-couple { margin-bottom: 20px; } + .submit { border: inherit; width: auto; height: 25px; } + //label, .label_with_help { label { margin-bottom: 5px; @@ -631,6 +750,7 @@ label { margin-top: 20px; display: block; } + label.input_label { font-size: inherit; margin-bottom: inherit; @@ -638,11 +758,13 @@ label.input_label { font-weight: normal; margin-top: inherit; } + small label { font-size: inherit; font-weight: inherit; display: inherit; } + label.stripped { float: none; display: inline; @@ -651,40 +773,48 @@ label.stripped { margin: 0; padding: 0; } + #body_field { float: left; width: 60%; } + .no_side_body { width: 72% !important; } + #side_body_field { float: left; width: 38%; margin-left: 18px; } + #body_field textarea, #side_body_field textarea { width: 99%; } + .record .title span { line-height: 30px; } #records.files .record .title, #dialog_main #resource_file_area .pages_list ul li a.page_link { - display:inline-block; - padding-left:24px; - min-height:16px; - background-repeat:no-repeat; - background-position:left; + display: inline-block; + padding-left: 24px; + min-height: 16px; + background-repeat: no-repeat; + background-position: left; @include icon('file-o', blue) } -#dialog_main #resource_file_area .pages_list ul li a.page_link{ - background-position:5px center; - display:block; + +#dialog_main #resource_file_area .pages_list ul li a.page_link { + background-position: 5px center; + display: block; } + #records.files .record .title.pdf, #dialog_main #resource_file_area .pages_list ul li a.page_link.pdf { @include icon('file-pdf-o') } + #records.files .record .title.jpg, #records.files .record .title.gif, #records.files .record .title.jpeg, @@ -701,18 +831,23 @@ label.stripped { #records.files .record .title.doc, #records.files .record .title.pages, #records.files .record .title.docx, #dialog_main #resource_file_area .pages_list ul li a.page_link.doc, #dialog_main #resource_file_area .pages_list ul li a.page_link.docx, #dialog_main #resource_file_area .pages_list ul li a.page_link.pages { @include icon('file-word-o'); } + #records.files .record .title.ppt, #records.files .record .title.keynote, #dialog_main #resource_file_area .pages_list ul li a.page_link.ppt, #dialog_main #resource_file_area .pages_list ul li a.page_link.keynote { @include icon('file-powerpoint-o'); } + #records.files .record .title.xls, #records.files .record .title.numbers, #dialog_main #resource_file_area .pages_list ul li a.page_link.xls, #dialog_main #resource_file_area .pages_list ul li a.page_link.numbers { @include icon('file-excel-o'); } + #records.files .record .title.zip, #records.files .record .title.rar, #dialog_main #resource_file_area .pages_list ul li a.page_link.zip, #dialog_main #resource_file_area .pages_list ul li a.page_link.rar { @include icon('file-zip-o'); } + #records.files .record .title.mp3, #records.files .record .title.wav, #records.files .record .title.aiff, #records.files .record .title.m4a, #dialog_main #resource_file_area .pages_list ul li a.page_link.mp3, #dialog_main #resource_file_area .pages_list ul li a.page_link.wav, #dialog_main #resource_file_area .pages_list ul li a.page_link.aiff, #dialog_main #resource_file_area .pages_list ul li a.page_link.m4a { @include icon('file-audio-o'); } + #records .actions { position: absolute; right: 0; @@ -722,6 +857,7 @@ label.stripped { display: block; line-height: 28px; } + #records.tree.icons .title { display: block; margin: 0 120px 0 20px; @@ -730,28 +866,35 @@ label.stripped { cursor: pointer; } } + #records.tree .actions { line-height: 22px; top: 1px; } + .published { width: 25px; text-align: center; } + #content #records.tree > ul li .actions a { margin: 3px 3px 0 3px; line-height: 24px; } + .actions a * { padding: 4px 4px 1px 4px; } + .actions a img { vertical-align: middle; padding: 2px 4px 4px 4px; } + #records.tree .actions a img { padding-top: 4px; } + #records.tree li span.item { display: block; float: left; @@ -762,12 +905,14 @@ label.stripped { &.toggle { cursor: pointer; - @include icon('folder',$icon_folder_colour); + @include icon('folder', $icon_folder_colour); } + &.toggle.expanded { - @include icon('folder-open',$icon_folder_colour); + @include icon('folder-open', $icon_folder_colour); } } + #records.tree li.loading > div > span.icon { @include icon('spinner'); } @@ -776,54 +921,70 @@ label.stripped { background: inherit !important; cursor: move; } + #other_records { width: 68%; } + #common_actions { margin: 0; padding: 0; + li { margin: 0; list-style: none; padding: 5px 0 5px 0; } - a { - font-weight: bold; - } + + a { + font-weight: bold; + } } + .larger { font-size: 200%; } + .brown_border { border: 1px solid #99998B; } + #inquiry, .inquiry { border-collapse: collapse; width: 100%; } + #inquiry td, .inquiry td { border-bottom: 1px solid #CCCCCC; padding: 7px; } + #inquiry tr:last-child td, .inquiry tr:last-child td { border-bottom: 0; } + #inquiry td label.stripped, .inquiry td label.stripped { font-weight: bold; } + body.dialog { background: #FFF; } + body.dialog, body.visual_editor_dialog { text-align: left; } + body.dialog form { width: 100% !important; } + .visual_editor_dialog_table { height: 250px; } + .no_picked_image_selected {@include icon('warning', $icon_warning_colour)} + .dialog { #dialog_main { float: left; @@ -831,31 +992,38 @@ body.dialog form { min-height: 405px; width: 696px; } + span.radio { display: block; line-height: 18px; padding: 6px 0; } + span.radio * { cursor: pointer; font-weight: bold; } + #dialog_menu_left { position: fixed; left: 12px; top: 9px; width: 130px; } + #existing_image_content { position: relative; height: 300px; padding: 12px; } + #existing_image_area_content, #existing_image_area_crops { margin-top: 28px; + ul { margin: 0; padding: 0; + li { list-style: none; padding: 0; @@ -869,31 +1037,38 @@ body.dialog form { cursor: pointer; text-align: center; vertical-align: middle; + img { border: 4px solid transparent; } + &.selected img { border: 4px solid #22A7F2; } } } } + #existing_image_size_area { margin-top: 18px; margin-bottom: 50px; + ul { margin: 0; padding: 10px 0 0 0; + li { float: left; list-style: none; margin: 0 18px 0 0; - text-align:center; + text-align: center; + a { display: block; border: 1px solid #999999; font-size: 10px; } + &.selected { a { border-color: #22A7F2; @@ -905,23 +1080,31 @@ body.dialog form { } } } + #existing_image_size_area { #image_dialog_size_0 a { height: 30px; width: 30px; line-height: 30px; margin-top: 10px } - #image_dialog_size_1 a { height: 50px; width: 50px; line-height: 50px; margin-top: 0 } - #image_dialog_size_2 a { height: 70px; width: 70px; line-height: 70px; margin-top:-10px } - #image_dialog_size_3 a { height: 90px; width: 90px; line-height: 90px; margin-top:-20px } + + #image_dialog_size_1 a { height: 50px; width: 50px; line-height: 50px; margin-top: 0 } + + #image_dialog_size_2 a { height: 70px; width: 70px; line-height: 70px; margin-top: -10px } + + #image_dialog_size_3 a { height: 90px; width: 90px; line-height: 90px; margin-top: -20px } } + #content { padding: 0; } } + #upload_image_area, #upload_resource_area { padding: 12px; } + .visual_editor_dialog #page { width: 940px; padding: 6px; } + #dialog_main { .pagination { margin: 0; @@ -930,16 +1113,20 @@ body.dialog form { right: 12px; z-index: 1000; } + .pages_list { width: 100%; padding-bottom: 40px; + ul { margin: 0 12px 0 12px; padding: 0; + li { cursor: pointer; line-height: 24px; list-style: none; + a { display: block; padding: 3px 3px 3px 27px; @@ -947,57 +1134,72 @@ body.dialog form { border-bottom: none; border: 1px solid transparent; } + &:hover { background-color: #C9DAE2; } } + li.child a { padding-left: 27px; } + li.child1 a { padding-left: 47px; } + li.child2 a { padding-left: 67px; } + li.child3 a { padding-left: 87px; } + li.child4 a { padding-left: 107px; } } } + .actions { margin-right: 48px; } + .pages_list .linked a, .pages_list .linked a:hover { border: 1px solid #00A017; color: #00A017; @include icon('check-circle', green); } + .pages_list .linked a em { color: #00A017; } + #web_address_area, #dialog_main #email_address_area { padding: 12px; } } + #link_title { margin-top: 12px; + label { margin: 3px 0 0 0 !important; width: 130px; display: block; float: left; } + input { width: 770px; } } + ul#menu.reordering_menu li a { cursor: move; } + #search { border: 1px solid #b3b3b3; line-height: 18px; @@ -1005,36 +1207,46 @@ ul#menu.reordering_menu li a { font-size: 16px; width: 10em; } + .cancel-search { float: right; } + .pt-BR #search { width: 107px; } + .en #search { width: 130px; } + .search_form { position: relative; + .button, .button-wrapper { position: absolute; right: 0; top: 2px; } } + form input[type=submit]:hover { background: #65c3f7; } + .clearfix:after { - content:"."; - display:block; - height:0; - clear:both; - visibility:hidden; + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; } -.clearfix {display:inline-block;} + +.clearfix {display: inline-block;} + /* Hide from IE Mac \*/ -.clearfix {display:block;} +.clearfix {display: block;} + /* End hide from IE Mac */ /* Firefox Dotted Line Fix @@ -1043,6 +1255,7 @@ Firefox Dotted Line Fix a:focus { outline: none; } + a img { border: 0 none; } @@ -1082,61 +1295,75 @@ ul#page_parts_controls { #page-tabs.ui-sortable.reordering { li { cursor: move; + a { cursor: move; } } } + #page_part_editors { - clear:left; + clear: left; } /* dialog stuff */ #dialog_frame { - width:952px; - height:460px; + width: 952px; + height: 460px; padding: 0; border: 0 solid #F2F1ED; } + .visual_editor_hideables { display: none; } + #content .form-actions, .wym_dialog .form-actions, .ui-dialog .form-actions { @include form-actions; } + .dialog form { margin-bottom: 45px; } + .dialog .dialog_area > div > .field label:first-child, .dialog .dialog_area > div > label:first-child { margin-top: 0; } + .visual_editor_dialog .form-actions, #content.form-actions.dialog-form-actions, .ui-dialog .form-actions { margin-top: 16px; } + #content .form-actions .form-actions-left, #content .form-actions .form-actions-right, - .visual_editor_dialog .form-actions .form-actions-left, .visual_editor_dialog .form-actions .form-actions-right, - .ui-dialog .form-actions .form-actions-left, .ui-dialog .form-actions .form-actions-right { +.visual_editor_dialog .form-actions .form-actions-left, .visual_editor_dialog .form-actions .form-actions-right, +.ui-dialog .form-actions .form-actions-left, .ui-dialog .form-actions .form-actions-right { position: absolute; top: 10px; } + #content .form-actions .form-actions-left, .visual_editor_dialog .form-actions .form-actions-left, .ui-dialog .form-actions .form-actions-left { left: 10px; } + #content .form-actions .form-actions-right, .visual_editor_dialog .form-actions .form-actions-right, .ui-dialog .form-actions .form-actions-right { right: 10px; } + #content .form-actions .save-loader { position: absolute; right: -24px; top: 4px; } + .visual_editor_dialog .form-actions, .ui-dialog .form-actions { border: 0 none; border-top: 1px solid #E8E8E8; } + #dialog_iframe { position: relative; } + #dialog_container #content .form-actions, .ui-dialog .form-actions, .dialog_container .form-actions { position: absolute; bottom: 0; @@ -1147,21 +1374,26 @@ ul#page_parts_controls { border-bottom: 0; width: auto; } + #dialog_container.dialog.iframed #content .form-actions { position: fixed; } + body.dialog #content .search_form { float: right; width: auto !important; min-width: 300px; margin-bottom: 0; } + #existing_image_area { - padding-top:6px; + padding-top: 6px; } + .ui-dialog div.field { margin: 0 10px; } + #dialog_container, .dialog_container { margin: 0; padding: 12px; @@ -1183,47 +1415,58 @@ ul.checkboxes { margin: 0; padding: 0; } + ul.checkboxes li { list-style: none; } + .label_inline_with_link label { float: left; margin-right: 6px; } + .label_inline_with_link a { border: 0 none; margin-top: 19px; line-height: 17px; float: left; } + .label_inline_with_link a img { vertical-align: middle; } + .remove_picked_image { - margin-top:8px; - display:inline-block; - width:auto; + margin-top: 8px; + display: inline-block; + width: auto; } + #new_page_part_dialog .field { padding: 0 10px; } + .hide-overflow { overflow: hidden; } + #remove_resource { - margin-top:8px; - display:inline-block; - width:auto; + margin-top: 8px; + display: inline-block; + width: auto; } + .visual_editor_dialog_paste .field textarea { width: 98%; } + .ui-dialog .visual_editor_dialog_paste .field, .ui-dialog .visual_editor_dialog_paste .field textarea { margin: 0 0 45px 0; height: 300px; } + input.button, a.button, #content a.button, span.button-wrapper, span.button-wrapper input { - cursor:pointer; + cursor: pointer; background: #22a7f2; color: white; padding: 0 14px 0 14px; @@ -1235,93 +1478,119 @@ input.button, a.button, #content a.button, span.button-wrapper, span.button-wrap margin-top: 0; margin-bottom: 0; } + /* for those pesky IE browsers */ span.button-wrapper { padding: 0; } + span.button-wrapper input { display: inherit; } + /* fixes firefox display */ input.button { padding-bottom: 3px; } + input.button.close_dialog, a.button.close_dialog, #content a.button.close_dialog, span.button-wrapper.close_dialog, span.button-wrapper.close_dialog input { background: #bcbcbc; } + input.button:hover, a.button:hover, #content a.button:hover, span.button-wrapper:hover, span.button-wrapper:hover input { - background:#62bef2; + background: #62bef2; } + input.button.close_dialog:hover, a.button.close_dialog:hover, #content a.button.close_dialog:hover, span.button-wrapper.close_dialog:hover { background: #cdcdcd; } + input.button:active, a.button:active, #content a.button:active, span.button-wrapper:active, span.button-wrapper:active input { background: #004a8f; } -input.button.close_dialog:active, a.button.close_dialog:active, #content a.button.close_dialog:active, span.button-wrapper.close_dialog:active, span.button-wrapper.close_dialog:active input { + +input.button.close_dialog:active, a.button.close_dialog:active, #content a.button.close_dialog:active, span.button-wrapper.close_dialog:active, span.button-wrapper.close_dialog:active input { background: #808080; } + .visual_editor_dialog a.button.visual_editor_cancel.close_dialog { margin-left: 6px; } + #content a.button.close_dialog:active { color: white; } + .form-actions a.confirm-delete, #content .form-actions a.confirm-delete { background: #ee1100; position: absolute; right: 0; } + .form-actions a.confirm-delete:hover, #content .form-actions a.confirm-delete:hover { background: #ff3322; } + .form-actions a.confirm-delete:active, #content .form-actions a.confirm-delete:active { background: #bb0000; } + .field input[type=text], .field input[type=password], .field input[type=email], .field input[type=tel], .field input[type=number], .field textarea { border: 1px solid #7f9db9; padding: 0.4% 0.5%; line-height: 20px; } + /* ## Advanced Page Options --------------------------------------------- */ #more_options_field { span#draft_field {float: right;} } -#more_options{ - overflow:hidden; + +#more_options { + overflow: hidden; } + .hemisquare { padding: 10px; margin: 0 0; margin-left: 10px; float: left; width: 45%; + &.right_side { - float: right; + float: right; } + input, textarea, select { margin: 7px 0; } + textarea { margin-bottom: 0; } + label { margin: 0; } + small { font-size: 0.9em; } } + #content .hemisquare h2 { margin-top: 0; } + #content .hemisquare .field { margin: 0 0 20px 0; width: 98%; } + .label_with_help { @include label_with_help } + //.label_with_help label, .label_with_help span.help, .label_with_help img.help { // display: inline; //} @@ -1343,9 +1612,11 @@ input.button.close_dialog:active, a.button.close_dialog:active, #content a.butto margin-bottom: 12px; display: inline-block; } + #upgrade_wrapper li a { line-height: 20px; } + /* resource-picker */ #resource_actions { float: right; @@ -1356,11 +1627,13 @@ input.button.close_dialog:active, a.button.close_dialog:active, #content a.butto #switch_locale_picker { margin: 0; padding: 0; + li { float: left; padding: 0; margin: 0; list-style: none; + a { background-color: #cdcdcd; background-position: 12px; @@ -1371,13 +1644,16 @@ input.button.close_dialog:active, a.button.close_dialog:active, #content a.butto display: block; padding: 7px 3px 3px 3px; line-height: 0; + &:hover { background-color: #cae7fb; } + &:active { background-color: #22A7F2; } } + &:first-child a { border-radius-left: 5px; -moz-border-radius-topleft: 5px; @@ -1385,6 +1661,7 @@ input.button.close_dialog:active, a.button.close_dialog:active, #content a.butto -webkit-border-top-left-radius: 5px; -webkit-border-bottom-left-radius: 5px; } + &:last-child a { border-radius-right: 5px; -moz-border-radius-topright: 5px; @@ -1392,15 +1669,18 @@ input.button.close_dialog:active, a.button.close_dialog:active, #content a.butto -webkit-border-top-right-radius: 5px; -webkit-border-bottom-right-radius: 5px; } + &.selected a { background-color: #65c3f7; } } } + #locale_picker { li { list-style: none; margin: 0; + a { background-color: #cdcdcd; background-position: 12px; @@ -1410,12 +1690,15 @@ input.button.close_dialog:active, a.button.close_dialog:active, #content a.butto display: block; padding: 3px 14px 3px 9px; position: relative; + &:hover { background-color: #cae7fb; } + &:active { background-color: #22A7F2; } + span.action { border-bottom: 1px dotted #727272; position: absolute; @@ -1423,6 +1706,7 @@ input.button.close_dialog:active, a.button.close_dialog:active, #content a.butto top: 5px; } } + &:first-child a { border-radius-top: 5px; -moz-border-radius-topleft: 5px; @@ -1430,6 +1714,7 @@ input.button.close_dialog:active, a.button.close_dialog:active, #content a.butto -webkit-border-top-left-radius: 5px; -webkit-border-top-right-radius: 5px; } + &:last-child a { border-radius-bottom: 5px; -moz-border-radius-bottomleft: 5px; @@ -1438,32 +1723,39 @@ input.button.close_dialog:active, a.button.close_dialog:active, #content a.butto -webkit-border-bottom-right-radius: 5px; } } + #current_locale, #other_locales { margin: 0; } + #current_locale li { a { background-color: #65c3f7; } } } + .locale_marker { - font-size:8px; + font-size: 8px; display: inline; + .fa-stack .fa-comment {color: $icon_locale_colour; font-size: 24px;} } #content .locales a { border-bottom: none; } + #content #records .title .preview a.locale { border-bottom: 0 none; } + /* AJAX pagination */ .pagination_container { position: relative; } + .pagination_frame { padding: 0; width: 100%; @@ -1473,12 +1765,15 @@ input.button.close_dialog:active, a.button.close_dialog:active, #content a.butto -webkit-transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out; } + .pagination_container > div.pagination_frame { top: 40px; } + .pagination_frame.frame_left { left: -3000px; } + .pagination_frame.frame_right { left: 3000px; -moz-transition: all 0.3s ease-in-out; @@ -1486,21 +1781,26 @@ input.button.close_dialog:active, a.button.close_dialog:active, #content a.butto -webkit-transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out; } + .pagination_frame.frame_center { left: 0; } + .pagination_frame li { position: relative; padding-left: 5px; } + a.information:hover { background: #22a7f2; } + /* dubiously in here */ .current_image_link { display: inline-block; width: auto; } + .label { padding: 1px 3px 2px; font-size: 9.75px; @@ -1510,16 +1810,20 @@ a.information:hover { white-space: nowrap; background-color: #bfbfbf; margin-right: 0.25em; - &.important{ + + &.important { background-color: #c43c35; } - &.warning{ + + &.warning { background-color: #f89406; } - &.success{ + + &.success { background-color: #46a546; } - &.notice{ + + &.notice { background-color: #62cffc; } } From d50409864596d4223cc5de95532272d8afb41e66 Mon Sep 17 00:00:00 2001 From: Anita Date: Tue, 7 Jan 2025 11:28:25 +0800 Subject: [PATCH 20/74] Add defined colours for image index actions. extend image mixins use mixins to handle grid view and list view for images. --- .../stylesheets/refinery/global/_colours.scss | 6 +- .../stylesheets/refinery/mixins/_images.scss | 60 ++++++++++++++++--- .../refinery/sections/_layout.scss | 28 +++++---- 3 files changed, 74 insertions(+), 20 deletions(-) diff --git a/core/app/assets/stylesheets/refinery/global/_colours.scss b/core/app/assets/stylesheets/refinery/global/_colours.scss index e7eb7dc36b0..0e3f586609c 100644 --- a/core/app/assets/stylesheets/refinery/global/_colours.scss +++ b/core/app/assets/stylesheets/refinery/global/_colours.scss @@ -18,4 +18,8 @@ $action_border_colour: $icon_locale_colour; // $icon_preview_colour: #7f00ff; // violet // $icon_edit_colour: #007fff; // azure -// $icon_add_colour: #7fff00; // chartreuse \ No newline at end of file +// $icon_add_colour: #7fff00; // chartreuse + +$index_entry_hover_background: #f1f1f1; +$index_entry_actions_background: #f1f1f1; +$index_entry_actions_border: #788; diff --git a/core/app/assets/stylesheets/refinery/mixins/_images.scss b/core/app/assets/stylesheets/refinery/mixins/_images.scss index a2c67bebbbb..75806b34a27 100644 --- a/core/app/assets/stylesheets/refinery/mixins/_images.scss +++ b/core/app/assets/stylesheets/refinery/mixins/_images.scss @@ -1,7 +1,10 @@ @mixin indexImage { + a {font-weight: bold; + &:has(img) { border-bottom: 0} + } .alt {font-style: italic} - .preview {color: #A3A093} // the filename - .title {font-weight: bold;} + .filename {font-style: italic} + .title {text-transform: capitalize} } @mixin gridImage { @@ -14,18 +17,36 @@ display: inline-block; margin-right: 2px; } + .actions { + @include imageActions { + justify-content: flex-start; + }; + margin: 0 0.25rem; + border: 0.5px solid $index_entry_actions_border; + background-color: $index_entry_actions_background; + border-radius: 2px; + } } @mixin listImage { @include indexImage; - margin: 0 12px 12px 0; + //margin: 0 12px 12px 0; padding: 0; overflow: hidden; + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: baseline; + gap: 1rem; .actions { - top: auto; - bottom: 0; - right: 0; - width: 100%; + @include imageActions { + margin-left: auto; + justify-content: flex-end; + } + } + + &:hover { + background-color: $index_entry_hover_background; } } @@ -38,15 +59,36 @@ // apply to #image_grid @mixin imageGridLayout { @include imageIndexLayout { + list-style-type: none; + padding-inline-start: 0; display: grid; grid-template-columns: repeat(4, 150px); grid-template-rows: auto; - gap: 15px;} + gap: 15px; + } } -// apply to #image_list (note: 1. id, 2. not plural +// apply to #image_list @mixin imageListLayout { @include imageIndexLayout { margin: 10px 0 15px 0; } } + +@mixin imageActions { + // unset some + position: relative; + top: unset; + right: unset; + width: auto; + + display: flex; + flex-direction: row; + gap: 0.5rem; + line-height: 1.5; + a { + float: none; + margin: 0; + } + @content; + } diff --git a/core/app/assets/stylesheets/refinery/sections/_layout.scss b/core/app/assets/stylesheets/refinery/sections/_layout.scss index 82c5080f44d..cbede97421c 100644 --- a/core/app/assets/stylesheets/refinery/sections/_layout.scss +++ b/core/app/assets/stylesheets/refinery/sections/_layout.scss @@ -1398,16 +1398,24 @@ body.dialog #content .search_form { margin: 0; padding: 12px; } -#records ul#image_grid { - @include imageGridLayout; - li { - @include gridImage - } -} -#records ul#images_list { - @include imageListLayout; - li { - @include listImage + +#records.images { + #image_index { + .image_group.grid { + @include imageGridLayout; + + li { + @include gridImage + } + } + + .image_group.list { + @include imageListLayout; + + li { + @include listImage + } + } } } From 8837cdcf6d9950260b08f1299f0c8b581b844cbc Mon Sep 17 00:00:00 2001 From: Anita Date: Tue, 7 Jan 2025 11:31:27 +0800 Subject: [PATCH 21/74] Add defined colours for image index actions. extend image mixins use mixins to handle grid view and list view for images. replace hash.merge with Ruby3 keyword format --- core/app/helpers/refinery/image_helper.rb | 2 +- core/app/helpers/refinery/pagination_helper.rb | 1 + core/app/helpers/refinery/tag_helper.rb | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/core/app/helpers/refinery/image_helper.rb b/core/app/helpers/refinery/image_helper.rb index f87535307d1..8010b5cdc26 100644 --- a/core/app/helpers/refinery/image_helper.rb +++ b/core/app/helpers/refinery/image_helper.rb @@ -29,7 +29,7 @@ def image_fu(image, geometry = nil, options = {}) image_tag_args = (image.thumbnail_dimensions(geometry) rescue {}) image_tag_args[:alt] = image.respond_to?(:title) ? image.title : image.image_name - image_tag(image.thumbnail(thumbnail_args).url, image_tag_args.merge(options)) + image_tag image.thumbnail(thumbnail_args).url, **image_tag_args, **options end end end diff --git a/core/app/helpers/refinery/pagination_helper.rb b/core/app/helpers/refinery/pagination_helper.rb index e6e6c006e87..8c05196bd90 100644 --- a/core/app/helpers/refinery/pagination_helper.rb +++ b/core/app/helpers/refinery/pagination_helper.rb @@ -9,6 +9,7 @@ def pagination_css_class "frame_center" end end + alias pagination_selector pagination_css_class end end diff --git a/core/app/helpers/refinery/tag_helper.rb b/core/app/helpers/refinery/tag_helper.rb index db6842b9b50..da7b4ef4670 100644 --- a/core/app/helpers/refinery/tag_helper.rb +++ b/core/app/helpers/refinery/tag_helper.rb @@ -29,9 +29,10 @@ def action_label(action, url, title, options={}) # See icons.scss for defined icons/classes def action_icon_label(action, url, title, options={}, label = true) + action_classes = [options.delete(:class), "#{action}_icon"] + action_classes << "icon_label" if label + options[:class] = action_classes.compact.join(' ') options[:title] = title - options[:class].presence ? options[:class] << " #{action}_icon " : options[:class] = "#{action}_icon" - options[:class] << ' icon_label' if label case action when :preview @@ -41,7 +42,6 @@ def action_icon_label(action, url, title, options={}, label = true) when :reorder_done options[:class] << ' hidden' end - link_to(label && title || '', url, options) end From 6eafef5f67cf17c33cb71d74e3fa287b4dfba8f4 Mon Sep 17 00:00:00 2001 From: Anita Date: Tue, 7 Jan 2025 11:47:06 +0800 Subject: [PATCH 22/74] Change naming of 'grid_view' and 'list_view' to 'grid_format' and 'grid_view'. (view, format and layout are already special words, and I may yet change format to something useful, but not duplicating a term used in another context) keep the format variable as a symbol wherever it is used. renamed configuration accessors to 'index_formats' (et al) to be clear about their function. --- .../controllers/refinery/admin/images_controller.rb | 11 +++++++---- images/app/models/refinery/image.rb | 2 +- images/app/viewobjects/image_object.rb | 10 ++++++---- images/lib/refinery/images/configuration.rb | 6 +++--- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/images/app/controllers/refinery/admin/images_controller.rb b/images/app/controllers/refinery/admin/images_controller.rb index 958199a4b8d..58819827768 100644 --- a/images/app/controllers/refinery/admin/images_controller.rb +++ b/images/app/controllers/refinery/admin/images_controller.rb @@ -8,7 +8,7 @@ class ImagesController < ::Refinery::AdminController sortable: false, conditions: 'parent_id IS NULL' - before_action :change_list_mode_if_specified, :init_dialog + before_action :change_format_if_specified, :init_dialog def new @image = ::Refinery::Image.new if @image.nil? @@ -166,9 +166,12 @@ def init_dialog @conditions = params[:conditions] end - def change_list_mode_if_specified - if action_name == 'index' && params[:view].present? && Refinery::Images.image_views.include?(params[:view].to_sym) - Refinery::Images.preferred_image_view = params[:view] + def change_format_if_specified + return unless params[:format].present? + + format = params[:format].to_sym + if action_name == 'index' && format && Refinery::Images.index_formats.include?(format) + Refinery::Images.preferred_index_format = format end end diff --git a/images/app/models/refinery/image.rb b/images/app/models/refinery/image.rb index 551b4095333..a46a439b29b 100644 --- a/images/app/models/refinery/image.rb +++ b/images/app/models/refinery/image.rb @@ -46,7 +46,7 @@ def per_page(dialog = false, has_size_options = false) # Get a thumbnail job object given a geometry and whether to strip image profiles and comments. def thumbnail(options = {}) - options = { geometry: nil, strip: false }.merge(options) + options = { geometry: nil, strip: false, **options} geometry = convert_to_geometry(options[:geometry]) thumbnail = image thumbnail = thumbnail.thumb(geometry) if geometry diff --git a/images/app/viewobjects/image_object.rb b/images/app/viewobjects/image_object.rb index fa5cdebe7e9..e694099e7a4 100644 --- a/images/app/viewobjects/image_object.rb +++ b/images/app/viewobjects/image_object.rb @@ -15,20 +15,22 @@ def initialize(image, context, scope=nil) end def title - translated_field(image, :title) + translated_field(image, :image_title).titleize end def alt - translated_field(image, :alt) + translated_field(image, :image_alt).titleize end - def img_element - image_fu image, '149x149#c', title: ::I18n.t('edit_title', scope: i18n_scope, title: image.image_title || image.image_alt) + def grid_image_attributes + { src: image.thumbnail(geometry: Refinery::Images.admin_image_sizes[:grid], strip: true).url, + title: ::I18n.t('edit_title', scope: i18n_scope, title: title) } end def filename image.image_name end + def locales_with_titles image.translations .reject { |i| i.image_title.blank? } diff --git a/images/lib/refinery/images/configuration.rb b/images/lib/refinery/images/configuration.rb index e9ea01750c3..b6348976402 100644 --- a/images/lib/refinery/images/configuration.rb +++ b/images/lib/refinery/images/configuration.rb @@ -6,7 +6,7 @@ module Images config_accessor :max_image_size, :pages_per_dialog, :pages_per_admin_index, :pages_per_dialog_that_have_size_options, :user_image_sizes, :user_image_ratios, - :image_views, :preferred_image_view, :admin_image_sizes, + :index_formats, :preferred_index_format, :admin_image_sizes, :allowed_mime_types, :allowed_mime_types_msg self.max_image_size = 5_242_880 @@ -29,8 +29,8 @@ module Images } self.allowed_mime_types = %w[image/jpeg image/png image/gif image/tiff] self.allowed_mime_types_msg = self.allowed_mime_types.to_sentence(last_word_connector: ' or ') - self.image_views = [:grid, :list] - self.preferred_image_view = :grid + self.index_formats = [:grid, :list] + self.preferred_index_format = :grid # Images should always use these changes to the dragonfly defaults self.dragonfly_name = :refinery_images From 7cf83cbea9ba9e1fa66f2b4ed1c94498a0ebccbd Mon Sep 17 00:00:00 2001 From: Anita Date: Tue, 28 Jan 2025 13:53:39 +0800 Subject: [PATCH 23/74] Pages simplify view code for the admin index of a page. put selectors used in testing into single file (still some way to go) --- .../helpers/refinery/admin/pages_helper.rb | 31 ++- .../views/refinery/admin/pages/_page.html.erb | 71 +++--- pages/spec/support/selector_helpers.rb | 42 ++++ .../spec/system/refinery/admin/pages_spec.rb | 224 +++++++++--------- 4 files changed, 214 insertions(+), 154 deletions(-) create mode 100644 pages/spec/support/selector_helpers.rb diff --git a/pages/app/helpers/refinery/admin/pages_helper.rb b/pages/app/helpers/refinery/admin/pages_helper.rb index ae862744fb4..267d49cd0b1 100644 --- a/pages/app/helpers/refinery/admin/pages_helper.rb +++ b/pages/app/helpers/refinery/admin/pages_helper.rb @@ -3,22 +3,22 @@ module Admin module PagesHelper def parent_id_nested_set_options(current_page) pages = [] - nested_set_options(::Refinery::Page, current_page) { |page| pages << page} + nested_set_options(::Refinery::Page, current_page) { |page| pages << page } # page.title needs the :translations association, doing something like # nested_set_options(::Refinery::Page.includes(:translations), page) doesn't work, yet. # See https://github.com/collectiveidea/awesome_nested_set/pull/123 ActiveRecord::Associations::Preloader.new.preload(pages, :translations) - pages.map { |page| ["#{'-' * page.level} #{page.title}", page.id]} + pages.map { |page| ["#{'-' * page.level} #{page.title}", page.id] } end def template_options(template_type, current_page) - html_options = { :selected => send("default_#{template_type}", current_page) } + html_options = { selected: send("default_#{template_type}", current_page) } if (template = current_page.send(template_type).presence) - html_options.update :selected => template + html_options.update selected: template elsif current_page.parent_id? && !current_page.send(template_type).presence template = current_page.parent.send(template_type).presence - html_options.update :selected => template if template + html_options.update selected: template if template end html_options @@ -32,6 +32,27 @@ def default_layout_template(current_page) "application" end + def page_icon(number_of_children) + # 1. has_children 'folder|folderopen', 'toggle' + # 2. no_children 'page' + + # .toggle scss handles adding icons to pages with children + classes = [] + case number_of_children + when 0 + classes.push icon_class('page') + title = ::I18n.t('edit', scope: 'refinery.admin.pages') + else + classes.push 'toggle' + classes.push 'expanded' if Refinery::Pages.auto_expand_admin_tree + title = ::I18n.t('expand_collapse', scope: 'refinery.admin.pages') + end + + tag.span(class: classes.join(' '), title: title) + end + + def icon_class(icon) = "#{icon}_icon" + # In the admin area we use a slightly different title # to inform the which pages are draft or hidden pages def page_meta_information(page) diff --git a/pages/app/views/refinery/admin/pages/_page.html.erb b/pages/app/views/refinery/admin/pages/_page.html.erb index efc2011519c..a95b2dc4441 100644 --- a/pages/app/views/refinery/admin/pages/_page.html.erb +++ b/pages/app/views/refinery/admin/pages/_page.html.erb @@ -1,49 +1,46 @@ <% # setup params for various action links - add_url = refinery.new_admin_page_path(parent_id: page.id) - edit_url = refinery.admin_edit_page_path(page.nested_url, switch_locale: (page.translations.first.locale unless page.translated_to_default_locale?)) - delete_url = refinery.admin_delete_page_path(page.nested_url) - delete_options = { - class: "cancel confirm-delete", - data: {confirm: t('message', scope: 'refinery.admin.delete', title: translated_field(page, :title))} - } - %> + add_url = refinery.new_admin_page_path(parent_id: page.id) + + edit_url = refinery.admin_edit_page_path(page.nested_url) + translated_locales = locales_with_titles(page, 'title', include_current: true) -
  • -
    - <% if page.children.present? %> - - - <% else %> - - <% end %> + delete_url = refinery.admin_delete_page_path(page.nested_url) + delete_options = { + class: "cancel confirm-delete", + data: { confirm: t('message', scope: 'refinery.admin.delete', title: translated_field(page, :title)) } + } + dom_id = dom_id(page) + i18n_scope = 'refinery.admin.pages' + record_classes = [:record, :page].join(' ') +%> - '> - <%= translated_field(page, :title) %> +
  • +
    + <%= page_icon(page.children.size) %> + <%= link_to translated_field(page, :title), edit_url, class: [:title, :edit], title: t('edit', scope: i18n_scope) %> + <%= page_meta_information page %> - <% if Refinery::I18n.frontend_locales.many? %> - - <% page.translations.sort_by{ |t| Refinery::I18n.frontend_locales.index(t.locale)}.each do |translation| %> - <% if translation.title.present? %> - <%= link_to refinery.admin_edit_page_path(page.nested_url, switch_locale: translation.locale), - class: 'locale', title: translation.locale.upcase do %> + + <% translated_locales.each do |locale| %> -
    - <%= locale_text_icon(translation.locale.upcase) %> -
    - <% end %> - <% end %> - <% end %> -
    - <% end %> + <% hint = ::I18n.with_locale(locale) do + I18n.t('.edit_title_locale', + title: translated_field(page, :title), + language: locale_language(locale), + scope: ".refinery.admin.locale_picker") + end + %> + <%= edit_in_locale locale, url: edit_url, title: hint %> + <% end %> +
    - <%= action_icon(:preview, page.url, t('.view_live_html')) %> - <%= action_icon(:add, add_url, t('new', scope: 'refinery.admin.pages' ) ) %> - <%= action_icon(:edit, edit_url , t('edit', scope: 'refinery.admin.pages' ) ) %> - <%= action_icon(:delete, delete_url, t('delete', scope: 'refinery.admin.pages' ), delete_options ) if page.deletable? %> + <%= action_icon(:preview, page.url, t('.view_live_html')) %> + <%= action_icon(:add, add_url, t('new', scope: i18n_scope)) %> + <%= action_icon(:edit, edit_url, t('edit', scope: i18n_scope)) %> + <%= action_icon(:delete, delete_url, t('delete', scope: i18n_scope), delete_options) if page.deletable? %>
    diff --git a/pages/spec/support/selector_helpers.rb b/pages/spec/support/selector_helpers.rb new file mode 100644 index 00000000000..2ba883ac83e --- /dev/null +++ b/pages/spec/support/selector_helpers.rb @@ -0,0 +1,42 @@ + + def index_entry + 'li.record.page' + end + + def index_item(id) + [id, '.item'].join(' ') + end + + # various edit actions selectors: there are several in a single index entry + # 1. the page title is wrapped in an edit link + # page title + # 2. if there are several locales there is an edit_in_locale link for each locale + # + # 3. the actions group has an edit icon with a link to edit the page (equivalent to 1) + # + # + def edit_selector(class_name: nil, locale: nil, slug: nil) + class_selector = [class_name, 'edit'].compact.join('.') + query_selector = locale.nil? ? '' : "?switch_locale=#{locale}" + "a.#{class_selector}[href$='#{slug}/edit#{query_selector}']" + end + + def title_link_selector(slug: '') + edit_selector(slug: slug, class_name: :title) + end + + def icon_link_selector(slug: '') + edit_selector(slug: slug, class_name: :edit_icon) + end + + def locale_link_selector(locale:, slug: '') + edit_selector(slug: slug, class_name: :locale, locale: locale.downcase) + end + + def locales + "span.locales" + end + + def locale_picker(locale) + ".locales ##{locale}" + end diff --git a/pages/spec/system/refinery/admin/pages_spec.rb b/pages/spec/system/refinery/admin/pages_spec.rb index faf49a85224..f3b67c25e2f 100644 --- a/pages/spec/system/refinery/admin/pages_spec.rb +++ b/pages/spec/system/refinery/admin/pages_spec.rb @@ -15,17 +15,16 @@ def expect_window_without_content(content, window: windows.last) def switch_page_form_locale(locale) within "#switch_locale_picker" do - find("a", text: locale.upcase).click + find("a", id: locale.downcase).click end # make sure that the locale change has taken effect expect(page).to have_selector("#switch_locale_picker li.selected a##{locale.downcase}") end - module Refinery module Admin - describe "Pages", :type => :system do + describe "Pages", type: :system do refinery_login context "when no pages" do @@ -88,10 +87,10 @@ module Admin end context "when sub pages exist" do - let!(:company) { Page.create :title => 'Our Company' } - let!(:team) { company.children.create :title => 'Our Team' } - let!(:locations) { company.children.create :title => 'Our Locations' } - let!(:location) { locations.children.create :title => 'New York' } + let!(:company) { Page.create title: 'Our Company' } + let!(:team) { company.children.create title: 'Our Team' } + let!(:locations) { company.children.create title: 'Our Locations' } + let!(:location) { locations.children.create title: 'New York' } context "with auto expand option turned off" do before do @@ -111,15 +110,15 @@ module Admin end it "expands children", js: true do - find("#page_#{company.id} .title.toggle").click + find("#page_#{company.id} .item .toggle").click expect(page).to have_content(team.title) expect(page).to have_content(locations.title) end it "expands children when nested multiple levels deep", js: true do - find("#page_#{company.id} .title.toggle").click - find("#page_#{locations.id} .title.toggle").click + find("#page_#{company.id} .item .toggle").click + find("#page_#{locations.id} .item .toggle").click expect(page).to have_content("New York") end @@ -138,28 +137,30 @@ module Admin expect(page).to have_content(locations.title) end - it "refreshes the cache if sub_children change" do + it "refreshes the cache if sub_children change", js: true do expect(page).to have_content(location.title) - find("#page_#{location.id} .edit_icon").click - fill_in "Title", with: 'Las Vegas' - click_button "Save" + within page.find("#page_#{location.id}") do + first(title_link_selector).click + end + fill_in "page_title", with: 'Las Vegas' + click_button "Save " expect(page).to have_content('Las Vegas') end end end end - describe "new/create" do - it "Creates a page", js: true do - Rails.logger.debug "#{Time.current} about to visit #{refinery.admin_pages_path}" + describe " new / create " do + it " Creates a page ", js: true do + Rails.logger.debug " #{Time.current} about to visit #{refinery.admin_pages_path}" visit refinery.admin_pages_path Rails.logger.debug "#{Time.current} at #{refinery.admin_pages_path}" find('a', text: 'Add new page').click - fill_in "Title", :with => "My first page" + fill_in "Title", with: "My first page" expect { click_button "Save" }.to change(Refinery::Page, :count).from(0).to(1) expect(page).to have_content("'My first page' was successfully added.") @@ -176,11 +177,11 @@ module Admin it "includes menu title field", js: true do visit refinery.new_admin_page_path - fill_in "Title", :with => "My first page" + fill_in "Title", with: "My first page" find('#toggle_advanced_options').click - fill_in "Menu title", :with => "The first page" + fill_in "Menu title", with: "The first page" # For some reason the first click doesn't always work? begin @@ -194,13 +195,13 @@ module Admin end it "allows to easily create nested page" do - parent_page = Page.create! :title => "Rails 4" + parent_page = Page.create! title: "Rails 4" visit refinery.admin_pages_path - find("a[href='#{refinery.new_admin_page_path(:parent_id => parent_page.id)}']").click + find("a[href='#{refinery.new_admin_page_path(parent_id: parent_page.id)}']").click - fill_in "Title", :with => "Parent page" + fill_in "Title", with: "Parent page" click_button "Save" expect(page).to have_content("'Parent page' was successfully added.") @@ -220,9 +221,9 @@ module Admin context 'when saving and returning to index' do it "updates page", js: true do - find("a[href$='#{updateable_page.slug}/edit']").click + first(edit_selector(slug: updateable_page.slug)).click - fill_in "Title", :with => "Updated" + fill_in "Title", with: "Updated" find("#submit_button").click expect(page).to have_content("'Updated' was successfully updated.") @@ -231,10 +232,10 @@ module Admin context 'when saving and continuing to edit', js: true do before :each do - expect(page).to have_selector("a[href$='#{updateable_page.slug}/edit']", visible: true) - find("a[href$='#{updateable_page.slug}/edit']").click + expect(page).to have_selector(edit_selector(slug: updateable_page.slug), visible: true) + first(edit_selector(slug: updateable_page.slug)).click - fill_in "Title", :with => "Updated you" + fill_in "Title", with: "Updated you" find("#submit_continue_button").click find('#flash').visible? end @@ -264,12 +265,12 @@ module Admin describe 'Previewing', js: true do let(:preview_content) { "Some changes I'm unsure what they will look like".freeze } context "an existing page" do - before { Page.create :title => 'Preview me' } + before { Page.create title: 'Preview me' } it 'will show the preview changes in a new window' do visit refinery.admin_pages_path - find('a[tooltip^=Edit]').click + first(edit_selector).click fill_in "Title", with: preview_content window = window_opened_by do click_button "Preview" @@ -283,7 +284,7 @@ module Admin it 'will not show the site bar' do visit refinery.admin_pages_path - find('a[tooltip^=Edit]').click + first(edit_selector).click fill_in "Title", with: preview_content window = window_opened_by do click_button "Preview" @@ -292,11 +293,11 @@ module Admin expect_window_without_content( ::I18n.t('switch_to_website', scope: 'refinery.site_bar'), window: window - ) + ) expect_window_without_content( ::I18n.t('switch_to_website_editor', scope: 'refinery.site_bar'), window: window - ) + ) window.close end @@ -304,7 +305,7 @@ module Admin it 'will not save the preview changes' do visit refinery.admin_pages_path - find('a[tooltip^=Edit]').click + first(edit_selector).click fill_in "Title", with: preview_content window = window_opened_by do click_button "Preview" @@ -313,7 +314,7 @@ module Admin expect_window_with_content( preview_content, window: window - ) + ) window.close @@ -324,8 +325,8 @@ module Admin it 'will show the preview in a new window after save-and-continue' do visit refinery.admin_pages_path - find('a[tooltip^=Edit]').click - fill_in "Title", :with => "Save this" + first(edit_selector).click + fill_in "Title", with: "Save this" click_button "Save & continue editing" expect(page).to have_content("'Save this' was successfully updated") @@ -335,9 +336,9 @@ module Admin expect_window_with_content("Save this", window: window) expect_window_without_content( - ::I18n.t('switch_to_website', :scope => 'refinery.site_bar'), + ::I18n.t('switch_to_website', scope: 'refinery.site_bar'), window: window - ) + ) window.close end @@ -347,8 +348,8 @@ module Admin it 'will save-and-continue after show the preview in a new window' do visit refinery.admin_pages_path - find('a[tooltip^=Edit]').click - fill_in "Title", :with => "Save this" + first(title_link_selector).click + fill_in "Title", with: "Save this" window = window_opened_by do click_button "Preview" @@ -356,7 +357,7 @@ module Admin expect_window_with_content("Save this", window: window) expect_window_without_content( - ::I18n.t('switch_to_website', :scope => 'refinery.site_bar'), + ::I18n.t('switch_to_website', scope: 'refinery.site_bar'), window: window ) @@ -366,13 +367,13 @@ module Admin expect(page).to have_content("'Save this' was successfully updated") end - it 'will show pages with inherited templates', js:true do + it 'will show pages with inherited templates', js: true do visit refinery.admin_pages_path - find('a[tooltip^=Edit]').click - fill_in 'Title', :with => 'Searchable' + first(edit_selector).click + fill_in 'Title', with: 'Searchable' find('#toggle_advanced_options').click - select 'Searchable', :from => 'View template' + select 'Searchable', from: 'View template' Timeout::timeout(5) do click_button 'Preview' sleep 0.1 and redo unless windows.many? @@ -382,11 +383,11 @@ module Admin end context 'a brand new page' do - it "will not save when just previewing", js:true do + it "will not save when just previewing", js: true do visit refinery.admin_pages_path find('a', text: 'Add new page').click - fill_in "Title", :with => "My first page" + fill_in "Title", with: "My first page" window = window_opened_by do click_button "Preview" end @@ -399,29 +400,29 @@ module Admin end context 'a nested page' do - let!(:parent_page) { Page.create :title => "Our Parent Page" } - let!(:nested_page) { parent_page.children.create :title => 'Preview Me' } + let!(:parent_page) { Page.create title: "Our Parent Page" } + let!(:nested_page) { parent_page.children.create title: 'Preview Me' } - it "works like an un-nested page" do + it "works like an un-nested page", js: true do visit refinery.admin_pages_path - within "#page_#{nested_page.id}" do - find('a[tooltip^=Edit]').click + within "#page_#{nested_page.id} .item" do + first(title_link_selector).click end fill_in "Title", with: preview_content - window = window_opened_by do + window = page.window_opened_by do click_button "Preview" end - expect_window_with_content(preview_content) + expect_window_with_content(preview_content, window: window) end end end describe "destroy" do - context "when page can be deleted", js:true do - before { Page.create :title => "Delete me" } + context "when page can be deleted", js: true do + before { Page.create title: "Delete me" } it "will show delete button" do visit refinery.admin_pages_path @@ -437,7 +438,7 @@ module Admin end context "when page can't be deleted" do - before { Page.create :title => "Indestructible", :deletable => false } + before { Page.create title: "Indestructible", deletable: false } it "wont show delete button" do visit refinery.admin_pages_path @@ -449,12 +450,12 @@ module Admin end context "duplicate page titles" do - before { Page.create :title => "I was here first" } + before { Page.create title: "I was here first" } it "will append nr to url path" do visit refinery.new_admin_page_path - fill_in "Title", :with => "I was here first" + fill_in "Title", with: "I was here first" click_button "Save" expect(Refinery::Page.last.url[:path].first).to match(%r{\Ai-was-here-first-.+?}) @@ -467,9 +468,9 @@ module Admin # Create a home page in both locales (needed to test menus) home_page = Mobility.with_locale(:en) do - Page.create :title => 'Home', - :link_url => '/', - :menu_match => "^/$" + Page.create title: 'Home', + link_url: '/', + menu_match: "^/$" end Mobility.with_locale(:ru) do @@ -478,11 +479,11 @@ module Admin end end - describe "add a page with title for default locale", js:true do + describe "add a page with title for default locale", js: true do before do visit refinery.admin_pages_path find('a', text: "Add new page").click - fill_in "Title", :with => "News" + fill_in "Title", with: "News" click_button "Save" end @@ -491,11 +492,10 @@ module Admin expect(Refinery::Page.count).to eq(2) end - it "shows locale flag for page" do + it "shows locale for page" do p = ::Refinery::Page.by_slug('news').first within "#page_#{p.id} .locales" do - expect(page).to have_css('.locale_marker') - expect(page).to have_content('EN') + expect(page).to have_selector(locale_link_selector(locale: 'en')) end end @@ -503,7 +503,7 @@ module Admin p = ::Refinery::Page.by_slug('news').first within "#page_#{p.id}" do expect(page).to have_content('News') - expect(page.find('a[tooltip="Edit this page"]')[:href]).to include('news') + expect(page.first(title_link_selector)[:text]).to have_content('News') end end @@ -522,7 +522,7 @@ module Admin within "#menu" do # we should only have the home page in the menu - expect(page).to have_css('li', :count => 1) + expect(page).to have_css('li', count: 1) end end end @@ -536,7 +536,7 @@ module Admin allow(Refinery::I18n).to receive(:frontend_locales).and_return([:en, :ru]) _page = Mobility.with_locale(:en) { - Page.create :title => en_page_title + Page.create title: en_page_title } Mobility.with_locale(:ru) do _page.title = ru_page_title @@ -553,7 +553,7 @@ module Admin click_link "Add new page" switch_page_form_locale "RU" - fill_in "Title", :with => ru_page_title + fill_in "Title", with: ru_page_title click_button "Save" expect(page).to have_selector("#page_#{Page.last.id} .actions") @@ -562,7 +562,7 @@ module Admin end switch_page_form_locale "EN" - fill_in "Title", :with => en_page_title + fill_in "Title", with: en_page_title find("#submit_button").click expect(page).to have_content("'#{en_page_title}' was successfully updated.") @@ -573,9 +573,9 @@ module Admin visit refinery.admin_pages_path within "#page_#{news_page.id} .locales" do - expect(page).to have_css('.locale_marker', count: 2) - expect(page).to have_content('EN') - expect(page).to have_content('RU') + # expect(page).to have_selector(edit_in_locale, count: 2) + expect(page).to have_selector(locale_link_selector(locale: 'en')) + expect(page).to have_selector(locale_link_selector(locale: 'ru')) end end @@ -591,7 +591,7 @@ module Admin visit refinery.admin_pages_path within "#page_#{news_page.id}" do - expect(page.find('a[tooltip="Edit this page"]')[:href]).to include(en_page_slug) + expect(page.first(title_link_selector)[:href]).to match(en_page_slug) end end @@ -612,10 +612,10 @@ module Admin end end - describe "add a page with title only for secondary locale", js:true do + describe "add a page with title only for secondary locale", js: true do let(:ru_page) { Mobility.with_locale(:ru) { - Page.create :title => ru_page_title + Page.create title: ru_page_title } } let(:ru_page_id) { ru_page.id } @@ -632,7 +632,7 @@ module Admin find('a', text: 'Add new page').click switch_page_form_locale "RU" - fill_in "Title", :with => ru_page_title + fill_in "Title", with: ru_page_title click_button "Save" expect(page).to have_content("'#{ru_page_title}' was successfully added.") @@ -641,13 +641,13 @@ module Admin it "shows locale indicator for page" do within "#page_#{ru_page_id}" do - expect(page).to have_selector('.locale_marker', text: 'RU') + expect(page).to have_selector(locale_link_selector(locale: 'ru')) end end it "doesn't show locale indicator for primary locale" do within "#page_#{ru_page_id}" do - expect(page).to_not have_selector('.locale_marker', text: 'EN') + expect(page).to_not have_selector(locale_picker('en')) end end @@ -657,9 +657,9 @@ module Admin end end - it "uses slug in admin" do + it "uses secondary locale slug in admin" do within "#page_#{ru_page_id}" do - expect(page.find('a[tooltip="Edit this page"]')[:href]).to include(ru_page_slug_encoded) + expect(page.find(title_link_selector)[:href]).to match(ru_page_slug_encoded) end end @@ -677,23 +677,23 @@ module Admin within "#menu" do # we should only have the home page in the menu - expect(page).to have_css('li', :count => 1) + expect(page).to have_css('li', count: 1) end end context "when page is a child page" do it 'succeeds' do ru_page.destroy! - parent_page = Page.create(:title => "Parent page") + parent_page = Page.create(title: "Parent page") sub_page = Mobility.with_locale(:ru) { - Page.create :title => ru_page_title, :parent_id => parent_page.id + Page.create title: ru_page_title, parent_id: parent_page.id } expect(sub_page.parent).to eq(parent_page) visit refinery.admin_pages_path within "#page_#{sub_page.id}" do - find("a.edit_icon").click + first(edit_selector).click end - fill_in "Title", :with => ru_page_title + fill_in "Title", with: ru_page_title click_button "Save" expect(page).to have_content("'#{ru_page_title}' was successfully updated") end @@ -711,7 +711,7 @@ module Admin find("#add_page_part").click within "#new_page_part_dialog" do - fill_in "new_page_part_title", :with => "testy" + fill_in "new_page_part_title", with: "testy" click_button "Save" end @@ -722,12 +722,12 @@ module Admin end describe "delete existing page part" do - let!(:some_page) { Page.create! :title => "Some Page" } + let!(:some_page) { Page.create! title: "Some Page" } before do - some_page.parts.create! :title => "First Part", :slug => "first_part", :position => 1 - some_page.parts.create! :title => "Second Part", :slug => "second_part", :position => 2 - some_page.parts.create! :title => "Third Part", :slug => "third_part", :position => 3 + some_page.parts.create! title: "First Part", slug: "first_part", position: 1 + some_page.parts.create! title: "Second Part", slug: "second_part", position: 2 + some_page.parts.create! title: "Third Part", slug: "third_part", position: 3 allow(Refinery::Pages).to receive(:new_page_parts).and_return(true) end @@ -774,10 +774,10 @@ module Admin allow(Refinery::Pages).to receive(:use_layout_templates).and_return(true) allow(Refinery::Pages).to receive(:layout_template_whitelist).and_return(['abc', 'refinery']) allow(Refinery::Pages).to receive(:valid_templates).and_return(['abc', 'refinery']) - parent_page = Page.create :title => 'Parent Page', - :view_template => 'refinery', - :layout_template => 'refinery' - @page = parent_page.children.create :title => 'Child Page' + parent_page = Page.create title: 'Parent Page', + view_template: 'refinery', + layout_template: 'refinery' + @page = parent_page.children.create title: 'Child Page' end specify 'sub page should inherit them', js: true do @@ -800,7 +800,7 @@ module Admin # regression spec for https://github.com/refinery/refinerycms/issues/1891 describe "a page part with HTML" do before do - page = Refinery::Page.create! :title => "test" + page = Refinery::Page.create! title: "test" Refinery::Pages.default_parts.each_with_index do |default_page_part, index| page.parts.create( title: default_page_part[:title], @@ -811,17 +811,17 @@ module Admin end end - specify "should retain the html", js:true do + specify "should retain the html", js: true do visit refinery.admin_pages_path - find('a[tooltip="Edit this page"]').click - Capybara.ignore_hidden_elements = false - expect(page).to have_content("header class='regression'") - Capybara.ignore_hidden_elements = true + first(edit_selector(slug: 'test')).click + Capybara.ignore_hidden_elements do + expect(page).to have_content("header class='regression'") + end end end end - describe "TranslatePages", :type => :system do + describe "TranslatePages", type: :system do before { Mobility.locale = :en } refinery_login @@ -829,13 +829,13 @@ module Admin before do allow(Refinery::I18n).to receive(:frontend_locales).and_return([:en, :lv]) end - let(:first_page){Page.create title: 'First Page'} + let(:first_page) { Page.create title: 'First Page' } it "can have a second locale added to it" do visit refinery.edit_admin_page_path(first_page.id) - switch_page_form_locale "LV" - fill_in "Title", :with => "Brīva vieta reklāmai" + switch_page_form_locale "lv" + fill_in "Title", with: "Brīva vieta reklāmai" click_button "Save" expect(page).to have_content("'Brīva vieta reklāmai' was successfully updated.") @@ -849,7 +849,7 @@ module Admin # Create a page in both locales about_page = Mobility.with_locale(:en) do - Page.create :title => 'About' + Page.create title: 'About' end Mobility.with_locale(:ru) do @@ -862,7 +862,7 @@ module Admin page = Refinery::Page.last # we need page parts so that there's a visual editor Refinery::Pages.default_parts.each_with_index do |default_page_part, index| - page.parts.create(:title => default_page_part[:title], :slug => default_page_part[:slug], :body => nil, :position => index) + page.parts.create(title: default_page_part[:title], slug: default_page_part[:slug], body: nil, position: index) end page end @@ -872,14 +872,14 @@ module Admin before { Refinery::Pages.absolute_page_links = false } it "shows Russian pages if we're editing the Russian locale" do - visit refinery.link_to_admin_pages_dialogs_path(:visual_editor => true, :switch_locale => :ru) + visit refinery.link_to_admin_pages_dialogs_path(visual_editor: true, switch_locale: :ru) expect(page).to have_content("About Ru") expect(page).to have_selector("a[href='/ru/about-ru']") end it "shows default to the default locale if no query string is added" do - visit refinery.link_to_admin_pages_dialogs_path(:visual_editor => true) + visit refinery.link_to_admin_pages_dialogs_path(visual_editor: true) expect(page).to have_content("About") expect(page).to have_selector("a[href='/about']") From c68d4786d7cf8e54392ae2d09b5c3f673db5725f Mon Sep 17 00:00:00 2001 From: Anita Date: Tue, 28 Jan 2025 13:55:07 +0800 Subject: [PATCH 24/74] Pages move helpers for actions (edit/preview/delete etc) from tag_helper.rb to (new)action_helper.rb move helpers for actions (edit/preview/delete etc) from tag_helper.rb to (new)action_helper.rb simplify view code for the admin index of a page. put selectors used in testing into single file (still some way to go) --- core/app/helpers/refinery/action_helper.rb | 47 +++++++++++++++++++ core/app/helpers/refinery/tag_helper.rb | 35 +------------- .../helpers/refinery/translation_helper.rb | 11 +++++ 3 files changed, 59 insertions(+), 34 deletions(-) create mode 100644 core/app/helpers/refinery/action_helper.rb diff --git a/core/app/helpers/refinery/action_helper.rb b/core/app/helpers/refinery/action_helper.rb new file mode 100644 index 00000000000..52a6604a780 --- /dev/null +++ b/core/app/helpers/refinery/action_helper.rb @@ -0,0 +1,47 @@ +module Refinery + module ActionHelper + def i18n_scope + 'refinery.index.locale_picker' + end + + # returns a link to the requested url, with the requested icon as content + def action_icon(action, url, title, options={}) + action_icon_label(action, url, title, options, false) + end + + # returns a link to the requested url, with icon and label as content + def action_label(action, url, title, options={}) + action_icon_label(action, url, title, options, true) + end + + # See icons.scss for defined icons/classes + def action_icon_label(action, url, title, options={}, label = true) + options[:title] = title + action_classes = ["#{action}_icon", action] + + case action + when :preview + options[:target] = '_blank' + when :delete + options[:method] = 'delete' + when :reorder_done + action_classes.push 'hidden' + end + + options[:class] = [options[:class], *action_classes].compact.join(' ') + link_to(label && title || '', url, **options) + end + + + def edit_in_current_locale(url:, title:, **options) + action_icon(:edit, url, title, class: :edit, **options ) + end + + def edit_in_locale(locale, url:, title:, **options) + Rails.logger.debug "[Refinery] edit_in_locale(#{locale}): #{url}, title: #{title}, options: #{options}" + action_icon(:locale, "#{url}?switch_locale=#{locale}", title, + locale: locale, class: :edit, **options + ) + end + end +end diff --git a/core/app/helpers/refinery/tag_helper.rb b/core/app/helpers/refinery/tag_helper.rb index da7b4ef4670..92ed89f5778 100644 --- a/core/app/helpers/refinery/tag_helper.rb +++ b/core/app/helpers/refinery/tag_helper.rb @@ -12,45 +12,12 @@ def refinery_help_tag(title='Tip') # This is just a quick wrapper to render an image tag that lives inside refinery/icons. # They are all 16x16 so this is the default but is able to be overriden with supplied options. def refinery_icon_tag(filename, options = {}) - Refinery.deprecate('Refinery::TagHelper.refinery_icon_tag', when: '5.1', replacement: 'Refinery::TagHelper.action_icon') + Refinery.deprecate('Refinery::TagHelper.refinery_icon_tag', when: '5.1', replacement: 'Refinery::ActionHelper.action_icon') filename = "#{filename}.png" unless filename.split('.').many? path = image_path "refinery/icons/#{filename}", skip_pipeline: true image_tag path, {:width => 16, :height => 16}.merge(options) end - def action_icon(action, url, title, options={}) - action_icon_label(action, url, title, options, false) - end - - def action_label(action, url, title, options={}) - action_icon_label(action, url, title, options, true) - end - - # See icons.scss for defined icons/classes - def action_icon_label(action, url, title, options={}, label = true) - action_classes = [options.delete(:class), "#{action}_icon"] - action_classes << "icon_label" if label - options[:class] = action_classes.compact.join(' ') - options[:title] = title - - case action - when :preview - options[:target] = '_blank' - when :delete - options[:method] = :delete - when :reorder_done - options[:class] << ' hidden' - end - link_to(label && title || '', url, options) - end - - # this stacks the text onto the locale icon (actually a comment balloon) - def locale_text_icon(text) - content_tag(:span, class: 'fa-stack') do - content_tag(:i, '', class: 'fa fa-comment') << content_tag(:strong, text) - end - end - end end diff --git a/core/app/helpers/refinery/translation_helper.rb b/core/app/helpers/refinery/translation_helper.rb index d211543bb75..8c98f9386a9 100644 --- a/core/app/helpers/refinery/translation_helper.rb +++ b/core/app/helpers/refinery/translation_helper.rb @@ -14,5 +14,16 @@ def t(key, **options) def translated_field(record, field) Refinery::TranslatedFieldPresenter.new(record).call(field) end + + def locales_with_titles(record, title_field, include_current: true) + translations = record.translations.where.not(title_field, nil ) + translations = translations.where.not(locale: current_locale) unless include_current + translations.map(&:locale).sort_by { |t| Refinery::I18n.frontend_locales.index(t.to_sym) } + end + + def current_locale?(locale) = locale.to_sym == Refinery::I18n.current_locale + def current_locale = Refinery::I18n.current_locale.to_s + def current_language = Refinery::I18n.locales[Refinery::I18n.current_locale] + def locale_language(locale) = Refinery::I18n.locales[locale.to_sym] end end From 3176ae817d42f26c2f4ec953b76cf00aa5f3b4e4 Mon Sep 17 00:00:00 2001 From: Anita Date: Sat, 1 Feb 2025 14:30:33 +0800 Subject: [PATCH 25/74] refactor(assets): stylesheets use simple selectors for file_type icons, use IconHelper to match file_type to icon use grid layout for images grid --- .../refinery/components/_file_type_icons.scss | 36 +++++ .../refinery/components/_icons.scss | 145 ++++++++++++------ .../stylesheets/refinery/global/_colours.scss | 7 +- .../stylesheets/refinery/mixins/_images.scss | 17 +- .../assets/stylesheets/refinery/refinery.scss | 1 + core/app/helpers/refinery/icon_helper.rb | 51 ++++++ .../refinery/admin/images_controller.rb | 15 +- 7 files changed, 207 insertions(+), 65 deletions(-) create mode 100644 core/app/assets/stylesheets/refinery/components/_file_type_icons.scss create mode 100644 core/app/helpers/refinery/icon_helper.rb diff --git a/core/app/assets/stylesheets/refinery/components/_file_type_icons.scss b/core/app/assets/stylesheets/refinery/components/_file_type_icons.scss new file mode 100644 index 00000000000..34f8f97b4ef --- /dev/null +++ b/core/app/assets/stylesheets/refinery/components/_file_type_icons.scss @@ -0,0 +1,36 @@ +%icon-font { font-family: 'FontAwesome', Helvetica, Arial, sans-serif; } +$adobe_red: B30C00FF; +$ms_word_blue: #1558BB; +$msxl_green: #0F7C42; +$mspp_brown: #C94B26; +/* icons */ + +.pdf_icon {@include icon(file-pdf-o, $adobe_red);} + +.word_icon {@include icon(file-word-o, $ms_word_blue);} + +.excel_icon {@include icon(file-excel-o, $msxl_green);} + +.powerpoint_icon {@include icon(file-powerpoint-o, $mspp_brown);} + +.photo_icon {@include icon(file-photo-o);} + +.picture_icon {@include icon(file-picture-o);} + +.plain_icon {@include icon('file-text')} // text/plain + +.image_icon {@include icon(file-image-o);} + +.zip_icon {@include icon(file-zip-o);} + +.archive_icon {@include icon(file-archive-o);} + +.sound_icon {@include icon(file-sound-o);} + +.audio_icon {@include icon(file-audio-o);} + +.movie_icon {@include icon(file-movie-o);} + +.video_icon {@include icon(file-video-o);} + +.code_icon {@include icon(file-code-o);} diff --git a/core/app/assets/stylesheets/refinery/components/_icons.scss b/core/app/assets/stylesheets/refinery/components/_icons.scss index 41eb7536f7f..bbb3d698c45 100644 --- a/core/app/assets/stylesheets/refinery/components/_icons.scss +++ b/core/app/assets/stylesheets/refinery/components/_icons.scss @@ -2,62 +2,113 @@ /* icons */ -.add_icon {@include icon('plus-circle');} -.back_icon {@include icon('arrow-left');} -.close_icon {@include icon('times-circle')} +.add_icon {@include icon('plus-circle');} + +.back_icon {@include icon('arrow-left');} + +.close_icon {@include icon('times-circle')} .delete_icon, -.delete_section_icon {@include icon('minus-circle', $icon_delete_colour);} - -.download_icon {@include icon('download');} -.edit_email_icon {@include icon('envelope-o');} -.edit_icon {@include icon('edit', $icon_edit_colour);} -.email_icon {@include icon('envelope-o');} -.error_icon {@include icon('question-circle', $icon_error_colour);} -.failure_icon {@include icon('times');} -.folder_icon {@include icon('folder',$icon_folder_colour)} -.go_icon {@include icon('caret-square-o-right');} -.info_icon {@include icon('info-circle');} -.loading_icon {@include icon('spinner'); @extend .fa-spin} -.page_icon {@include icon('file-o',$icon_page_colour)} -.preview_icon {@include icon('eye', $icon_preview_colour)} -.remove_icon {@include icon('unlink', $icon_delete_colour);} -.reorder_done_icon {@include icon('thumbs-o-up')} -.reorder_h_icon {@include icon('exchange');} -.reorder_icon {@include icon('unsorted');} +.delete_section_icon {@include icon('minus-circle', $icon_delete_colour);} + +.download_icon {@include icon('download');} + +.edit_email_icon {@include icon('envelope-o');} + +.edit_icon {@include icon('edit', $icon_edit_colour);} + +.edit_locale_icon {@include icon('edit', $icon_locale_colour, $size: 1.2rem)} + +.locale_icon {@include icon('comment', $icon_locale_colour) } + +.email_icon {@include icon('envelope-o');} + +.error_icon {@include icon('question-circle', $icon_error_colour);} + +.failure_icon {@include icon('times');} + +.folder_icon {@include icon('folder', $icon_folder_colour)} + +.folderopen_icon {@include icon('folder-open', $icon_folder_colour)} + +.go_icon {@include icon('caret-square-o-right');} + +.info_icon {@include icon('info-circle');} + +.loading_icon {@include icon('spinner'); @extend .fa-spin} + +.page_icon {@include icon('file-o', $icon_page_colour)} + +.preview_icon {@include icon('eye', $icon_preview_colour)} + +.remove_icon {@include icon('unlink', $icon_delete_colour);} + +.reorder_done_icon {@include icon('thumbs-o-up')} + +.reorder_h_icon {@include icon('exchange');} + +.reorder_icon {@include icon('unsorted');} + .reorder_icon.loading {@include icon('spinner')} -.search_icon {@include icon('search')} -.settings_icon {@include icon('gear')} -.sortdown_icon {@include icon('sort-down')} -.sortup_icon {@include icon('sort-up')} -.spam_empty_icon {@include icon('trash-o');} -.spam_icon {@include icon('trash');} -.success_icon {@include icon('check');} + +.search_icon {@include icon('search')} + +.settings_icon {@include icon('gear')} + +.sortdown_icon {@include icon('sort-down')} + +.sortup_icon {@include icon('sort-up')} + +.spam_empty_icon {@include icon('trash-o');} + +.spam_icon {@include icon('trash');} + +.success_icon {@include icon('check');} + .switch_view_grid_icon {@include icon('th')} + .switch_view_list_icon {@include icon('list')} -.upload_icon {@include icon('upload');} -.user_comment_icon {@include icon('comment-o');} -.warning_icon {@include icon('warning', $icon_warning_colour)} -.hidden_icon {display:none} +.upload_icon {@include icon('upload');} + +.user_comment_icon {@include icon('comment-o');} + +.warning_icon {@include icon('warning', $icon_warning_colour)} + +.hidden_icon {display: none} + .unhidden_icon {display: inline-block} -// stacked icon/text used for locales -.locale_marker { - display: inline-block; - font-size: 8px; - margin-right: 10px; - position: relative; - top: -8px; - .fa-stack strong { - left: 7px; - line-height: 1em; - position: absolute; - top: 9px; - } + +.locale_icon { + position: relative; + display: flex; + flex-direction: row; + gap: 0.5rem; + align-items: center; + font-size: 1rem; + // icon_base occupies the ::before position + @include icon_base($icon_locale_colour, $size: 1.5rem); + + &::after { + line-height: 1em; + position: absolute; + top: 25%; + left: 10%; + font-size: 0.8rem; + color: #444; + font-weight: bold; + content: attr(locale); + text-transform: uppercase; + + } } // don't want underlines on icons -#content a[class$='icon'] {border-bottom: none} -#content a[class$='icon']:hover {border-bottom: none} +#content { + a[class$='icon'] { + border-bottom: none; + &:hover {border-bottom: none} + } +} diff --git a/core/app/assets/stylesheets/refinery/global/_colours.scss b/core/app/assets/stylesheets/refinery/global/_colours.scss index 0e3f586609c..fb7523f3b52 100644 --- a/core/app/assets/stylesheets/refinery/global/_colours.scss +++ b/core/app/assets/stylesheets/refinery/global/_colours.scss @@ -13,12 +13,9 @@ $icon_preview_colour: $icon_default_colour; $icon_edit_colour: $icon_default_colour; $icon_add_colour: $icon_default_colour; +$current_active_locale: #22A7F2; $action_background_colour: lighten($icon_locale_colour,10%); -$action_border_colour: $icon_locale_colour; - -// $icon_preview_colour: #7f00ff; // violet -// $icon_edit_colour: #007fff; // azure -// $icon_add_colour: #7fff00; // chartreuse +$action_border_colour: #b7e3fc; $index_entry_hover_background: #f1f1f1; $index_entry_actions_background: #f1f1f1; diff --git a/core/app/assets/stylesheets/refinery/mixins/_images.scss b/core/app/assets/stylesheets/refinery/mixins/_images.scss index 75806b34a27..1cbfc94262a 100644 --- a/core/app/assets/stylesheets/refinery/mixins/_images.scss +++ b/core/app/assets/stylesheets/refinery/mixins/_images.scss @@ -1,5 +1,12 @@ @mixin indexImage { - a {font-weight: bold; + padding: 0; + margin: 0; + line-height: 1.2; + a.edit_link { + display: inline-block; + font-weight: bold; + height: fit-content; + &:has(img) { border-bottom: 0} } .alt {font-style: italic} @@ -9,11 +16,8 @@ @mixin gridImage { @include indexImage; - padding: 0; - margin: 0; - line-height: 1.2; - img ~ * { + a:has(img) ~ * { display: inline-block; margin-right: 2px; } @@ -51,15 +55,16 @@ } @mixin imageIndexLayout { + list-style-type: none; width: 100%; padding: 0; + line-height: 1.2; @content; } // apply to #image_grid @mixin imageGridLayout { @include imageIndexLayout { - list-style-type: none; padding-inline-start: 0; display: grid; grid-template-columns: repeat(4, 150px); diff --git a/core/app/assets/stylesheets/refinery/refinery.scss b/core/app/assets/stylesheets/refinery/refinery.scss index e96de19f354..4653091df83 100644 --- a/core/app/assets/stylesheets/refinery/refinery.scss +++ b/core/app/assets/stylesheets/refinery/refinery.scss @@ -14,6 +14,7 @@ @import 'plugins/ui'; @import 'components/icons'; +@import 'components/file_type_icons'; @import 'components/dialog'; @import 'components/submenu'; @import 'components/tooltips'; diff --git a/core/app/helpers/refinery/icon_helper.rb b/core/app/helpers/refinery/icon_helper.rb new file mode 100644 index 00000000000..ab5e2517ce2 --- /dev/null +++ b/core/app/helpers/refinery/icon_helper.rb @@ -0,0 +1,51 @@ +module Refinery + module IconHelper + require 'set' + + # finds icons for documents such as resources and images + # split mime_type, + # handle special case of 'application', + # match type or subtype to icons we support + def mime_type_icon(mime_type) + default_icon = 'file-o' + + type, sub_type = mime_type.split('/') + sub_type = application_type(sub_type) if type == 'application' + + icons = available_icons & Set[type, sub_type] # intersection + icon = icons.empty? ? default_icon : icons.first + + tag.span class: icon_class(icon) + end + + # handle mime-types like 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' and + # 'application/vnd.ms-excel' + def application_type(type) + last_word = type.split('.').last # remove intermediate paths + .gsub('-', '_') # convert dashes to underscore + .to_sym + application_icons[last_word] + end + + def icon_class(icon) = "#{icon}_icon" + + def application_icons = { + document: "word", + ms_excel: "excel", + ms_powerpoint: "powerpoint", + msword: "word", + pdf: "pdf", + presentation: "powerpoint", + x_rar: "archive", + sheet: "excel", + zip: "zip", + } + + # these are the fontawesome-4 icons matching a document type + def available_icons = Set[ + 'archive', 'audio', 'code', 'excel', 'image', + 'movie', 'pdf', 'photo', 'picture', 'plain', + 'powerpoint', 'sound', 'text', 'video', 'word', 'zip', + ] + end +end diff --git a/images/app/controllers/refinery/admin/images_controller.rb b/images/app/controllers/refinery/admin/images_controller.rb index 58819827768..11069486e5b 100644 --- a/images/app/controllers/refinery/admin/images_controller.rb +++ b/images/app/controllers/refinery/admin/images_controller.rb @@ -1,5 +1,6 @@ module Refinery module Admin + require 'will_paginate/array' class ImagesController < ::Refinery::AdminController crudify :'refinery/image', @@ -18,9 +19,8 @@ def new def index search_all_images if searching? - paginate_all_images - - @images = @images.map { |image| ImageObject.new(image, view_context) } + find_all_images if @images.nil? + @images = @images.map{ |image| Refinery::Admin::ImagePresenter.new(image, view_context)}.paginate(page: params[:page], per_page: paginate_per_page) end @@ -167,11 +167,11 @@ def init_dialog end def change_format_if_specified - return unless params[:format].present? + return unless params[:view].present? - format = params[:format].to_sym - if action_name == 'index' && format && Refinery::Images.index_formats.include?(format) - Refinery::Images.preferred_index_format = format + view = params[:view].to_sym + if action_name == 'index' && view && Refinery::Images.index_formats.include?(view) + Refinery::Images.preferred_index_format = view end end @@ -199,5 +199,6 @@ def permitted_image_params ] end end + end end From 082bc6003c59fe43be6a259acbc0fef6e75c0592 Mon Sep 17 00:00:00 2001 From: Anita Date: Mon, 10 Feb 2025 12:10:28 +0800 Subject: [PATCH 26/74] refactor(refinery-images): make ruby3 compatible use data-attributes hash in markup, in place of "data-#{attributes}..." define image_presenters for admin/index unify code for grid_view and list_view presenters: group_presenter: handle the formatting for a group of images list_presenter and grid_presenter inherit from group presenter, image_presenter: generate an entry for the image_index, including clickable links, text information, and actions. configuration: define admin_image_sizes in configuration replace hard coded image sizes from several locations in views and helpers. rename Refinery::Images.image_views to Refinery::Images.index_views // note: looking for a better name for these I tried :formats and :layouts before noting that they, like :views, are magic words for Ruby/Rails and are, therefore, no better. // remove temporary changes to admin/images_controller remove image view objects (using image presenters) split grid_view and list_view in the test contexts --- .../refinery/admin/images_controller.rb | 15 +- .../helpers/refinery/admin/images_helper.rb | 21 +-- .../refinery/admin/grid_presenter.rb | 42 +++++ .../refinery/admin/group_presenter.rb | 68 ++++++++ .../refinery/admin/image_presenter.rb | 147 ++++++++++++++++++ .../refinery/admin/list_presenter.rb | 23 +++ images/app/viewobjects/image_object.rb | 58 ------- .../refinery/admin/images/_actions.html.erb | 8 +- .../admin/images/_existing_image.html.erb | 16 +- .../refinery/admin/images/_image.html.erb | 34 +--- .../refinery/admin/images/_images.html.erb | 29 ++-- .../refinery/admin/images/_list_view.html.erb | 10 -- .../refinery/admin/images/_records.html.erb | 4 +- images/config/locales/en.yml | 6 + .../initializers/refinery/images.rb.erb | 6 +- images/lib/refinery/images/configuration.rb | 12 +- images/lib/refinery/images/engine.rb | 2 - images/spec/factories/image.rb | 6 +- .../shared_contexts/admin_images_tab.rb | 12 +- .../spec/support/shared_contexts/grid_view.rb | 8 + .../spec/support/shared_contexts/list_view.rb | 12 ++ .../support/shared_contexts/many_images.rb | 2 +- .../spec/support/shared_contexts/one_image.rb | 2 +- .../visual_editor_add_image.rb | 27 ++-- .../support/shared_examples/image_deleter.rb | 26 ++-- .../support/shared_examples/image_indexer.rb | 79 ++++------ .../shared_examples/image_previewer.rb | 2 +- .../shared_examples/image_translator.rb | 4 +- .../support/shared_examples/image_uploader.rb | 4 +- images/spec/support/spec_helper.rb | 12 +- 30 files changed, 451 insertions(+), 246 deletions(-) create mode 100644 images/app/presenters/refinery/admin/grid_presenter.rb create mode 100644 images/app/presenters/refinery/admin/group_presenter.rb create mode 100644 images/app/presenters/refinery/admin/image_presenter.rb create mode 100644 images/app/presenters/refinery/admin/list_presenter.rb delete mode 100644 images/app/viewobjects/image_object.rb delete mode 100644 images/app/views/refinery/admin/images/_list_view.html.erb create mode 100644 images/spec/support/shared_contexts/grid_view.rb create mode 100644 images/spec/support/shared_contexts/list_view.rb diff --git a/images/app/controllers/refinery/admin/images_controller.rb b/images/app/controllers/refinery/admin/images_controller.rb index 11069486e5b..a58cdbdaec1 100644 --- a/images/app/controllers/refinery/admin/images_controller.rb +++ b/images/app/controllers/refinery/admin/images_controller.rb @@ -9,7 +9,7 @@ class ImagesController < ::Refinery::AdminController sortable: false, conditions: 'parent_id IS NULL' - before_action :change_format_if_specified, :init_dialog + before_action :change_view_if_specified, :init_dialog def new @image = ::Refinery::Image.new if @image.nil? @@ -17,13 +17,6 @@ def new @url_override = refinery.admin_images_path(dialog: from_dialog?) end - def index - search_all_images if searching? - find_all_images if @images.nil? - @images = @images.map{ |image| Refinery::Admin::ImagePresenter.new(image, view_context)}.paginate(page: params[:page], per_page: paginate_per_page) - end - - # This renders the image insert dialog def insert self.new if @image.nil? @@ -166,12 +159,12 @@ def init_dialog @conditions = params[:conditions] end - def change_format_if_specified + def change_view_if_specified return unless params[:view].present? view = params[:view].to_sym - if action_name == 'index' && view && Refinery::Images.index_formats.include?(view) - Refinery::Images.preferred_index_format = view + if action_name == 'index' && view && Refinery::Images.index_views.include?(view) + Refinery::Images.preferred_index_view = view end end diff --git a/images/app/helpers/refinery/admin/images_helper.rb b/images/app/helpers/refinery/admin/images_helper.rb index 8f7dd1a1d1f..86e44465f4d 100644 --- a/images/app/helpers/refinery/admin/images_helper.rb +++ b/images/app/helpers/refinery/admin/images_helper.rb @@ -1,23 +1,26 @@ module Refinery module Admin module ImagesHelper - def other_image_views - Refinery::Images.image_views.reject do |image_view| - image_view.to_s == Refinery::Images.preferred_image_view.to_s + def other_index_views + Refinery::Images.index_views.reject do |view| + view.to_s == Refinery::Images.preferred_index_view.to_s end end def thumbnail_urls(image) - thumbnail_urls = { - :"data-original" => image_path(image.url), - :"data-grid" => image_path(image.thumbnail(:geometry => '135x135#c').url) + thumbnails = { + original: image_path(image.url), + grid: image_path(image.thumbnail(geometry: Refinery::Images.admin_image_sizes[:thumbnail]).url) } - Refinery::Images.user_image_sizes.sort_by{ |key, geometry| geometry}.each do |size, pixels| - thumbnail_urls[:"data-#{size.to_s.parameterize}"] = image_path(image.thumbnail(:geometry => pixels).url) + Refinery::Images.user_image_sizes.sort_by { |key, geometry| geometry }.each do |size, pixels| + thumbnails[size.to_s.parameterize] = image_path(image.thumbnail(geometry: pixels).url) end + { data: thumbnails } + end - thumbnail_urls + def localized(date) + ::I18n.l(Date.parse(date)) end end end diff --git a/images/app/presenters/refinery/admin/grid_presenter.rb b/images/app/presenters/refinery/admin/grid_presenter.rb new file mode 100644 index 00000000000..c81f5b1b089 --- /dev/null +++ b/images/app/presenters/refinery/admin/grid_presenter.rb @@ -0,0 +1,42 @@ +module Refinery + module Admin + + # Refinery::Admin::GridPresenter is a class intended for presenting + # grouped records in a grid format within the Refinery CMS admin interface. + # + # It inherits from Refinery::Admin::GroupPresenter and overrides specific + # properties to provide custom behavior suitable for grid presentations. + # + # The primary features provided by this class include: + # - Custom grouping of records based on predefined logic. + # - Control over headers, identity keys, and displayed content organization. + # + # == Inclusions + # This class includes the following modules to extend functionality: + # - Refinery::Admin::ImagesHelper: Provides helper methods for working with images. + # - ActionView::Helpers::TagHelper: Supplies methods for creating HTML tags. + # + # == Attributes + # - @group_headers: Controls whether headers for each record group are displayed. Defaults to `false`. + # - @identity_keys: Represents the keys used to identify and present record attributes, including thumbnail, title, and alt text attributes. + # - @header: Stores header configurations, initialized as `nil`. + # - @groups: A lambda function designed to group records by associating them with the current date. + # + # == Initialization + # When initialized, `GridPresenter` accepts an execution `context` (e.g., a controller, view, etc.) + # and propagates this context to the superclass. + class GridPresenter < GroupPresenter + include Refinery::Admin::ImagesHelper + include ActionView::Helpers::TagHelper + + def initialize(context) + super(context) + @group_headers = false + @identity_keys = [:thumbnail, :title, :alt] + @header = nil + @groups = ->(records) { [[Date.today, records]] } + end + + end + end +end diff --git a/images/app/presenters/refinery/admin/group_presenter.rb b/images/app/presenters/refinery/admin/group_presenter.rb new file mode 100644 index 00000000000..74f86696117 --- /dev/null +++ b/images/app/presenters/refinery/admin/group_presenter.rb @@ -0,0 +1,68 @@ +module Refinery + module Admin + + # Refinery::Admin::GroupPresenter + # + # This class represents a presenter for organizing and displaying + # groups with associated metadata in a structured format. + # + # The presenter utilizes several helper modules for enhanced + # functionality and template rendering support. + # + # == Includes: + # * `Refinery::Admin::ImagesHelper` - Provides image-related functionality. + # * `ActionView::Helpers::TagHelper` - Used to create HTML-like tags dynamically. + # + # == Attributes: + # * `context` [Object] - Represents the context within which the presenter operates. + # * `groups` [Object] - Holds a collection of groups to be managed. + # * `group_classes` [Array] - Provides a list of CSS class names applied to the group tag. + # * `group_header` [Proc] - Callable object (e.g., lambda) to determine the header for each group. + # * `group_headers` [Boolean] - Controls whether group headers are displayed. + # * `group_tag` [Symbol] - Defines the tag used to wrap groups, e.g., `:ul`. + # * `group_wrapper` [Proc] - Customizable wrapper for rendering group collections. + # * `header` [Proc] - Callable object to format the header for individual groups. + # * `header_tag` [Symbol] - Specifies the tag for rendering headers, e.g., `:h3`. + # * `identity_keys` [Object] - Additional attribute for handling unique identity keys. + # + # == Public Instance Methods: + # - `initialize(context)` + # Initializes a new instance of the presenter within a given context, + # setting default values for various attributes like headers and tags. + # + # - `group_wrapper(&block)` + # Wraps the rendering of groups using a specified tag. + # This is customizable via the `group_tag` and `group_classes` attributes. + # A block must be passed to render the inner contents. + # + # - `group_header(date)` + # Constructs a header for individual groups based on a provided date. + # The format of the header is defined by the `header` callable and + # displayed using the specified `header_tag`. + class GroupPresenter + include Refinery::Admin::ImagesHelper + include ActionView::Helpers::TagHelper + + attr_accessor :context, :groups, :group_classes, :group_header, :group_headers, :group_tag, :group_wrapper, + :header, :header_tag, :identity_keys + + def initialize(context) + @context = context + @group_headers = true + @group_tag = :ul + @group_classes = [:image_group] + @header_tag = :h3 + end + + def group_wrapper(&block) + context.tag group_tag, class: group_classes do + yield block + end + end + + def group_header(date) + tag.send(header_tag, header.call(date), class: :group_header) + end + end + end +end diff --git a/images/app/presenters/refinery/admin/image_presenter.rb b/images/app/presenters/refinery/admin/image_presenter.rb new file mode 100644 index 00000000000..0da1bf9e626 --- /dev/null +++ b/images/app/presenters/refinery/admin/image_presenter.rb @@ -0,0 +1,147 @@ +module Refinery + module Admin + + # Refinery::Admin::ImagePresenter + # + # This class represents a presenter for managing and displaying image data + # in the Refinery admin interface. It provides methods to format image-related + # data for display and to generate necessary HTML components for interacting + # with images in the admin UI. + # + # === Includes + # - ActionView::RecordIdentifier: Provides methods for DOM ID generation for records. + # - ActionView::Helpers::UrlHelper: Allows generation of URLs and links. + # - ActionView::Helpers::TagHelper: Provides methods for generating HTML tags. + # - ActionView::Context: Allows rendering within the correct context. + # - Refinery::TranslationHelper: Adds support for translation-related helper methods. + # - Refinery::Admin::ImagesHelper: Includes specific helper methods for working with images. + # - Refinery::ActionHelper: Provides additional action-based helper methods. + # + # === Attributes + # - `image` [Reader]: The image object being presented. + # - `context` [Reader]: Context of the current view, often required for generating paths and URLs. + # - `index_keys` [Reader]: Used to specify the attributes displayed in the index table for images. + # - `i18n_scope` [Reader]: Specifies the internationalization scope for localization of text. + # - `title` [Writer]: Sets the title for the image. + # - `alt` [Writer]: Sets the alt text for the image. + # - `filename` [Writer]: Sets the filename for the image. + # - `translations` [Writer]: Stores translations for the image fields. + # - `edit_attributes` [Writer]: Stores attributes to be used for editing the image. + # - `delete_attributes` [Writer]: Stores attributes to be used for deleting the image. + # - `preview_attributes` [Writer]: Stores attributes for image preview options. + # + # === Constants + # - `IndexEntry`: Defines a structure to hold index entry data, including the image ID, + # edit link, text elements, locales, and actionable items. + # + # === Instance Methods + # + # - `initialize(image, context, scope = nil)`: + # Constructor that initializes the ImagePresenter with the given image object, view context, + # and optional internationalization scope. + # + # - `index_entry(index_keys)`: + # Generates an index entry for the image, including its ID, edit link, text elements, + # and actions. Accepts an array of keys for specifying index fields. + # These keys differ between the grid_view and list_view, and other views could be added + # + # - `link_to_edit(edit_key)`: + # Creates a link to edit the image, using the given key for determining the "text" of the link. + # + # - `text_elements(keys)`: + # Generates text elements for display based on the provided keys, wrapping them in HTML spans. + # + # - `thumbnail`: + # Returns the thumbnail for the image, including specific attributes such as its + # URL, description (alt) and title + class ImagePresenter < Refinery::BasePresenter + include ActionView::RecordIdentifier + include ActionView::Helpers::UrlHelper + include ActionView::Helpers::TagHelper + include ActionView::Context + + include Refinery::TranslationHelper + include Refinery::Admin::ImagesHelper + include Refinery::ActionHelper + + attr_reader :image, :context, :index_keys, :i18n_scope + attr_writer :title, :alt, :filename, :translations, :edit_attributes, :delete_attributes, :preview_attributes + delegate_missing_to :image + + IndexEntry = Struct.new('ImageEntry', :id, :edit_link, :text_elements, :locales, :actions) + + def initialize(image, context, scope = nil) + super(image) + @image = image + @context = context + @i18n_scope = scope || 'refinery.admin.images' + end + + def index_entry(index_keys) + edit_key, *text_keys = index_keys + IndexEntry.new( + id: image.id, + edit_link: link_to_edit(index_keys[0]), + text_elements: text_elements(index_keys[1..]), + actions: tag.span( image_actions, class: :actions) + ) + end + + def link_to_edit(edit_key) + link_to(self.send(edit_key), + context.edit_admin_image_path(image), + { title: ::I18n.t('.edit', title: image.title, scope: i18n_scope), class: :edit_link}) + end + + def text_elements(keys) + keys.reduce(ActiveSupport::SafeBuffer.new) do |buffer, key| + buffer << tag.span(self.send(key), title: ::I18n.t(key, scope: i18n_scope), class: key) + end + end + + def thumbnail + tag.img src: image.thumbnail(geometry: Refinery::Images.admin_image_sizes[:grid], strip: true).url(only_path: true), + alt: image.alt, title: image.title, class: :thumbnail + end + + def title + translated_field(image, :title) + end + + def alt + alt_text = translated_field(@image, :alt) + alt_text unless alt_text === translated_field(image, :title) + end + + def filename + image.image_name + end + + def image_actions + [*edit_actions, delete_action, preview_action].reduce(ActiveSupport::SafeBuffer.new) do |buffer, action| + buffer << action + end + end + + def edit_actions + locales_with_titles(image, 'image_title').reduce(ActiveSupport::SafeBuffer.new) do |buffer, locale| + buffer << edit_in_locale(locale, + url: context.edit_admin_image_path(image), + title: image.title) + end + end + + def preview_action(options = {}) + action_icon(:preview, image.url, ::I18n.t('.view_live_html', scope: i18n_scope), + options: options) + end + + def delete_action(options = {}) + action_icon(:delete, context.admin_image_path(image), + ::I18n.t('.delete', scope: i18n_scope), + data: { confirm: ::I18n.t('message', scope: 'refinery.admin.delete', title: image.title) }, + **options ) + end + end + end +end diff --git a/images/app/presenters/refinery/admin/list_presenter.rb b/images/app/presenters/refinery/admin/list_presenter.rb new file mode 100644 index 00000000000..56edd2b2d5e --- /dev/null +++ b/images/app/presenters/refinery/admin/list_presenter.rb @@ -0,0 +1,23 @@ +module Refinery + module Admin + class ListPresenter < GroupPresenter + include Refinery::Admin::ImagesHelper + include ActionView::Helpers::TagHelper + + # Initialize the presenter with a context, a lambda for headers, and a lambda for groups. + # + # @param context [Object] An object that provides the necessary methods for grouping and rendering. + # @param header [Proc] Callable for formatting group headers. + # @param groups [Proc] Callable for grouping records. + def initialize(context, + header: ->(date) { localized(date) }, + groups: ->(records) { context.group_by_date(records, :updated_at) }) + super(context) + @identity_keys = [:title, :filename, :alt] + @group_headers = true + @header = header + @groups = groups + end + end + end +end diff --git a/images/app/viewobjects/image_object.rb b/images/app/viewobjects/image_object.rb deleted file mode 100644 index e694099e7a4..00000000000 --- a/images/app/viewobjects/image_object.rb +++ /dev/null @@ -1,58 +0,0 @@ -class ImageObject - include Refinery::TranslationHelper - include Refinery::ImageHelper - - attr_reader :image, :context, :i18n_scope - attr_writer :title, :alt, :filename, :translations, :edit_attributes, :delete_attributes, :preview_attributes - delegate_missing_to :image - - Action = Struct.new('Action', :href, :title, :text, :data, :options, keyword_init: true) - - def initialize(image, context, scope=nil) - @image = image - @context = context - @i18n_scope = scope || 'refinery.admin.images' - end - - def title - translated_field(image, :image_title).titleize - end - - def alt - translated_field(image, :image_alt).titleize - end - - def grid_image_attributes - { src: image.thumbnail(geometry: Refinery::Images.admin_image_sizes[:grid], strip: true).url, - title: ::I18n.t('edit_title', scope: i18n_scope, title: title) } - end - - def filename - image.image_name - end - - def locales_with_titles - image.translations - .reject { |i| i.image_title.blank? } - .map(&:locale).sort_by { |t| Refinery::I18n.frontend_locales.index(t.to_sym) } - end - - def edit_action(options = {}) - Action.new(href: context.edit_admin_image_path(image, switch_locale: options.delete(:switch_locale)), - title: I18n.t('.edit', scope: i18n_scope), - text: I18n.t('.edit_title', title: image.title, scope: i18n_scope), - options: options) - end - - def preview_action(options = {}) - Action.new(href: image.url, title: I18n.t('.view_live_html', scope: i18n_scope), options: options) - end - - def delete_action(options = {}) - Action.new(href: context.admin_image_path(image), - title: I18n.t('.delete', scope: i18n_scope), - data:{ confirm: I18n.t('message', scope: 'refinery.admin.delete', title: image.title) }, - options: { class: [:delete, 'confirm-delete'], **options }) - end - -end diff --git a/images/app/views/refinery/admin/images/_actions.html.erb b/images/app/views/refinery/admin/images/_actions.html.erb index bc676070a98..28d0594f175 100644 --- a/images/app/views/refinery/admin/images/_actions.html.erb +++ b/images/app/views/refinery/admin/images/_actions.html.erb @@ -3,11 +3,11 @@
  • <%= action_label :add, refinery.new_admin_image_path(dialog: true), t('.create_new_image') %>
  • - <% other_image_views.each do |image_view| %> + <% other_index_views.each do |view| %>
  • - <%= action_label :"switch_view_#{image_view}", - refinery.admin_images_path(view: image_view, page: params[:page]), - t('switch_to', scope: 'refinery.admin.images.index.view', view_name: t("#{image_view}", scope: 'refinery.admin.images.index.view')) + <%= action_label :"switch_view_#{view}", # reference to the view icon + refinery.admin_images_path(view: view, page: params[:page]), + I18n.t('switch_to', view_name: view, scope: 'refinery.admin.images.index.view') %>
  • <% end %> diff --git a/images/app/views/refinery/admin/images/_existing_image.html.erb b/images/app/views/refinery/admin/images/_existing_image.html.erb index 9d108d76769..30dc0b26d4d 100644 --- a/images/app/views/refinery/admin/images/_existing_image.html.erb +++ b/images/app/views/refinery/admin/images/_existing_image.html.erb @@ -1,24 +1,28 @@ +<% image_size = Refinery::Images.admin_image_sizes[:form] %>
    > - <%= render '/refinery/admin/search', :url => refinery.insert_admin_images_path(params.to_unsafe_h.dup.except('image')) %> + <%= render '/refinery/admin/search', url: refinery.insert_admin_images_path(params.to_unsafe_h.dup.except('image')) %> +
    <% if @images.any? %>
      <% @images.each do |image| -%> <%= content_tag :li, class: ('selected' if @image_id == image.id) do %> - <%= image_fu(image, '106x106#c', { + <%= image_fu(image, image_size, alt: image.title, title: image.title, - id: "image_#{image.id}", - 'data-id': image.id - }.merge(thumbnail_urls(image)))-%> + id: dom_id(image), + data: { + id: image.id, + **thumbnail_urls(image) }) + %> <% if image.crops.any? %>
        <% image.crops.each do |crop| %> <%= content_tag :li, class: ('selected' if @image_id == image.id) do %> - <%= image_fu(crop, '106x106>', { + <%= image_fu(crop, image_size, { alt: crop.parent.alt, title: crop.parent.title, id: "image_#{crop.id}", diff --git a/images/app/views/refinery/admin/images/_image.html.erb b/images/app/views/refinery/admin/images/_image.html.erb index 07c5f26e9a3..d1ccdb6cfc7 100644 --- a/images/app/views/refinery/admin/images/_image.html.erb +++ b/images/app/views/refinery/admin/images/_image.html.erb @@ -1,31 +1,5 @@ -
      • - <% edit_action = image.edit_action %> - <% preview_action = image.preview_action %> - <% delete_action = image.delete_action %> - - <% view_identity_elements = [] %> - <% case Refinery::Images.preferred_image_view %> - <% when 'list' %> - <% view_identity_elements = [ - link_to(image.title, edit_action.href, class: :title, title: edit_action.text), - tag.span(image.filename, class: :filename) - ] %> - <% when 'grid' %> - <% view_identity_elements = [ - link_to(image.img_element, edit_action.href, title: edit_action.text), - tag.span(image.title, class: "title", title: "t('.title')") - ] %> - <% else %> - <% Rails.logger.error "Undefined image view #{Refinery::Images.preferred_image_view} " %> - <% end %> - <%= view_identity_elements.compact.join(' ').html_safe %> - - <%= image.alt unless image.alt == image.title %> - <%= edit_in_locales(image, image.locales_with_titles) %> - - - <%= action_icon :preview, preview_action.href, preview_action.title, preview_action.options %> - <%= action_icon :edit, edit_action.href, edit_action.title, edit_action.options %> - <%= action_icon :delete, delete_action.href, delete_action.title, **delete_action.options %> - +
      • + <%= image.edit_link %> + <%= image.text_elements %> + <%= image.actions %>
      • diff --git a/images/app/views/refinery/admin/images/_images.html.erb b/images/app/views/refinery/admin/images/_images.html.erb index 7b5faf6bcfe..f312a72a029 100644 --- a/images/app/views/refinery/admin/images/_images.html.erb +++ b/images/app/views/refinery/admin/images/_images.html.erb @@ -1,19 +1,18 @@ -<%#= will_paginate @images if Refinery::Admin::ImagesController.pageable? %> -<% images = @images %> -<% view = Refinery::Images.preferred_image_view %> -<% if view == 'grid' - groups = [[Date.today, images]] - group_header = nil - else - groups = group_by_date(images, :updated_at) - - end +<%= will_paginate @images if Refinery::Admin::ImagesController.pageable? %> +<% + view = Refinery::Images.preferred_index_view || 'list' + presenter = Refinery::Admin.const_get("#{view}_presenter".camelize).new(self) + images = @images.map { |image| Refinery::Admin::ImagePresenter.new(image, self) } %> -<%= tag.ul id: "images_#{view}", classes: [*%w(clearfix pagination_frame images_list), pagination_selector] %> -<% groups.each do |(date, group)| %> -

        <%= l(Date.parse(date.to_s), format: :long ) %>

        - <%= tag.ul class: :images_list do %> - <%= render partial: 'refinery/admin/images/image', collection: group, view: "#{view}_view" %> +<% groups = presenter.groups.call(images) %> +<%= tag.ul id: "image_index", class: ['pagination_frame', view, pagination_selector].compact do %> + + <% groups.each do |group_date, group_images| %> + <%= presenter.group_header(group_date) if presenter.group_headers %> + <% group_images.each do |image| %> + <%= render 'refinery/admin/images/image', image: image.index_entry(presenter.identity_keys) %> + <% end %> <% end %> + <% end %> diff --git a/images/app/views/refinery/admin/images/_list_view.html.erb b/images/app/views/refinery/admin/images/_list_view.html.erb deleted file mode 100644 index f2479074e17..00000000000 --- a/images/app/views/refinery/admin/images/_list_view.html.erb +++ /dev/null @@ -1,10 +0,0 @@ -
        - <% group_by_date(@images, :updated_at).each do |container| %> - <% date_time = (image_group = container.last).first.updated_at %> - <% date = Date.parse(date_time.to_s) %> -

        <%= l(date, format: :long ) %>

        -
          - <%= render partial: 'list_view_image', collection: image_group %> -
        - <% end %> -
        diff --git a/images/app/views/refinery/admin/images/_records.html.erb b/images/app/views/refinery/admin/images/_records.html.erb index 4af5e907d29..bc833ba283d 100644 --- a/images/app/views/refinery/admin/images/_records.html.erb +++ b/images/app/views/refinery/admin/images/_records.html.erb @@ -1,11 +1,11 @@ -<%= render 'refinery/admin/search_header', :url => refinery.admin_images_path %> +<%= render 'refinery/admin/search_header', url: refinery.admin_images_path %>
        <% if @images.any? %> <%= render 'images' %> <% else %>

        <% if searching? %> - <%= t('no_results', :scope => 'refinery.admin.search') %> + <%= t('no_results', scope: 'refinery.admin.search') %> <% else %> <%= t('.no_images_yet') %> diff --git a/images/config/locales/en.yml b/images/config/locales/en.yml index b6cc910d9b8..76bc2ae4ba3 100644 --- a/images/config/locales/en.yml +++ b/images/config/locales/en.yml @@ -48,9 +48,15 @@ en: insert: existing_image: Library new_image: Upload + alt: Description + filename: File name + title: Title activerecord: models: refinery/image: image + attributes: + image_title: Title + image_alt: Description errors: models: refinery/image: diff --git a/images/lib/generators/refinery/images/templates/config/initializers/refinery/images.rb.erb b/images/lib/generators/refinery/images/templates/config/initializers/refinery/images.rb.erb index 8a78c100beb..6c1c8fcd2a5 100644 --- a/images/lib/generators/refinery/images/templates/config/initializers/refinery/images.rb.erb +++ b/images/lib/generators/refinery/images/templates/config/initializers/refinery/images.rb.erb @@ -26,10 +26,10 @@ Refinery::Images.configure do |config| # config.allowed_mime_types = <%= Refinery::Images.allowed_mime_types.inspect %> # Configure image view options - # config.image_views = <%= Refinery::Images.image_views.inspect %> + # config.index_views = <%= Refinery::Images.index_views.inspect %> - # Configure default image view - # config.preferred_image_view = <%= Refinery::Images.preferred_image_view.inspect %> + # Configure default index view + # config.preferred_index_view = <%= Refinery::Images.preferred_index_view.inspect %> # Configure Dragonfly. # Refer to config/initializers/refinery/dragonfly.rb for the full list of dragonfly configurations which can be used. diff --git a/images/lib/refinery/images/configuration.rb b/images/lib/refinery/images/configuration.rb index b6348976402..961f8440210 100644 --- a/images/lib/refinery/images/configuration.rb +++ b/images/lib/refinery/images/configuration.rb @@ -6,7 +6,7 @@ module Images config_accessor :max_image_size, :pages_per_dialog, :pages_per_admin_index, :pages_per_dialog_that_have_size_options, :user_image_sizes, :user_image_ratios, - :index_formats, :preferred_index_format, :admin_image_sizes, + :index_views, :preferred_index_view, :admin_image_sizes, :allowed_mime_types, :allowed_mime_types_msg self.max_image_size = 5_242_880 @@ -24,13 +24,17 @@ module Images '1:1': 1 } + # note: these dimensions were previously hard coded. self.admin_image_sizes = { - grid: '149x149#c' + form: '106x106>', + grid: '135x135#c', + thumbnail: '149x149#c#', + crop: '255x255#c' } self.allowed_mime_types = %w[image/jpeg image/png image/gif image/tiff] self.allowed_mime_types_msg = self.allowed_mime_types.to_sentence(last_word_connector: ' or ') - self.index_formats = [:grid, :list] - self.preferred_index_format = :grid + self.index_views = [:grid, :list] + self.preferred_index_view = :grid # Images should always use these changes to the dragonfly defaults self.dragonfly_name = :refinery_images diff --git a/images/lib/refinery/images/engine.rb b/images/lib/refinery/images/engine.rb index b3d9fe624fe..1ae5fd43638 100644 --- a/images/lib/refinery/images/engine.rb +++ b/images/lib/refinery/images/engine.rb @@ -6,8 +6,6 @@ class Engine < ::Rails::Engine isolate_namespace Refinery engine_name :refinery_images - config.autoload_paths += %W( #{config.root}/lib #{config.root}/refinery/images/view_objects ) - initializer 'attach-refinery-images-with-dragonfly', :before => :finisher_hook do |app| ::Refinery::Dragonfly.configure!(::Refinery::Images) ::Refinery::Dragonfly.attach!(app, ::Refinery::Images) diff --git a/images/spec/factories/image.rb b/images/spec/factories/image.rb index 212fb3425c3..6d201468180 100644 --- a/images/spec/factories/image.rb +++ b/images/spec/factories/image.rb @@ -1,13 +1,13 @@ FactoryBot.define do - factory :image, :class => ::Refinery::Image do + factory :image, class: ::Refinery::Image do image { Refinery.roots('refinery/images').join("spec/fixtures/beach.jpeg") } end - factory :alternate_image, :class => ::Refinery::Image do + factory :alternate_image, class: ::Refinery::Image do image { Refinery.roots('refinery/images').join("spec/fixtures/beach-alternate.jpeg") } end - factory :another_image, :class => ::Refinery::Image do + factory :another_image, class: ::Refinery::Image do image { Refinery.roots('refinery/images').join("spec/fixtures/fathead.png") } end end diff --git a/images/spec/support/shared_contexts/admin_images_tab.rb b/images/spec/support/shared_contexts/admin_images_tab.rb index 0f337286873..f50684837fe 100644 --- a/images/spec/support/shared_contexts/admin_images_tab.rb +++ b/images/spec/support/shared_contexts/admin_images_tab.rb @@ -1,17 +1,13 @@ shared_context 'admin images tab' do - + let(:initial_path){refinery.admin_images_path} let(:open_upload_dialog) { find('a', text: ::I18n.t('create_new_image', scope: 'refinery.admin.images.actions')).click } let(:select_upload) {} let(:initialize_context) {} let(:index_in_frame) {false} let(:dialog_frame_id) {'dialog_iframe'} - let(:initial_path) { refinery.admin_images_path(view: %w(grid list).sample) } let(:index_item_selector) {'#records li'} - let(:gridview_img_selector) {' > img'} - let(:gridview_title_selector) {'[title]'} - let(:gridview_alt_selector) {'[alt]'} - let(:listview_title_selector) {' > span.title'} - let(:listview_filename_selector) {' > span.preview'} - + let(:image_item) { [index_item_selector, img_selector].join(' ') } + let(:image_title) { [index_item_selector, title_selector].join(' ') } + let(:image_alt) { [index_item_selector, alt_selector ].join(' ') } end diff --git a/images/spec/support/shared_contexts/grid_view.rb b/images/spec/support/shared_contexts/grid_view.rb new file mode 100644 index 00000000000..1c104fe4185 --- /dev/null +++ b/images/spec/support/shared_contexts/grid_view.rb @@ -0,0 +1,8 @@ +shared_context 'grid view' do + let(:initial_path) { refinery.admin_images_path + '?view=grid' } + let(:img_selector) { '> a img' } + let(:alt_selector) { 'span.alt' } + let(:title_selector) { 'span.title' } + let(:view_selector) {'#image_index.grid'} + let(:alt_view) {'list'} +end diff --git a/images/spec/support/shared_contexts/list_view.rb b/images/spec/support/shared_contexts/list_view.rb new file mode 100644 index 00000000000..d8d3063f7d6 --- /dev/null +++ b/images/spec/support/shared_contexts/list_view.rb @@ -0,0 +1,12 @@ +shared_context 'list view' do + let(:initial_path) { refinery.admin_images_path + '?view=list' } + let(:img_selector) { '' } + let(:alt_selector) { 'span.alt' } + let(:title_selector) { 'a.edit_link' } + let(:image_title) { [index_item_selector, title_selector].join(' ') } + let(:view_selector) {'#image_index.list'} + + let(:filename_selector) { '.filename' } + let(:filename) { [index_item_selector, filename_selector ].join(' ') } + let(:alt_view) {'grid'} +end diff --git a/images/spec/support/shared_contexts/many_images.rb b/images/spec/support/shared_contexts/many_images.rb index 466c4a20157..c51b1a3d9d1 100644 --- a/images/spec/support/shared_contexts/many_images.rb +++ b/images/spec/support/shared_contexts/many_images.rb @@ -1,5 +1,5 @@ shared_context 'many images' do - let!(:image) { FactoryBot.create(:image) } + let!(:image) { FactoryBot.create(:image, image_alt: "A Beach", image_title: "Peachy Beachy") } let!(:alt_image) { FactoryBot.create(:alternate_image) } let!(:another_image) { FactoryBot.create(:another_image) } end diff --git a/images/spec/support/shared_contexts/one_image.rb b/images/spec/support/shared_contexts/one_image.rb index a9cef818d67..b80b0145698 100644 --- a/images/spec/support/shared_contexts/one_image.rb +++ b/images/spec/support/shared_contexts/one_image.rb @@ -1,3 +1,3 @@ shared_context "one image" do - let!(:image) { FactoryBot.create(:image) } + let!(:image) { FactoryBot.create(:image, image_alt: "A Beach", image_title: "Peachy Beachy") } end diff --git a/images/spec/support/shared_contexts/visual_editor_add_image.rb b/images/spec/support/shared_contexts/visual_editor_add_image.rb index b91f3c4158a..992f86806d7 100644 --- a/images/spec/support/shared_contexts/visual_editor_add_image.rb +++ b/images/spec/support/shared_contexts/visual_editor_add_image.rb @@ -1,31 +1,30 @@ shared_context 'Visual Editor - add image' do - let(:page_for_image) { - page = Refinery::Page.create :title => "Add Image to me" + page = Refinery::Page.create title: "Add Image to me" # we need page parts so that there's a visual editor Refinery::Pages.default_parts.each_with_index do |default_page_part, index| - page.parts.create(:title => default_page_part, :slug => default_page_part, :body => nil, :position => index) + page.parts.create(title: default_page_part, slug: default_page_part, body: nil, position: index) end page } - let(:initialize_context) {click_on('Add Image') } + let(:initial_path) { refinery.edit_admin_page_path(page_for_image) } + let(:initialize_context) { click_link('Add Image') } + let(:add_image_selector) { '.page_part_body li.wym_tools_image > a' } let(:open_upload_dialog) {} - let(:select_upload) {choose(::I18n.t('new_image', :scope => 'refinery.admin.images.insert')) } - - let(:initial_path) { refinery.edit_admin_page_path(page_for_image) } + let(:select_upload) { choose(::I18n.t('new_image', scope: 'refinery.admin.images.insert')) } # let(:target_image) { find(:xpath, "//img[@title='Beach']")} # let(:url){target_image["data-#{size}"]} - let(:index_in_frame) {true} - let(:dialog_frame_id) {'dialog_frame'} - let(:index_item_selector) {'#existing_image_area_content ul li img'} - let(:title_attribute_selector) {'[title]'} - let(:alt_attribute_selector) {'[alt]'} + let(:index_in_frame) { true } + let(:dialog_frame_id) { 'dialog_frame' } + let(:index_item_selector) { '#existing_image_area_content ul li img' } + let(:title_attribute_selector) { '[title]' } + let(:alt_attribute_selector) { '[alt]' } - let(:image_title_selector) {|title| '//img[@alt="' << title << '"]'} - let(:image_size_selector) {|size| '//a[@href="#' << size << '"]'} + let(:image_title_selector) { |title| '//img[@alt="' << title << '"]' } + let(:image_size_selector) { |size| '//a[@href="#' << size << '"]' } end diff --git a/images/spec/support/shared_examples/image_deleter.rb b/images/spec/support/shared_examples/image_deleter.rb index 6c04e4f8de6..2598bc7000d 100644 --- a/images/spec/support/shared_examples/image_deleter.rb +++ b/images/spec/support/shared_examples/image_deleter.rb @@ -8,23 +8,27 @@ def deleting_an_image first("#records li").click_link(::I18n.t('delete', scope: 'refinery.admin.images')) end + def delete_an_image + first("#records li").click_link(::I18n.t('delete', scope: 'refinery.admin.images')) + end + let(:image_count) { [Refinery::Image.count, Refinery::Images.pages_per_admin_index].min } it 'has a delete image link for each image' do - expect(page).to have_selector('#records.images li a.confirm-delete', count: image_count) - end - - it "removes an image" do - expect { deleting_an_image }.to change(Refinery::Image, :count).by(-1) + expect(page).to have_selector('#records.images li a.delete', count: image_count) end - it 'says the image has been removed' do - image_title = page.has_selector?('#image_grid') ? find("#image_grid li:first").first("img")[:title] : - find("#image_list li:first").first("span.title").text - expect(image_title).to be_present + %i(grid list).each do |view| + init_index_view(view) do + it "removes an image and reports the result" do + expect(page).to have_selector(view_selector) + expect(page).to have_selector('#records li a.delete', count: image_count) - first(".images_list li:first").click_link(::I18n.t('delete', scope: 'refinery.admin.images')) - expect(page).to have_content(::I18n.t('destroyed', scope: 'refinery.crudify', what: "'#{image_title}'")) + title = first('#records li').find(title_selector).text + expect { deleting_an_image }.to change(Refinery::Image, :count).by(-1) + expect(page).to have_content(::I18n.t('destroyed', scope: 'refinery.crudify', what: "'#{title}'")) + end + end end end diff --git a/images/spec/support/shared_examples/image_indexer.rb b/images/spec/support/shared_examples/image_indexer.rb index 216dcbf1914..0c405ef0ee4 100644 --- a/images/spec/support/shared_examples/image_indexer.rb +++ b/images/spec/support/shared_examples/image_indexer.rb @@ -1,6 +1,6 @@ shared_examples_for 'Index' do - let(:image_count) {[Refinery::Image.count, Refinery::Images.pages_per_admin_index].min} + let(:image_count) { [Refinery::Image.count, Refinery::Images.pages_per_admin_index].min } before do raise "please set let(:initial_path)" if initial_path.blank? @@ -17,65 +17,47 @@ expect(page).to have_selector(index_item_selector, count: image_count) end end +end -end # image index - -shared_examples_for 'Index Views' do - - let(:image_count) {[Refinery::Image.count, Refinery::Images.pages_per_admin_index].min} - - before do - raise "please set let(:initial_path)" if initial_path.blank? - ensure_on(initial_path) +shared_examples_for 'All Index Views' do + it 'shows the alt attribute of each image if it different from the title', js: true do + expect(page).to have_selector(image_alt, text: 'A Beach', count: 1) end - context "when in grid view" do - - before { ensure_on(current_path + "?view=grid") } - - it 'shows the images with thumbnails', js: true do - expect(page).to have_selector(index_item_selector << gridview_img_selector, count: image_count) - end + it 'shows the title attribute of an image', js: true do + expect(page).to have_selector(image_title, count: image_count) + end - it 'makes the title attribute of each image available', js: true do - expect(page).to have_selector(index_item_selector << gridview_img_selector << gridview_title_selector, count: image_count) - end + it 'has an option to switch to another view', js: true do + expect(page).to have_content(::I18n.t('switch_to', view_name: alt_view, scope: 'refinery.admin.images.index.view')) + end +end - it 'makes the alt attribute of each image available', js: true do - expect(page).to have_selector(index_item_selector << gridview_img_selector << gridview_alt_selector, count: image_count) - end +shared_examples_for 'Index Views' do + let(:image_count) { [Refinery::Image.count, Refinery::Images.pages_per_admin_index].min } - it 'has an option to switch to list view' do - expect(page).to have_content(::I18n.t('switch_to', view_name: 'list', scope: 'refinery.admin.images.index.view')) + context "when in grid view" do + init_index_view('grid') do + it_behaves_like 'All Index Views' + it 'shows thumbnails', js: true do + expect(page).to have_selector(image_item, count: image_count) + end end - - end # grid view + end # grid view context "when in list view" do - - before do - ensure_on(current_path + "?view=list") - end - - it 'makes the title attribute of each image available', js: true do - expect(page).to have_selector(index_item_selector << listview_title_selector, count: image_count) - end - - it 'makes the filename of each image available' do - expect(page).to have_selector(index_item_selector << listview_filename_selector, count: image_count) + init_index_view('list') do + it_behaves_like 'All Index Views' + it 'show filenames' do + expect(page).to have_selector(filename, count: image_count) + end end + end - it 'has an option to switch to grid view' do - ensure_on(current_path + '?view=list') - - expect(page).to have_content(::I18n.t('switch_to', view_name: 'grid', scope: 'refinery.admin.images.index.view')) - end - end # list view end shared_examples_for 'Index Pagination' do - - let(:image_count) {[Refinery::Image.count, Refinery::Images.pages_per_admin_index].min} + let(:image_count) { [Refinery::Image.count, Refinery::Images.pages_per_admin_index].min } before do raise "please set let(:initial_path)" if initial_path.blank? @@ -111,7 +93,8 @@ it 'has disabled the next link' do expect(page).to have_selector('.next_page.disabled') end + end # last page + + end # pagination - end - end # pagination end # image index diff --git a/images/spec/support/shared_examples/image_previewer.rb b/images/spec/support/shared_examples/image_previewer.rb index 7656d7fdbcb..71206e6137c 100644 --- a/images/spec/support/shared_examples/image_previewer.rb +++ b/images/spec/support/shared_examples/image_previewer.rb @@ -16,7 +16,7 @@ def preview_image end let(:image_url) { - uri = URI(first(:xpath, "//a[@class='preview_icon']")[:href]) + uri = URI(first(:xpath, "//a[contains(concat(' ', @class, ' '), ' preview ')]")[:href]) uri.path << '?' << uri.query } diff --git a/images/spec/support/shared_examples/image_translator.rb b/images/spec/support/shared_examples/image_translator.rb index 4ab18c40494..9b44aab103d 100644 --- a/images/spec/support/shared_examples/image_translator.rb +++ b/images/spec/support/shared_examples/image_translator.rb @@ -16,7 +16,7 @@ click_link "Edit this image" within "#switch_locale_picker" do - click_link "FR" + click_link "fr" end fill_in "Title", :with => "Titre de la première image" @@ -25,7 +25,7 @@ click_button "Save" expect(page).to have_content("'Titre de la première image' was successfully updated.") - expect(Refinery::Image::Translation.count).to eq(1) + expect(Refinery::Image::Translation.count).to eq(2) end end end diff --git a/images/spec/support/shared_examples/image_uploader.rb b/images/spec/support/shared_examples/image_uploader.rb index 74eb90ccaea..927f30758a7 100644 --- a/images/spec/support/shared_examples/image_uploader.rb +++ b/images/spec/support/shared_examples/image_uploader.rb @@ -7,13 +7,13 @@ def uploading_an_image open_upload_dialog - # page.within_frame(dialog_frame_id) do + page.within_frame(dialog_frame_id) do select_upload attach_file 'image_image', image_path fill_in 'image_image_title', with: 'Image With Dashes' fill_in 'image_image_alt', with: "Alt description for image" click_button ::I18n.t('save', scope: 'refinery.admin.form_actions') - # end + end end context 'when the image type is acceptable' do diff --git a/images/spec/support/spec_helper.rb b/images/spec/support/spec_helper.rb index 6cd287bc092..6c6796564f3 100644 --- a/images/spec/support/spec_helper.rb +++ b/images/spec/support/spec_helper.rb @@ -6,7 +6,17 @@ def has_image?(src) end add_selector(:linkhref) do - xpath {|href| ".//a[@href='#{href}']"} + xpath { |href| ".//a[@href='#{href}']" } + end +end + +def init_index_view(view, &block) + include_context( "#{view} view") do + before do + ensure_on(initial_path) + initialize_context + end + yield end end From 9b1bf45c3090c3369b77c9d354b37a33fef9e639 Mon Sep 17 00:00:00 2001 From: Anita Date: Mon, 10 Feb 2025 12:21:33 +0800 Subject: [PATCH 27/74] Changes to refinery/pages --- .../helpers/refinery/admin/pages_helper.rb | 42 +++++++++---------- .../views/refinery/admin/pages/_page.html.erb | 41 ++++++------------ pages/config/locales/en.yml | 10 +++-- .../refinery/pages/admin/pages_helper_spec.rb | 18 ++------ .../spec/system/refinery/admin/pages_spec.rb | 4 -- 5 files changed, 43 insertions(+), 72 deletions(-) diff --git a/pages/app/helpers/refinery/admin/pages_helper.rb b/pages/app/helpers/refinery/admin/pages_helper.rb index 267d49cd0b1..ae4a4a206c2 100644 --- a/pages/app/helpers/refinery/admin/pages_helper.rb +++ b/pages/app/helpers/refinery/admin/pages_helper.rb @@ -32,27 +32,6 @@ def default_layout_template(current_page) "application" end - def page_icon(number_of_children) - # 1. has_children 'folder|folderopen', 'toggle' - # 2. no_children 'page' - - # .toggle scss handles adding icons to pages with children - classes = [] - case number_of_children - when 0 - classes.push icon_class('page') - title = ::I18n.t('edit', scope: 'refinery.admin.pages') - else - classes.push 'toggle' - classes.push 'expanded' if Refinery::Pages.auto_expand_admin_tree - title = ::I18n.t('expand_collapse', scope: 'refinery.admin.pages') - end - - tag.span(class: classes.join(' '), title: title) - end - - def icon_class(icon) = "#{icon}_icon" - # In the admin area we use a slightly different title # to inform the which pages are draft or hidden pages def page_meta_information(page) @@ -73,7 +52,26 @@ def page_meta_information(page) ::I18n.t('draft', :scope => 'refinery.admin.pages.page') end if page.draft? - meta_information + tag.span meta_information, class: :meta + end + + def page_icon(number_of_children) + # 1. has_children 'folder|folderopen', 'toggle' + # 2. no_children 'page' + + # .toggle scss handles adding icons to pages with children + classes = [] + case + when number_of_children.zero? + classes.push icon_class('page') + title = ::I18n.t('edit', scope: 'refinery.admin.pages') + else + expanded_class = Refinery::Pages.auto_expand_admin_tree ? 'expanded' : '' + classes.push ['toggle', expanded_class] + title = ::I18n.t('expand_collapse', scope: 'refinery.admin.pages') + end + + tag.span(class: classes.join(' '), title: title) end end end diff --git a/pages/app/views/refinery/admin/pages/_page.html.erb b/pages/app/views/refinery/admin/pages/_page.html.erb index a95b2dc4441..3ef019ddd03 100644 --- a/pages/app/views/refinery/admin/pages/_page.html.erb +++ b/pages/app/views/refinery/admin/pages/_page.html.erb @@ -1,46 +1,29 @@ <% # setup params for various action links - add_url = refinery.new_admin_page_path(parent_id: page.id) - edit_url = refinery.admin_edit_page_path(page.nested_url) - translated_locales = locales_with_titles(page, 'title', include_current: true) - - delete_url = refinery.admin_delete_page_path(page.nested_url) - delete_options = { - class: "cancel confirm-delete", - data: { confirm: t('message', scope: 'refinery.admin.delete', title: translated_field(page, :title)) } - } - dom_id = dom_id(page) - i18n_scope = 'refinery.admin.pages' - record_classes = [:record, :page].join(' ') + translated_locales = locales_with_titles(page, 'title') %> -

      • +
      • ' id='<%= dom_id(page) %>'>
        <%= page_icon(page.children.size) %> - <%= link_to translated_field(page, :title), edit_url, class: [:title, :edit], title: t('edit', scope: i18n_scope) %> - - <%= page_meta_information page %> - + <%= link_to translated_field(page, :title), edit_url, class: [:title, :edit], title: t('edit') %> + <%= page_meta_information(page) %> <% translated_locales.each do |locale| %> - - <% hint = ::I18n.with_locale(locale) do - I18n.t('.edit_title_locale', - title: translated_field(page, :title), - language: locale_language(locale), - scope: ".refinery.admin.locale_picker") - end - %> - <%= edit_in_locale locale, url: edit_url, title: hint %> + <%= edit_in_locale locale, url: edit_url, title: translated_field(page, :title) %> <% end %> <%= action_icon(:preview, page.url, t('.view_live_html')) %> - <%= action_icon(:add, add_url, t('new', scope: i18n_scope)) %> - <%= action_icon(:edit, edit_url, t('edit', scope: i18n_scope)) %> - <%= action_icon(:delete, delete_url, t('delete', scope: i18n_scope), delete_options) if page.deletable? %> + <%= action_icon(:add, refinery.new_admin_page_path(parent_id: page.id), t('.new')) %> + <%= action_icon(:edit, edit_url, t('.edit')) %> + <%= action_icon(:delete, refinery.admin_delete_page_path(page.nested_url), + t('.delete'), + class: "cancel confirm-delete", + data: { confirm: t('message', scope: 'refinery.admin.delete', title: translated_field(page, :title)) } + ) if page.deletable? %>
        diff --git a/pages/config/locales/en.yml b/pages/config/locales/en.yml index 709be9a48fa..a61bee750c8 100644 --- a/pages/config/locales/en.yml +++ b/pages/config/locales/en.yml @@ -32,15 +32,19 @@ en: tab_name: Your file link_to_this_resource: Link to this file pages: - delete: Remove this page forever edit: Edit this page - new: Add a new child page - expand_collapse: Expand or collapse sub pages page: draft: draft hidden: hidden redirected: Redirected skip_to_first_child: Skip to first child + delete: Remove this page forever + edit: Edit this page + edit_locale: Edit this page in %{language} + edit_title: Edit "%{title}" + edit_title_locale: Edit "%{title}" in %{language} + new: Add a new child page + expand_collapse: Expand or collapse sub pages view_live_html: View this page live
        (opens in a new window) form: preview: Preview diff --git a/pages/spec/helpers/refinery/pages/admin/pages_helper_spec.rb b/pages/spec/helpers/refinery/pages/admin/pages_helper_spec.rb index 5cb97c52835..46186152275 100644 --- a/pages/spec/helpers/refinery/pages/admin/pages_helper_spec.rb +++ b/pages/spec/helpers/refinery/pages/admin/pages_helper_spec.rb @@ -57,9 +57,7 @@ module Admin it "adds 'hidden' label" do page.show_in_menu = false - expect(helper.page_meta_information(page)).to eq( - %Q{#{::I18n.t('refinery.admin.pages.page.hidden')}} - ) + expect(helper.page_meta_information(page)).to have_text ::I18n.t('refinery.admin.pages.page.hidden') end end @@ -67,29 +65,21 @@ module Admin it "adds 'skip to first child' label" do page.skip_to_first_child = true - expect(helper.page_meta_information(page)).to eq( - %Q{#{::I18n.t('refinery.admin.pages.page.skip_to_first_child')}} - ) + expect(helper.page_meta_information(page)).to have_text ::I18n.t('refinery.admin.pages.page.skip_to_first_child') end end context "when link_url is present" do it "adds 'redirected' label" do page.link_url = '/redirect' - - expect(helper.page_meta_information(page)).to eq( - %Q{#{::I18n.t('refinery.admin.pages.page.redirected')}} - ) + expect(helper.page_meta_information(page)).to have_text ::I18n.t('refinery.admin.pages.page.redirected') end end context "when draft is true" do it "adds 'draft' label" do page.draft = true - - expect(helper.page_meta_information(page)).to eq( - %Q{#{::I18n.t('refinery.admin.pages.page.draft')}} - ) + expect(helper.page_meta_information(page)).to have_text ::I18n.t('refinery.admin.pages.page.draft') end end end diff --git a/pages/spec/system/refinery/admin/pages_spec.rb b/pages/spec/system/refinery/admin/pages_spec.rb index f3b67c25e2f..eae4751072c 100644 --- a/pages/spec/system/refinery/admin/pages_spec.rb +++ b/pages/spec/system/refinery/admin/pages_spec.rb @@ -154,12 +154,8 @@ module Admin describe " new / create " do it " Creates a page ", js: true do - Rails.logger.debug " #{Time.current} about to visit #{refinery.admin_pages_path}" visit refinery.admin_pages_path - Rails.logger.debug "#{Time.current} at #{refinery.admin_pages_path}" - find('a', text: 'Add new page').click - fill_in "Title", with: "My first page" expect { click_button "Save" }.to change(Refinery::Page, :count).from(0).to(1) From 4b068d4e1fa4929be1c2e1536988e871b21ec596 Mon Sep 17 00:00:00 2001 From: Anita Date: Tue, 11 Feb 2025 11:39:43 +0800 Subject: [PATCH 28/74] refactor(refinery-core): new helper 'locales_with_translated_field' add new helper 'locales_with_translated_field' in TranslationHelper to replace repeated code block in index templates. add test for helper call from index pages to identify locales with existing translations --- .../helpers/refinery/translation_helper.rb | 7 +++ .../refinery/translation_helper_spec.rb | 49 ++++++++++++++++--- .../refinery/admin/image_presenter.rb | 2 +- .../shared_examples/image_translator.rb | 4 +- .../views/refinery/admin/pages/_page.html.erb | 2 +- .../admin/resources/_resource.html.erb | 26 ++-------- spec/spec_helper.rb | 1 - 7 files changed, 57 insertions(+), 34 deletions(-) diff --git a/core/app/helpers/refinery/translation_helper.rb b/core/app/helpers/refinery/translation_helper.rb index 8c98f9386a9..26b17465ee5 100644 --- a/core/app/helpers/refinery/translation_helper.rb +++ b/core/app/helpers/refinery/translation_helper.rb @@ -25,5 +25,12 @@ def current_locale?(locale) = locale.to_sym == Refinery::I18n.current_locale def current_locale = Refinery::I18n.current_locale.to_s def current_language = Refinery::I18n.locales[Refinery::I18n.current_locale] def locale_language(locale) = Refinery::I18n.locales[locale.to_sym] + + private + + def sorted_locales(locales, frontend_locales) + locales.map(&:to_sym).sort_by { |locale| frontend_locales.index(locale) } + end + end end diff --git a/core/spec/helpers/refinery/translation_helper_spec.rb b/core/spec/helpers/refinery/translation_helper_spec.rb index 5339d9be458..12a6cd85779 100644 --- a/core/spec/helpers/refinery/translation_helper_spec.rb +++ b/core/spec/helpers/refinery/translation_helper_spec.rb @@ -1,7 +1,7 @@ require "spec_helper" module Refinery - describe TranslationHelper, :type => :helper do + describe TranslationHelper, type: :helper do describe "#t" do it "overrides Rails' translation method" do @@ -15,8 +15,6 @@ module Refinery before do Mobility.with_locale(:en) do - # page.title = "draft" - # page.save! page.update!({title: 'draft'}) end @@ -26,19 +24,56 @@ module Refinery end end - context "when title is present" do - it "returns it" do + context "when field for current locale is present" do + it "it's value is returned" do expect(helper.translated_field(page, :title)).to eq("draft") end end - context "when title for current locale isn't available" do - it "returns existing title from translations" do + context "when field for current locale isn't present" do + + it "returns existing field value from other translations" do Page::Translation.where(locale: :en).first.destroy expect(helper.translated_field(page, :title)).to eq("melnraksts") end end end + describe '#locales_with_translated_field' do + let(:page) { FactoryBot.build(:page) } + + before do + allow(Refinery::I18n).to receive(:frontend_locales).and_return([:en, :lv, :it]) + Mobility.with_locale(:en) do + page.update!({title: 'draft'}) + end + + Mobility.with_locale(:lv) do + page.title = "melnraksts" + page.save! + end + end + + it 'returns an array of locales which have the named field translated' do + expect(helper.locales_with_translated_field(page, :title)).to eq([:en, :lv]) + end + + context 'The field has no translations' do + before do + Refinery::I18n.frontend_locales.each do |locale| + Mobility.with_locale(locale) do + page.title = "A title" + page.menu_title = "" + page.save! + end + end + end + + it 'returns an empty array' do + expect(helper.locales_with_translated_field(page, :menu_title)).to eq([]) + end + end + end + end end diff --git a/images/app/presenters/refinery/admin/image_presenter.rb b/images/app/presenters/refinery/admin/image_presenter.rb index 0da1bf9e626..4b6feffe872 100644 --- a/images/app/presenters/refinery/admin/image_presenter.rb +++ b/images/app/presenters/refinery/admin/image_presenter.rb @@ -124,7 +124,7 @@ def image_actions end def edit_actions - locales_with_titles(image, 'image_title').reduce(ActiveSupport::SafeBuffer.new) do |buffer, locale| + locales_with_translated_field(image, :image_title).reduce(ActiveSupport::SafeBuffer.new) do |buffer, locale| buffer << edit_in_locale(locale, url: context.edit_admin_image_path(image), title: image.title) diff --git a/images/spec/support/shared_examples/image_translator.rb b/images/spec/support/shared_examples/image_translator.rb index 9b44aab103d..c0c0dad24a2 100644 --- a/images/spec/support/shared_examples/image_translator.rb +++ b/images/spec/support/shared_examples/image_translator.rb @@ -19,8 +19,8 @@ click_link "fr" end - fill_in "Title", :with => "Titre de la première image" - fill_in "Alt", :with => "Texte alternatif de la première image" + fill_in "Title", with: "Titre de la première image" + fill_in "Alt", with: "Texte alternatif de la première image" click_button "Save" diff --git a/pages/app/views/refinery/admin/pages/_page.html.erb b/pages/app/views/refinery/admin/pages/_page.html.erb index 3ef019ddd03..f565a939888 100644 --- a/pages/app/views/refinery/admin/pages/_page.html.erb +++ b/pages/app/views/refinery/admin/pages/_page.html.erb @@ -1,6 +1,6 @@ <% # setup params for various action links edit_url = refinery.admin_edit_page_path(page.nested_url) - translated_locales = locales_with_titles(page, 'title') + translated_locales = locales_with_translated_field(page, 'title') %>
      • ' id='<%= dom_id(page) %>'> diff --git a/resources/app/views/refinery/admin/resources/_resource.html.erb b/resources/app/views/refinery/admin/resources/_resource.html.erb index 86c16b44d54..d7b97f1b86e 100644 --- a/resources/app/views/refinery/admin/resources/_resource.html.erb +++ b/resources/app/views/refinery/admin/resources/_resource.html.erb @@ -5,28 +5,10 @@ %>
      • - - <%= translated_field(resource, :title) %> - - - <% if Refinery::I18n.frontend_locales.many? %> - - <% resource.translations.sort_by{ |t| Refinery::I18n.frontend_locales.index(t.locale)}.each do |translation| %> - <% if translation.resource_title.present? %> - <%= link_to refinery.edit_admin_resource_path(resource, switch_locale: translation.locale), - class: 'locale', title: translation.locale.upcase do %> - -
        - <%= locale_text_icon(translation.locale.upcase) %> -
        - <% end %> - <% end %> - <% end %> -
        - <% end %> - - - <%= resource.file_name %> - <%= number_to_human_size(resource.size) %> + + <% locales_with_translated_field(resource, :resource_title).each do |locale| %> + <%= edit_in_locale locale, url: edit_url %> + <% end %> diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3c327f77de9..2523639fd41 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -88,6 +88,5 @@ ([ENGINE_RAILS_ROOT, Rails.root.to_s].uniq | Refinery::Plugins.registered.pathnames).map{ |p| Dir[File.join(p, 'spec', 'support', '**', '*.rb').to_s] }.flatten.sort.each do |support_file| - # puts "Requiring #{support_file}" require support_file end From 8e734c5d953ccc68aae1d7f41854e63d540203b4 Mon Sep 17 00:00:00 2001 From: Anita Date: Tue, 11 Feb 2025 12:13:29 +0800 Subject: [PATCH 29/74] refactor(refinery-core): new helper 'edit_in_locales' add new helper 'edit_in_locales' to ActionHelper call from index pages to offer links to records with translations add edit_in_locales to images/pages/resources indexes --- core/app/helpers/refinery/action_helper.rb | 9 +++++++++ .../app/presenters/refinery/admin/image_presenter.rb | 7 ++----- pages/app/views/refinery/admin/pages/_page.html.erb | 7 +------ .../refinery/admin/resources/_resource.html.erb | 12 +++++------- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/core/app/helpers/refinery/action_helper.rb b/core/app/helpers/refinery/action_helper.rb index 52a6604a780..f508e2a7989 100644 --- a/core/app/helpers/refinery/action_helper.rb +++ b/core/app/helpers/refinery/action_helper.rb @@ -43,5 +43,14 @@ def edit_in_locale(locale, url:, title:, **options) locale: locale, class: :edit, **options ) end + + def edit_in_locales(edit_url, locales=[]) + return if locales.empty? + + edit_links = locales.map do |locale| + edit_in_locale(locale, url: edit_url ) + end + tag.span edit_links.compact.join(' ').html_safe, class: :locales + end end end diff --git a/images/app/presenters/refinery/admin/image_presenter.rb b/images/app/presenters/refinery/admin/image_presenter.rb index 4b6feffe872..f8bd9c389d4 100644 --- a/images/app/presenters/refinery/admin/image_presenter.rb +++ b/images/app/presenters/refinery/admin/image_presenter.rb @@ -124,11 +124,8 @@ def image_actions end def edit_actions - locales_with_translated_field(image, :image_title).reduce(ActiveSupport::SafeBuffer.new) do |buffer, locale| - buffer << edit_in_locale(locale, - url: context.edit_admin_image_path(image), - title: image.title) - end + translated_locales = locales_with_translated_field(image, :image_title) + edit_in_locales(context.edit_admin_image_path(image), translated_locales) end def preview_action(options = {}) diff --git a/pages/app/views/refinery/admin/pages/_page.html.erb b/pages/app/views/refinery/admin/pages/_page.html.erb index f565a939888..47eb95a4e34 100644 --- a/pages/app/views/refinery/admin/pages/_page.html.erb +++ b/pages/app/views/refinery/admin/pages/_page.html.erb @@ -8,12 +8,7 @@ <%= page_icon(page.children.size) %> <%= link_to translated_field(page, :title), edit_url, class: [:title, :edit], title: t('edit') %> <%= page_meta_information(page) %> - - - <% translated_locales.each do |locale| %> - <%= edit_in_locale locale, url: edit_url, title: translated_field(page, :title) %> - <% end %> - + <%= edit_in_locales(edit_url, translated_locales) %> <%= action_icon(:preview, page.url, t('.view_live_html')) %> diff --git a/resources/app/views/refinery/admin/resources/_resource.html.erb b/resources/app/views/refinery/admin/resources/_resource.html.erb index d7b97f1b86e..29f84357876 100644 --- a/resources/app/views/refinery/admin/resources/_resource.html.erb +++ b/resources/app/views/refinery/admin/resources/_resource.html.erb @@ -3,13 +3,11 @@ delete_url = refinery.admin_resource_path(resource) delete_options = {data: {confirm: t('message', scope: 'refinery.admin.delete', title: resource.title)}} %> -
      • - - - <% locales_with_translated_field(resource, :resource_title).each do |locale| %> - <%= edit_in_locale locale, url: edit_url %> - <% end %> - +
      • + <%= mime_type_icon(resource.mime_type) %> + <%= link_to translated_field(resource, :title), edit_url, class: [:title, :edit] %> + <%= resource_meta_information resource %> + <%= edit_in_locales(edit_url, translated_locales) %> <%= action_icon :download, resource.url, t('.download', size: number_to_human_size(resource.size)) %> From 0acd6e1afb46015c513f38557fbb06ce347551fa Mon Sep 17 00:00:00 2001 From: Anita Date: Tue, 11 Feb 2025 12:26:25 +0800 Subject: [PATCH 30/74] add resource_helper to resources_controller add newline at eof for resources/admin/form --- .../refinery/admin/resources_controller.rb | 7 +++--- .../helpers/refinery/admin/resource_helper.rb | 10 +++++++++ .../refinery/admin/resources/_form.html.erb | 2 +- .../admin/resources/_resource.html.erb | 4 +++- .../system/refinery/admin/resources_spec.rb | 22 +++++++++++-------- 5 files changed, 31 insertions(+), 14 deletions(-) create mode 100644 resources/app/helpers/refinery/admin/resource_helper.rb diff --git a/resources/app/controllers/refinery/admin/resources_controller.rb b/resources/app/controllers/refinery/admin/resources_controller.rb index 4c8c2a45682..b5ed7803dbe 100644 --- a/resources/app/controllers/refinery/admin/resources_controller.rb +++ b/resources/app/controllers/refinery/admin/resources_controller.rb @@ -1,6 +1,7 @@ module Refinery module Admin class ResourcesController < ::Refinery::AdminController + helper Refinery::Admin::ResourceHelper crudify :'refinery/resource', include: [:translations], @@ -62,9 +63,9 @@ def insert @resource_area_selected = from_dialog? if params[:visual_editor] - render '/refinery/admin/pages_dialogs/link_to' - else - render 'insert' + render '/refinery/admin/pages_dialogs/link_to' + else + render 'insert' end end diff --git a/resources/app/helpers/refinery/admin/resource_helper.rb b/resources/app/helpers/refinery/admin/resource_helper.rb new file mode 100644 index 00000000000..5b529a8a264 --- /dev/null +++ b/resources/app/helpers/refinery/admin/resource_helper.rb @@ -0,0 +1,10 @@ +module Refinery + module Admin + module ResourceHelper + + def resource_meta_information(resource) + tag.span number_to_human_size(resource.size), class: [:label, :meta] + end + end + end +end diff --git a/resources/app/views/refinery/admin/resources/_form.html.erb b/resources/app/views/refinery/admin/resources/_form.html.erb index 806d6347278..397fd3abbc3 100644 --- a/resources/app/views/refinery/admin/resources/_form.html.erb +++ b/resources/app/views/refinery/admin/resources/_form.html.erb @@ -57,4 +57,4 @@ link_dialog.init(); }); -<% end if from_dialog? %> \ No newline at end of file +<% end if from_dialog? %> diff --git a/resources/app/views/refinery/admin/resources/_resource.html.erb b/resources/app/views/refinery/admin/resources/_resource.html.erb index 29f84357876..208e1424637 100644 --- a/resources/app/views/refinery/admin/resources/_resource.html.erb +++ b/resources/app/views/refinery/admin/resources/_resource.html.erb @@ -1,7 +1,9 @@ <% + # setup params for various action links edit_url = refinery.edit_admin_resource_path(resource) delete_url = refinery.admin_resource_path(resource) - delete_options = {data: {confirm: t('message', scope: 'refinery.admin.delete', title: resource.title)}} + delete_options = {data: {confirm: t('message', scope: 'refinery.admin.delete', title: resource.resource_title)}} + translated_locales = locales_with_translated_field(resource, 'resource_title') %>
      • <%= mime_type_icon(resource.mime_type) %> diff --git a/resources/spec/system/refinery/admin/resources_spec.rb b/resources/spec/system/refinery/admin/resources_spec.rb index 709f19efdf6..6491b583b0b 100644 --- a/resources/spec/system/refinery/admin/resources_spec.rb +++ b/resources/spec/system/refinery/admin/resources_spec.rb @@ -24,8 +24,10 @@ module Admin def uploading_a_file visit refinery.admin_resources_path find('a', text: 'Upload new file').click - attach_file 'resource_file', file_path - click_button ::I18n.t('save', scope: 'refinery.admin.form_actions') + page.within_frame('dialog_iframe') do + attach_file 'resource_file', file_path + click_button ::I18n.t('save', scope: 'refinery.admin.form_actions') + end end context 'when the file mime_type is acceptable' do @@ -41,7 +43,9 @@ def uploading_a_file it 'the file is rejected', js: true do expect { uploading_a_file }.to_not change(Refinery::Resource, :count) - expect(page).to have_content(::I18n.t('incorrect_format', scope: 'activerecord.errors.models.refinery/resource')) + page.within_frame('dialog_iframe') do + expect(page).to have_content(::I18n.t('incorrect_format', scope: 'activerecord.errors.models.refinery/resource')) + end end end @@ -55,12 +59,12 @@ def uploading_a_file allow(Refinery::I18n).to receive(:current_locale).and_return(:en) end - it 'is shown' do + it 'is shown in English' do visit refinery.admin_resources_path click_link 'Upload new file' - within('#file') do - expect(page).to have_selector('a[tooltip="The maximum file size is 1.2 KB."]') + within '#file' do + expect(page).to have_text "The maximum file size is 1.2 KB." end end end @@ -70,12 +74,12 @@ def uploading_a_file allow(Refinery::I18n).to receive(:current_locale).and_return(:da) end - it 'is shown' do + it 'is shown in Danish' do visit refinery.admin_resources_path click_link 'Tilføj en ny fil' within '#file' do - expect(page).to have_selector('a[tooltip="Filen må maksimalt fylde 1,2 KB."]') + expect(page).to have_text "Filen må maksimalt fylde 1,2 kB." end end end @@ -115,7 +119,7 @@ def uploading_a_file click_link 'Edit this file' within '#switch_locale_picker' do - click_link 'FR' + click_link 'fr' end fill_in 'Title', with: 'Premier fichier' From c11dcf3d9e05c3600d47a42c5f0b6810406d9f8e Mon Sep 17 00:00:00 2001 From: Anita Date: Tue, 11 Feb 2025 12:43:37 +0800 Subject: [PATCH 31/74] Revert "Change naming of 'grid_view' and 'list_view' to 'grid_format' and 'grid_view'. (view, format and layout are already special words, and I may yet change format to something useful, but not duplicating a term used in another context)" This reverts commit 6eafef5f --- images/app/models/refinery/image.rb | 2 +- images/app/viewobjects/image_object.rb | 56 +++++++++++++++++++++ images/lib/refinery/images/configuration.rb | 12 ++--- 3 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 images/app/viewobjects/image_object.rb diff --git a/images/app/models/refinery/image.rb b/images/app/models/refinery/image.rb index a46a439b29b..551b4095333 100644 --- a/images/app/models/refinery/image.rb +++ b/images/app/models/refinery/image.rb @@ -46,7 +46,7 @@ def per_page(dialog = false, has_size_options = false) # Get a thumbnail job object given a geometry and whether to strip image profiles and comments. def thumbnail(options = {}) - options = { geometry: nil, strip: false, **options} + options = { geometry: nil, strip: false }.merge(options) geometry = convert_to_geometry(options[:geometry]) thumbnail = image thumbnail = thumbnail.thumb(geometry) if geometry diff --git a/images/app/viewobjects/image_object.rb b/images/app/viewobjects/image_object.rb new file mode 100644 index 00000000000..fa5cdebe7e9 --- /dev/null +++ b/images/app/viewobjects/image_object.rb @@ -0,0 +1,56 @@ +class ImageObject + include Refinery::TranslationHelper + include Refinery::ImageHelper + + attr_reader :image, :context, :i18n_scope + attr_writer :title, :alt, :filename, :translations, :edit_attributes, :delete_attributes, :preview_attributes + delegate_missing_to :image + + Action = Struct.new('Action', :href, :title, :text, :data, :options, keyword_init: true) + + def initialize(image, context, scope=nil) + @image = image + @context = context + @i18n_scope = scope || 'refinery.admin.images' + end + + def title + translated_field(image, :title) + end + + def alt + translated_field(image, :alt) + end + + def img_element + image_fu image, '149x149#c', title: ::I18n.t('edit_title', scope: i18n_scope, title: image.image_title || image.image_alt) + end + + def filename + image.image_name + end + def locales_with_titles + image.translations + .reject { |i| i.image_title.blank? } + .map(&:locale).sort_by { |t| Refinery::I18n.frontend_locales.index(t.to_sym) } + end + + def edit_action(options = {}) + Action.new(href: context.edit_admin_image_path(image, switch_locale: options.delete(:switch_locale)), + title: I18n.t('.edit', scope: i18n_scope), + text: I18n.t('.edit_title', title: image.title, scope: i18n_scope), + options: options) + end + + def preview_action(options = {}) + Action.new(href: image.url, title: I18n.t('.view_live_html', scope: i18n_scope), options: options) + end + + def delete_action(options = {}) + Action.new(href: context.admin_image_path(image), + title: I18n.t('.delete', scope: i18n_scope), + data:{ confirm: I18n.t('message', scope: 'refinery.admin.delete', title: image.title) }, + options: { class: [:delete, 'confirm-delete'], **options }) + end + +end diff --git a/images/lib/refinery/images/configuration.rb b/images/lib/refinery/images/configuration.rb index 961f8440210..e9ea01750c3 100644 --- a/images/lib/refinery/images/configuration.rb +++ b/images/lib/refinery/images/configuration.rb @@ -6,7 +6,7 @@ module Images config_accessor :max_image_size, :pages_per_dialog, :pages_per_admin_index, :pages_per_dialog_that_have_size_options, :user_image_sizes, :user_image_ratios, - :index_views, :preferred_index_view, :admin_image_sizes, + :image_views, :preferred_image_view, :admin_image_sizes, :allowed_mime_types, :allowed_mime_types_msg self.max_image_size = 5_242_880 @@ -24,17 +24,13 @@ module Images '1:1': 1 } - # note: these dimensions were previously hard coded. self.admin_image_sizes = { - form: '106x106>', - grid: '135x135#c', - thumbnail: '149x149#c#', - crop: '255x255#c' + grid: '149x149#c' } self.allowed_mime_types = %w[image/jpeg image/png image/gif image/tiff] self.allowed_mime_types_msg = self.allowed_mime_types.to_sentence(last_word_connector: ' or ') - self.index_views = [:grid, :list] - self.preferred_index_view = :grid + self.image_views = [:grid, :list] + self.preferred_image_view = :grid # Images should always use these changes to the dragonfly defaults self.dragonfly_name = :refinery_images From cc59b67cb146761eda0cd67eff519013d48ab68d Mon Sep 17 00:00:00 2001 From: Anita Date: Wed, 12 Feb 2025 09:31:43 +0800 Subject: [PATCH 32/74] update jquery-ui-rails to version '~7.0.0' --- core/refinerycms-core.gemspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/refinerycms-core.gemspec b/core/refinerycms-core.gemspec index 738bb76dc7c..eae45993546 100644 --- a/core/refinerycms-core.gemspec +++ b/core/refinerycms-core.gemspec @@ -3,7 +3,7 @@ require File.expand_path('../core/lib/refinery/version', __dir__) version = Refinery::Version.to_s -rails_version = ['>= 6.0.0', '< 7'] +rails_version = ['>= 6.0.0', '< 8'] Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY @@ -28,7 +28,7 @@ Gem::Specification.new do |s| s.add_dependency 'decorators', '~> 2.0', '>= 2.0.0' s.add_dependency 'font-awesome-sass', '>= 4.3.0', '< 5.0' s.add_dependency 'jquery-rails', '~> 4.3', '>= 4.3.1' - s.add_dependency 'jquery-ui-rails', '~> 6.0', '>= 6.0.0' + s.add_dependency 'jquery-ui-rails', '~> 7.0.0' s.add_dependency 'railties', rails_version s.add_dependency 'refinerycms-i18n', ['~> 5.0', '>= 5.0.1'] s.add_dependency 'sass-rails', '>= 4.0', '< 7' From df965f63b07eeefd07978fb849635e0973d46bf9 Mon Sep 17 00:00:00 2001 From: Anita Date: Wed, 12 Feb 2025 09:33:18 +0800 Subject: [PATCH 33/74] update Rails to ~>7.1 --- Gemfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 164020956a2..9452afa5f54 100644 --- a/Gemfile +++ b/Gemfile @@ -1,9 +1,9 @@ source 'https://rubygems.org' -ruby "3.1.2" +ruby "3.2.4" gemspec -gem 'rails', "~>6.1" +gem 'rails', "~>7.1" gem 'net-smtp', require: false gem 'net-imap', require: false gem 'net-pop', require: false @@ -17,10 +17,10 @@ path "./" do gem "refinerycms-resources" end -gem 'refinerycms-i18n', git: 'https://github.com/refinery/refinerycms-i18n', branch: 'ruby3' +gem 'refinerycms-i18n', git: 'https://github.com/anitagraham/refinerycms-i18n', branch: 'ruby3' # Add support for refinerycms-acts-as-indexed -gem 'refinerycms-acts-as-indexed', ['~> 4.0', '>= 4.0.0'], git: 'https://github.com/refinery/refinerycms-acts-as-indexed', branch: 'master' +gem 'refinerycms-acts-as-indexed', '~> 4.0', '>= 4.0.0', git: 'https://github.com/refinery/refinerycms-acts-as-indexed', branch: 'master' # Add the default visual editor, for now. gem 'refinerycms-wymeditor', ['~> 3.0', '>= 3.0.0'] From 56b763503964116936d362189bf45c0a92d4e743 Mon Sep 17 00:00:00 2001 From: Anita Date: Wed, 12 Feb 2025 09:39:33 +0800 Subject: [PATCH 34/74] update dependencies in refinerycms-testing --- testing/refinerycms-testing.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/refinerycms-testing.gemspec b/testing/refinerycms-testing.gemspec index 853c74b181a..1b1cec7af9f 100644 --- a/testing/refinerycms-testing.gemspec +++ b/testing/refinerycms-testing.gemspec @@ -23,7 +23,7 @@ Gem::Specification.new do |s| s.add_dependency 'rails-controller-testing', '>= 0.1.1' s.add_dependency 'refinerycms-core', version s.add_dependency 'rspec-rails', '~> 6.0.0.rc1' - s.add_dependency 'webdrivers', '~> 5.0' + s.add_dependency 'selenium-webdriver', '>= 2.46.0' s.required_ruby_version = Refinery::Version.required_ruby_version From e09e3ee4764fd7dc7921870bfcfd4bc0a378a39d Mon Sep 17 00:00:00 2001 From: Anita Date: Tue, 18 Feb 2025 12:25:38 +0800 Subject: [PATCH 35/74] stylesheets: refactor locale icons/buttons define colours for locale icon backgrounds and text use id attribute of link to determine id define mixins for locale elements remove browser-specific selectors from the rounded-corner mixins --- .../refinery/components/_icons.scss | 19 +-- .../stylesheets/refinery/global/_colours.scss | 8 +- .../stylesheets/refinery/mixins/_locales.scss | 22 +++ .../stylesheets/refinery/mixins/_rounded.scss | 32 ++--- .../assets/stylesheets/refinery/refinery.scss | 2 +- .../refinery/sections/_layout.scss | 132 ++---------------- 6 files changed, 58 insertions(+), 157 deletions(-) create mode 100644 core/app/assets/stylesheets/refinery/mixins/_locales.scss diff --git a/core/app/assets/stylesheets/refinery/components/_icons.scss b/core/app/assets/stylesheets/refinery/components/_icons.scss index bbb3d698c45..24a0c1efed4 100644 --- a/core/app/assets/stylesheets/refinery/components/_icons.scss +++ b/core/app/assets/stylesheets/refinery/components/_icons.scss @@ -79,28 +79,19 @@ .unhidden_icon {display: inline-block} - .locale_icon { position: relative; - display: flex; - flex-direction: row; - gap: 0.5rem; - align-items: center; - font-size: 1rem; - // icon_base occupies the ::before position - @include icon_base($icon_locale_colour, $size: 1.5rem); + // icon_base (the icon) occupies the ::before position + @include icon_base($locale_icon_colour, $size: 1.5rem); &::after { - line-height: 1em; position: absolute; - top: 25%; - left: 10%; + left: 0.3rem; font-size: 0.8rem; - color: #444; + color: $locale_text_colour; font-weight: bold; - content: attr(locale); + content: attr(id); text-transform: uppercase; - } } diff --git a/core/app/assets/stylesheets/refinery/global/_colours.scss b/core/app/assets/stylesheets/refinery/global/_colours.scss index fb7523f3b52..650a8489e32 100644 --- a/core/app/assets/stylesheets/refinery/global/_colours.scss +++ b/core/app/assets/stylesheets/refinery/global/_colours.scss @@ -7,13 +7,19 @@ $icon_locale_colour: #b7e3fc; // pale blue $icon_page_colour: #86cffa; // $icon_warning_colour: #FF6600; // orange -$info_icon_blue: #316CC8; +$info_icon_blue: #022864; $icon_preview_colour: $icon_default_colour; $icon_edit_colour: $icon_default_colour; $icon_add_colour: $icon_default_colour; +$locale_icon_colour: $icon_locale_colour; // pale blue +$locale_text_colour: #444444AA; +$locale_selected_background: #65c3f7; +$locale_hover_background: #cae7fb; $current_active_locale: #22A7F2; +$locale_active_background: #22A7F2; + $action_background_colour: lighten($icon_locale_colour,10%); $action_border_colour: #b7e3fc; diff --git a/core/app/assets/stylesheets/refinery/mixins/_locales.scss b/core/app/assets/stylesheets/refinery/mixins/_locales.scss new file mode 100644 index 00000000000..ac392c25f74 --- /dev/null +++ b/core/app/assets/stylesheets/refinery/mixins/_locales.scss @@ -0,0 +1,22 @@ +@mixin locale_button { + padding: 7px 3px 3px 3px; + background-color: #cdcdcd; + border-bottom: 0; + display: flex; + flex-direction: row; + gap: 0.25rem; + align-items: center; + font-size: 1rem; + &.selected { background-color: $locale_selected_background; } + &:active { background-color: $locale_active_background;} +} + +@mixin locale_group { + display: inline-flex; + flex-direction: row; + justify-content: flex-start; + gap: 0.5rem; + margin: 0; + padding: 0; + @content; +} diff --git a/core/app/assets/stylesheets/refinery/mixins/_rounded.scss b/core/app/assets/stylesheets/refinery/mixins/_rounded.scss index caa4f44cd8c..b6c4dbe0575 100644 --- a/core/app/assets/stylesheets/refinery/mixins/_rounded.scss +++ b/core/app/assets/stylesheets/refinery/mixins/_rounded.scss @@ -1,33 +1,19 @@ @mixin rounded($radius) { border-radius: $radius; - -moz-border-radius: $radius; - -webkit-border-radius: $radius; } @mixin top-rounded($radius) { - border-radius-top: $radius; - -moz-border-radius-topleft: $radius; - -moz-border-radius-topright: $radius; - -webkit-border-top-left-radius: $radius; - -webkit-border-top-right-radius: $radius; + border-top-left-radius: $radius; + border-top-right-radius: $radius; } @mixin right-rounded($radius) { - border-radius-right: $radius; - -moz-border-radius-topright: $radius; - -moz-border-radius-bottomright: $radius; - -webkit-border-top-right-radius: $radius; - -webkit-border-bottom-right-radius: $radius; + border-top-right-radius: $radius; + border-bottom-right-radius: $radius; } @mixin left-rounded($radius) { - border-radius-left: $radius; - -moz-border-radius-topleft: $radius; - -moz-border-radius-bottomleft: $radius; - -webkit-border-top-left-radius: $radius; - -webkit-border-bottom-left-radius: $radius; + border-top-left-radius: $radius; + border-bottom-left-radius: $radius; } @mixin bottom-rounded($radius) { - border-radius-bottom: $radius; - -moz-border-radius-bottomleft: $radius; - -moz-border-radius-bottomright: $radius; - -webkit-border-bottom-left-radius: $radius; - -webkit-border-bottom-right-radius: $radius; -} \ No newline at end of file + border-bottom-left-radius: $radius; + border-bottom-right-radius: $radius; +} diff --git a/core/app/assets/stylesheets/refinery/refinery.scss b/core/app/assets/stylesheets/refinery/refinery.scss index 4653091df83..3bc81910f39 100644 --- a/core/app/assets/stylesheets/refinery/refinery.scss +++ b/core/app/assets/stylesheets/refinery/refinery.scss @@ -9,6 +9,7 @@ @import 'mixins/icon'; @import 'mixins/rounded'; @import 'mixins/labels'; +@import 'mixins/locales'; @import 'mixins/images'; @import 'plugins/ui'; @@ -22,5 +23,4 @@ @import 'sections/layout'; - @import 'site_bar'; diff --git a/core/app/assets/stylesheets/refinery/sections/_layout.scss b/core/app/assets/stylesheets/refinery/sections/_layout.scss index cbede97421c..4a283761946 100644 --- a/core/app/assets/stylesheets/refinery/sections/_layout.scss +++ b/core/app/assets/stylesheets/refinery/sections/_layout.scss @@ -1631,134 +1631,30 @@ input.button.close_dialog:active, a.button.close_dialog:active, #content a.butto margin-right: 20px; } -/* Locale picker */ -#switch_locale_picker { - margin: 0; - padding: 0; - - li { - float: left; - padding: 0; - margin: 0; - list-style: none; - - a { - background-color: #cdcdcd; - background-position: 12px; - background-repeat: no-repeat; - border-bottom: 0; - border-right: solid 1px white; - margin-bottom: 1px; - display: block; - padding: 7px 3px 3px 3px; - line-height: 0; - - &:hover { - background-color: #cae7fb; - } - - &:active { - background-color: #22A7F2; +body.edit { + div.locales { + @include locale_group { + a { + @include locale_button; + &:after { + top: 0.5rem; + } } + &:first-child { @include left-rounded(5px); } + &:last-child { @include right-rounded(5px); } } - &:first-child a { - border-radius-left: 5px; - -moz-border-radius-topleft: 5px; - -moz-border-radius-bottomleft: 5px; - -webkit-border-top-left-radius: 5px; - -webkit-border-bottom-left-radius: 5px; - } - - &:last-child a { - border-radius-right: 5px; - -moz-border-radius-topright: 5px; - -moz-border-radius-bottomright: 5px; - -webkit-border-top-right-radius: 5px; - -webkit-border-bottom-right-radius: 5px; - } - - &.selected a { - background-color: #65c3f7; - } } } -#locale_picker { - li { - list-style: none; - margin: 0; - - a { - background-color: #cdcdcd; - background-position: 12px; - background-repeat: no-repeat; - border: 0; - border-bottom: solid 1px white; - display: block; - padding: 3px 14px 3px 9px; - position: relative; - - &:hover { - background-color: #cae7fb; - } - - &:active { - background-color: #22A7F2; - } - - span.action { - border-bottom: 1px dotted #727272; - position: absolute; - right: 9px; - top: 5px; - } - } - - &:first-child a { - border-radius-top: 5px; - -moz-border-radius-topleft: 5px; - -moz-border-radius-topright: 5px; - -webkit-border-top-left-radius: 5px; - -webkit-border-top-right-radius: 5px; - } - - &:last-child a { - border-radius-bottom: 5px; - -moz-border-radius-bottomleft: 5px; - -moz-border-radius-bottomright: 5px; - -webkit-border-bottom-left-radius: 5px; - -webkit-border-bottom-right-radius: 5px; - } - } - - #current_locale, - #other_locales { - margin: 0; - } - - #current_locale li { - a { - background-color: #65c3f7; +body.index { + span.locales { + @include locale_group { + a:after { top: 0; } } } } -.locale_marker { - font-size: 8px; - display: inline; - - .fa-stack .fa-comment {color: $icon_locale_colour; font-size: 24px;} -} - -#content .locales a { - border-bottom: none; -} - -#content #records .title .preview a.locale { - border-bottom: 0 none; -} - /* AJAX pagination */ .pagination_container { position: relative; From 6e16238dfdd41883aa62eb696f38b50749960e7c Mon Sep 17 00:00:00 2001 From: Anita Date: Tue, 18 Feb 2025 12:36:50 +0800 Subject: [PATCH 36/74] refactor locale actions share code generating a link to switch/set locale change name from 'locales_with_titles' to 'locales_with_translated_field' to better reflect what it does. add messages keys for language and locale in en.yml --- core/app/helpers/refinery/action_helper.rb | 18 ++++++++++++----- .../helpers/refinery/translation_helper.rb | 4 ++-- .../refinery/admin/_locale_picker.html.erb | 20 +++++++------------ core/config/locales/en.yml | 4 +++- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/core/app/helpers/refinery/action_helper.rb b/core/app/helpers/refinery/action_helper.rb index f508e2a7989..7530c8fa12d 100644 --- a/core/app/helpers/refinery/action_helper.rb +++ b/core/app/helpers/refinery/action_helper.rb @@ -38,17 +38,25 @@ def edit_in_current_locale(url:, title:, **options) end def edit_in_locale(locale, url:, title:, **options) - Rails.logger.debug "[Refinery] edit_in_locale(#{locale}): #{url}, title: #{title}, options: #{options}" - action_icon(:locale, "#{url}?switch_locale=#{locale}", title, - locale: locale, class: :edit, **options - ) + if options.delete(:label) + title = language(locale) + action_label(:locale, "#{url}?switch_locale=#{locale}", title, **options, + id: locale, class: :edit, **options + ) + else + title = ::I18n.t('.edit_in_language', language: locale_language(locale), scope: 'refinery.admin.locale_picker') + action_icon(:locale, "#{url}?switch_locale=#{locale}", title, + id: locale, class: :edit, **options + ) + end end + def edit_in_locales(edit_url, locales=[]) return if locales.empty? edit_links = locales.map do |locale| - edit_in_locale(locale, url: edit_url ) + edit_in_locale(locale, url: edit_url, title: t('.edit_in_locale', locale: locale) ) end tag.span edit_links.compact.join(' ').html_safe, class: :locales end diff --git a/core/app/helpers/refinery/translation_helper.rb b/core/app/helpers/refinery/translation_helper.rb index 26b17465ee5..a51391fb352 100644 --- a/core/app/helpers/refinery/translation_helper.rb +++ b/core/app/helpers/refinery/translation_helper.rb @@ -15,8 +15,8 @@ def translated_field(record, field) Refinery::TranslatedFieldPresenter.new(record).call(field) end - def locales_with_titles(record, title_field, include_current: true) - translations = record.translations.where.not(title_field, nil ) + def locales_with_translated_field(record, field_name, include_current: true) + translations = record.translations.where.not(field_name, nil ) translations = translations.where.not(locale: current_locale) unless include_current translations.map(&:locale).sort_by { |t| Refinery::I18n.frontend_locales.index(t.to_sym) } end diff --git a/core/app/views/refinery/admin/_locale_picker.html.erb b/core/app/views/refinery/admin/_locale_picker.html.erb index 9bc5d4d2360..3adb102aa86 100644 --- a/core/app/views/refinery/admin/_locale_picker.html.erb +++ b/core/app/views/refinery/admin/_locale_picker.html.erb @@ -1,18 +1,12 @@ <% if Refinery::I18n.frontend_locales.many? %> -
          - <% locales = Refinery::I18n.locales.clone %> - <% Refinery::I18n.frontend_locales.each do |locale| %> - <% locale_name = locales.delete(locale) %> - > - <%= link_to refinery.url_for(:switch_locale => locale, :parent_id => params[:parent_id]), id: locale do %> -
          - <%= locale_text_icon(locale.upcase) %> -
          - <%= locale_name %> - <% end %> - +
          + <% Refinery::I18n.frontend_locales.each do |locale, language| %> + <% classes = locale.to_s == local_assigns[:current_locale].to_s ? 'selected' : '' %> + <%= edit_in_locale(locale, + url: refinery.url_for(parent_id: params[:parent_id]), + title: language, class: classes, label: true ) %> <% end %> -
        +
      • <% end %> diff --git a/core/config/locales/en.yml b/core/config/locales/en.yml index 3ba67fa2607..ca8bea18532 100644 --- a/core/config/locales/en.yml +++ b/core/config/locales/en.yml @@ -43,7 +43,9 @@ en: change: Click here to pick an image show: Show locale_picker: - language: Language + language: Language, + edit_in_locale: Edit in %{locale} + edit_in_language: Edit in %{language} resource_picker: download_current: Download current file opens_in_new_window: Opens in a new window From e095a11ed0054ad91f39e2fdf8ded1d4baca6e7e Mon Sep 17 00:00:00 2001 From: Anita Date: Tue, 18 Feb 2025 12:49:44 +0800 Subject: [PATCH 37/74] Changes from ruby3 branch which are really Rails 7 changes --- Gemfile | 1 + .../helpers/refinery/translation_helper.rb | 1 + .../refinery/dummy/dummy_generator.rb | 37 ++++++++++++++----- .../dummy/templates/rails/application.rb.erb | 2 +- core/lib/refinery/crud.rb | 36 ++++++++++-------- .../refinery/admin/pages_controller.rb | 3 +- .../helpers/refinery/admin/pages_helper.rb | 2 +- .../views/refinery/admin/pages/_form.html.erb | 16 ++++---- .../refinery/admin/resources_controller.rb | 1 + 9 files changed, 62 insertions(+), 37 deletions(-) diff --git a/Gemfile b/Gemfile index 9452afa5f54..40c57ab82f2 100644 --- a/Gemfile +++ b/Gemfile @@ -50,6 +50,7 @@ group :development, :test do gem 'bootsnap', require: false gem 'listen', '~> 3.0' gem 'rspec-rails', '~> 6.0.0.rc1' + gem 'puma', require: false end group :test do diff --git a/core/app/helpers/refinery/translation_helper.rb b/core/app/helpers/refinery/translation_helper.rb index a51391fb352..d4767af80e4 100644 --- a/core/app/helpers/refinery/translation_helper.rb +++ b/core/app/helpers/refinery/translation_helper.rb @@ -16,6 +16,7 @@ def translated_field(record, field) end def locales_with_translated_field(record, field_name, include_current: true) + field_name = field_name.to_sym translations = record.translations.where.not(field_name, nil ) translations = translations.where.not(locale: current_locale) unless include_current translations.map(&:locale).sort_by { |t| Refinery::I18n.frontend_locales.index(t.to_sym) } diff --git a/core/lib/generators/refinery/dummy/dummy_generator.rb b/core/lib/generators/refinery/dummy/dummy_generator.rb index 80a9556f69b..676d1240054 100644 --- a/core/lib/generators/refinery/dummy/dummy_generator.rb +++ b/core/lib/generators/refinery/dummy/dummy_generator.rb @@ -13,9 +13,22 @@ def self.source_paths paths.flatten end + PASSTHROUGH_OPTIONS = [ - :skip_active_record, :skip_javascript, :skip_action_cable, :skip_action_mailer, :skip_active_storage, :database, - :javascript, :quiet, :pretend, :force, :skip + :database, + :force, + :pretend, + :quiet, + :skip, + :skip_action_cable, + :skip_action_mailbox, + :skip_action_mailer, + :skip_action_text, + :skip_active_job, + :skip_active_record, + :skip_active_storage, + :skip_hotwire, + :skip_javascript ] def generate_test_dummy @@ -24,8 +37,12 @@ def generate_test_dummy opts[:force] = true opts[:skip_bundle] = true opts[:skip_action_cable] = true + opts[:skip_action_mailbox] = true opts[:skip_action_mailer] = true + opts[:skip_action_text] = true + opts[:skip_active_job] = true opts[:skip_active_storage] = true + opts[:skip_hotwire] = true opts[:skip_javascript] = true invoke Rails::Generators::AppGenerator, [ File.expand_path(dummy_path, destination_root) ], opts @@ -34,14 +51,14 @@ def generate_test_dummy def test_dummy_config @database = options[:database] - template "rails/database.yml", "#{dummy_path}/config/database.yml", :force => true - template "rails/boot.rb.erb", "#{dummy_path}/config/boot.rb", :force => true - template "rails/application.rb.erb", "#{dummy_path}/config/application.rb", :force => true - template "rails/routes.rb", "#{dummy_path}/config/routes.rb", :force => true - template "rails/Rakefile", "#{dummy_path}/Rakefile", :force => true - template "rails/application.js", "#{dummy_path}/app/assets/javascripts/application.js", :force => true - template 'rails/blank.png', "#{dummy_path}/public/apple-touch-icon.png", :force => true - template 'rails/blank.png', "#{dummy_path}/public/apple-touch-icon-precomposed.png", :force => true + template "rails/database.yml", "#{dummy_path}/config/database.yml", force: true + template "rails/boot.rb.erb", "#{dummy_path}/config/boot.rb", force: true + template "rails/application.rb.erb", "#{dummy_path}/config/application.rb", force: true + template "rails/routes.rb", "#{dummy_path}/config/routes.rb", force: true + template "rails/Rakefile", "#{dummy_path}/Rakefile", force: true + template "rails/application.js", "#{dummy_path}/app/assets/javascripts/application.js", force: true + template 'rails/blank.png', "#{dummy_path}/public/apple-touch-icon.png", force: true + template 'rails/blank.png', "#{dummy_path}/public/apple-touch-icon-precomposed.png", force: true end def test_dummy_clean diff --git a/core/lib/generators/refinery/dummy/templates/rails/application.rb.erb b/core/lib/generators/refinery/dummy/templates/rails/application.rb.erb index d65a67e71a4..442700b0b4c 100644 --- a/core/lib/generators/refinery/dummy/templates/rails/application.rb.erb +++ b/core/lib/generators/refinery/dummy/templates/rails/application.rb.erb @@ -10,7 +10,7 @@ Bundler.require(*Rails.groups(assets: %w(development test))) module Dummy class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 6.0 + config.load_defaults 7.0 # Settings in config/environments/* take precedence over those specified here. # Application configuration can go into files in config/initializers diff --git a/core/lib/refinery/crud.rb b/core/lib/refinery/crud.rb index a94b86ecda5..5aaec29c07c 100644 --- a/core/lib/refinery/crud.rb +++ b/core/lib/refinery/crud.rb @@ -24,19 +24,21 @@ def self.default_options(model_name) plural_name = singular_name.pluralize { - :conditions => '', - :include => [], - :order => ('position ASC' if this_class.connected? && this_class.table_exists? && this_class.column_names.include?('position')), - :paging => true, - :per_page => false, - :redirect_to_url => "refinery.#{Refinery.route_for_model(class_name.constantize, :plural => true)}", - :searchable => true, - :search_conditions => '', - :sortable => true, - :title_attribute => "title", - :class_name => class_name, - :singular_name => singular_name, - :plural_name => plural_name + conditions: '', + include: [], + order: ('position ASC' if this_class.connected? && this_class.table_exists? && this_class.column_names.include?('position')), + paging: true, + per_page: false, + redirect_to_url: "refinery.#{Refinery.route_for_model(class_name.constantize, :plural => true)}", + searchable: true, + search_conditions: '', + sortable: true, + title_attribute: "title", + class_name: class_name, + singular_name: singular_name, + plural_name: plural_name, + find_actions: [:update, :destroy, :edit, :show], + exclude_from_find: [] } end @@ -52,14 +54,16 @@ def crudify(model_name, options = {}) class_name = options[:class_name] singular_name = options[:singular_name] plural_name = options[:plural_name] + find_actions = [*options[:find_actions]] + exclude_from_find = [*options[:exclude_from_find]] module_eval <<-RUBY, __FILE__, __LINE__ + 1 def self.crudify_options #{options.inspect} end - - prepend_before_action :find_#{singular_name}, - :only => [:update, :destroy, :edit, :show] + + actions = find_actions - exclude_from_find + prepend_before_action :find_#{singular_name}, only: actions prepend_before_action :merge_position_into_params!, :only => :create def new diff --git a/pages/app/controllers/refinery/admin/pages_controller.rb b/pages/app/controllers/refinery/admin/pages_controller.rb index 42a42cc20a3..02238c9fbb3 100644 --- a/pages/app/controllers/refinery/admin/pages_controller.rb +++ b/pages/app/controllers/refinery/admin/pages_controller.rb @@ -5,7 +5,8 @@ class PagesController < Refinery::AdminController crudify :'refinery/page', include: [:translations, :children], - paging: false + paging: false, + exclude_from_find: [:show] helper_method :valid_layout_templates, :valid_view_templates diff --git a/pages/app/helpers/refinery/admin/pages_helper.rb b/pages/app/helpers/refinery/admin/pages_helper.rb index ae4a4a206c2..1f903ca597a 100644 --- a/pages/app/helpers/refinery/admin/pages_helper.rb +++ b/pages/app/helpers/refinery/admin/pages_helper.rb @@ -7,7 +7,7 @@ def parent_id_nested_set_options(current_page) # page.title needs the :translations association, doing something like # nested_set_options(::Refinery::Page.includes(:translations), page) doesn't work, yet. # See https://github.com/collectiveidea/awesome_nested_set/pull/123 - ActiveRecord::Associations::Preloader.new.preload(pages, :translations) + ActiveRecord::Associations::Preloader.new(records: pages, associations: :translations) pages.map { |page| ["#{'-' * page.level} #{page.title}", page.id] } end diff --git a/pages/app/views/refinery/admin/pages/_form.html.erb b/pages/app/views/refinery/admin/pages/_form.html.erb index ca2562046c4..ffeeebed3b2 100644 --- a/pages/app/views/refinery/admin/pages/_form.html.erb +++ b/pages/app/views/refinery/admin/pages/_form.html.erb @@ -1,22 +1,22 @@ <%= form_for [refinery, :admin, @page], - :url => (refinery.admin_update_page_path(@page.nested_url) if @page.persisted?) do |f| %> + url: (refinery.admin_update_page_path(@page.nested_url) if @page.persisted?) do |f| %> - <%= render '/refinery/admin/error_messages', :object => @page, :include_object_name => true %> + <%= render '/refinery/admin/error_messages', object: @page, include_object_name: true %> - <%= render '/refinery/admin/locale_picker', :current_locale => Mobility.locale %> + <%= render '/refinery/admin/locale_picker', current_locale: Mobility.locale %>
        <%= f.label :title %> - <%= f.text_field :title, :class => "larger widest" %> + <%= f.text_field :title, class: %w(larger widest) %>
        - <%= render 'form_fields_after_title', :f => f %> + <%= render 'form_fields_after_title', f: f %>
        - <%= render 'form_page_parts', :f => f %> + <%= render 'form_page_parts', f: f %>
        - <%= render 'form_advanced_options', :f => f %> + <%= render 'form_advanced_options', f: f %> <%= render '/refinery/admin/form_actions', f: f, continue_editing: true, @@ -33,7 +33,7 @@ ), cancel_url: refinery.admin_pages_path %> - <%= render 'form_new_page_parts', :f => f if Refinery::Pages.new_page_parts %> + <%= render 'form_new_page_parts', f: f if Refinery::Pages.new_page_parts %> <% end %> <% content_for :javascripts do %> diff --git a/resources/app/controllers/refinery/admin/resources_controller.rb b/resources/app/controllers/refinery/admin/resources_controller.rb index b5ed7803dbe..1700aaf0407 100644 --- a/resources/app/controllers/refinery/admin/resources_controller.rb +++ b/resources/app/controllers/refinery/admin/resources_controller.rb @@ -5,6 +5,7 @@ class ResourcesController < ::Refinery::AdminController crudify :'refinery/resource', include: [:translations], + exclude_from_find: :show, order: "updated_at DESC", sortable: false From 4c86e606bac15b8875850145ab1893df2a3fa2b1 Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Wed, 31 Dec 2025 10:47:02 +1300 Subject: [PATCH 38/74] Remove Gemfile lines that interfere with CI --- Gemfile | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 40c57ab82f2..f03365ca314 100644 --- a/Gemfile +++ b/Gemfile @@ -1,9 +1,7 @@ source 'https://rubygems.org' -ruby "3.2.4" gemspec -gem 'rails', "~>7.1" gem 'net-smtp', require: false gem 'net-imap', require: false gem 'net-pop', require: false @@ -17,10 +15,10 @@ path "./" do gem "refinerycms-resources" end -gem 'refinerycms-i18n', git: 'https://github.com/anitagraham/refinerycms-i18n', branch: 'ruby3' +gem 'refinerycms-i18n', github: 'anitagraham/refinerycms-i18n', branch: 'ruby3' # Add support for refinerycms-acts-as-indexed -gem 'refinerycms-acts-as-indexed', '~> 4.0', '>= 4.0.0', git: 'https://github.com/refinery/refinerycms-acts-as-indexed', branch: 'master' +gem 'refinerycms-acts-as-indexed', '~> 4.0', '>= 4.0.0', github: 'refinery/refinerycms-acts-as-indexed', branch: 'master' # Add the default visual editor, for now. gem 'refinerycms-wymeditor', ['~> 3.0', '>= 3.0.0'] From 4cc9f70a49a9aab8dd46d600fd06a6919160ca8f Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Wed, 31 Dec 2025 11:20:56 +1300 Subject: [PATCH 39/74] Update Ruby versions and checkout action in workflow --- .github/workflows/main.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0ba43ef1b9c..30dbc09234e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,9 +12,8 @@ jobs: matrix: database: [ mysql, postgresql ] extension: [ core, dragonfly, images, pages, resources ] - ruby: [ 3.1] + ruby: [ 3.2, 3.3, 3.4, 4.0 ] fail-fast: false - max-parallel: 20 runs-on: ubuntu-latest env: @@ -30,7 +29,7 @@ jobs: name: ${{ matrix.extension }} ${{ matrix.ruby }} ${{ matrix.database }} steps: - run: sudo apt-get update && sudo apt-get install libsqlite3-dev -y - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: bundler-cache: true From 190895e1ec827700f937c316b7b6edcefef58920 Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Wed, 31 Dec 2025 11:24:20 +1300 Subject: [PATCH 40/74] Update factory_bot_rails dependency version --- testing/refinerycms-testing.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/refinerycms-testing.gemspec b/testing/refinerycms-testing.gemspec index 1b1cec7af9f..17892d5f949 100644 --- a/testing/refinerycms-testing.gemspec +++ b/testing/refinerycms-testing.gemspec @@ -19,7 +19,7 @@ Gem::Specification.new do |s| s.files = `git ls-files`.split("\n") s.add_dependency 'capybara', '>= 2.18' - s.add_dependency 'factory_bot_rails', '~> 4.8' + s.add_dependency 'factory_bot_rails', '~> 6.0' s.add_dependency 'rails-controller-testing', '>= 0.1.1' s.add_dependency 'refinerycms-core', version s.add_dependency 'rspec-rails', '~> 6.0.0.rc1' From c6da7d2e8547161b7b6ad81b27ba2a9ffdec276c Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Wed, 31 Dec 2025 11:28:33 +1300 Subject: [PATCH 41/74] Update rspec-rails dependency to version ~> 6.1 --- testing/refinerycms-testing.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/refinerycms-testing.gemspec b/testing/refinerycms-testing.gemspec index 17892d5f949..24ccdf94149 100644 --- a/testing/refinerycms-testing.gemspec +++ b/testing/refinerycms-testing.gemspec @@ -22,7 +22,7 @@ Gem::Specification.new do |s| s.add_dependency 'factory_bot_rails', '~> 6.0' s.add_dependency 'rails-controller-testing', '>= 0.1.1' s.add_dependency 'refinerycms-core', version - s.add_dependency 'rspec-rails', '~> 6.0.0.rc1' + s.add_dependency 'rspec-rails', '~> 6.1' s.add_dependency 'selenium-webdriver', '>= 2.46.0' s.required_ruby_version = Refinery::Version.required_ruby_version From 65940c69a200ef23e55e416032e5c647e844c4db Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Wed, 31 Dec 2025 11:30:24 +1300 Subject: [PATCH 42/74] Update rspec-rails gem version in Gemfile --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index f03365ca314..72735e43d3a 100644 --- a/Gemfile +++ b/Gemfile @@ -47,7 +47,7 @@ group :development, :test do gem 'activejob' gem 'bootsnap', require: false gem 'listen', '~> 3.0' - gem 'rspec-rails', '~> 6.0.0.rc1' + gem 'rspec-rails' gem 'puma', require: false end From 0fd73e63844c4c715bbe37a48e2e5b53a6e0ea14 Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Wed, 31 Dec 2025 11:45:10 +1300 Subject: [PATCH 43/74] Add sqlite to database matrix in CI workflow --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 30dbc09234e..504365cfcde 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,7 +10,7 @@ jobs: test: strategy: matrix: - database: [ mysql, postgresql ] + database: [ sqlite, mysql, postgresql ] extension: [ core, dragonfly, images, pages, resources ] ruby: [ 3.2, 3.3, 3.4, 4.0 ] fail-fast: false From cfa74f9c6b9c223e2ce331fc1e883b58e28a2899 Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Wed, 31 Dec 2025 11:47:39 +1300 Subject: [PATCH 44/74] allow sqlite3 --- Gemfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 72735e43d3a..0ee58addfce 100644 --- a/Gemfile +++ b/Gemfile @@ -6,7 +6,6 @@ gem 'net-smtp', require: false gem 'net-imap', require: false gem 'net-pop', require: false - path "./" do gem "refinerycms-core" gem "refinerycms-dragonfly" @@ -24,7 +23,7 @@ gem 'refinerycms-acts-as-indexed', '~> 4.0', '>= 4.0.0', github: 'refinery/refin gem 'refinerycms-wymeditor', ['~> 3.0', '>= 3.0.0'] # Database Configuration -unless ENV['CI'] +if !ENV['CI'] || ENV['DB'] == 'sqlite3' gem 'activerecord-jdbcsqlite3-adapter', '>= 1.3.0.rc1', platform: :jruby gem 'sqlite3', platform: :ruby end From c1d00196f08146c58ff1bdc10c583b60fa45bb18 Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Wed, 31 Dec 2025 11:47:58 +1300 Subject: [PATCH 45/74] Change sqlite to sqlite3 in workflow matrix --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 504365cfcde..f0c6fb3a11e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,7 +10,7 @@ jobs: test: strategy: matrix: - database: [ sqlite, mysql, postgresql ] + database: [ sqlite3, mysql, postgresql ] extension: [ core, dragonfly, images, pages, resources ] ruby: [ 3.2, 3.3, 3.4, 4.0 ] fail-fast: false From 0812146559c226b35ac8ee91abef40bee2c4c8da Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Wed, 31 Dec 2025 12:10:12 +1300 Subject: [PATCH 46/74] Add decorators gem for Zeitwerk compatibility Added a workaround for Zeitwerk loading issues by including the decorators gem from a specific GitHub branch. --- Gemfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Gemfile b/Gemfile index 0ee58addfce..bb83bf48378 100644 --- a/Gemfile +++ b/Gemfile @@ -22,6 +22,9 @@ gem 'refinerycms-acts-as-indexed', '~> 4.0', '>= 4.0.0', github: 'refinery/refin # Add the default visual editor, for now. gem 'refinerycms-wymeditor', ['~> 3.0', '>= 3.0.0'] +# Work around Zeitwerk loading issues +gem 'decorators', github: 'parndt/decorators', branch: 'zeitwerk' + # Database Configuration if !ENV['CI'] || ENV['DB'] == 'sqlite3' gem 'activerecord-jdbcsqlite3-adapter', '>= 1.3.0.rc1', platform: :jruby From 1b82ed49775ca1944f5b01aa887266debb20f3df Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Wed, 31 Dec 2025 14:09:27 +1300 Subject: [PATCH 47/74] Refactor Capybara app configuration for Rack 3+ Update Capybara configuration to handle Rack 3+ compatibility. --- spec/spec_helper.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 2523639fd41..0e73df3cc0c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -21,9 +21,9 @@ # if testing on localhost Capybara.configure do |config| - config.app = Rack::Builder.parse_file( - File.expand_path('../config.ru', __dir__) - ).first + parsed = Rack::Builder.parse_file(File.expand_path('../config.ru', __dir__)) + # Rack 3+ returns the app directly; Rack 2 returned [app, options] + config.app = parsed.is_a?(Array) ? parsed.first : parsed end if ENV['RETRY_COUNT'] From dc8b37f8935d7d8a15a167dc3fb445ca358e8697 Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Wed, 31 Dec 2025 14:32:56 +1300 Subject: [PATCH 48/74] Simplify find actions handling in crud.rb --- core/lib/refinery/crud.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/core/lib/refinery/crud.rb b/core/lib/refinery/crud.rb index 5aaec29c07c..6b01dfc8d70 100644 --- a/core/lib/refinery/crud.rb +++ b/core/lib/refinery/crud.rb @@ -54,16 +54,14 @@ def crudify(model_name, options = {}) class_name = options[:class_name] singular_name = options[:singular_name] plural_name = options[:plural_name] - find_actions = [*options[:find_actions]] - exclude_from_find = [*options[:exclude_from_find]] + actions = [*options[:find_actions]] - [*options[:exclude_from_find]] module_eval <<-RUBY, __FILE__, __LINE__ + 1 def self.crudify_options #{options.inspect} end - actions = find_actions - exclude_from_find - prepend_before_action :find_#{singular_name}, only: actions + prepend_before_action :find_#{singular_name}, only: #{actions} prepend_before_action :merge_position_into_params!, :only => :create def new From ce7f03986a01da0fbc32f332ac26b447a54d4f17 Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Wed, 31 Dec 2025 16:13:14 +1300 Subject: [PATCH 49/74] Attempt a few patches --- core/app/helpers/refinery/action_helper.rb | 2 +- core/app/helpers/refinery/translation_helper.rb | 4 ++-- core/lib/refinery/crud.rb | 2 +- core/lib/refinery/generators/generated_attribute.rb | 13 +++++++++++++ core/spec/lib/refinery/crud_spec.rb | 3 ++- core/spec/system/refinery/admin/xhr_paging_spec.rb | 6 +++--- .../controllers/refinery/admin/images_controller.rb | 3 ++- .../presenters/refinery/admin/image_presenter.rb | 1 + images/lib/refinery/images/configuration.rb | 6 +++--- .../refinery/pages/admin/preview_controller.rb | 1 + pages/app/models/refinery/page_part.rb | 9 ++++++++- .../controllers/refinery/pages_controller_spec.rb | 2 +- 12 files changed, 38 insertions(+), 14 deletions(-) diff --git a/core/app/helpers/refinery/action_helper.rb b/core/app/helpers/refinery/action_helper.rb index 7530c8fa12d..fb03eb92f47 100644 --- a/core/app/helpers/refinery/action_helper.rb +++ b/core/app/helpers/refinery/action_helper.rb @@ -39,7 +39,7 @@ def edit_in_current_locale(url:, title:, **options) def edit_in_locale(locale, url:, title:, **options) if options.delete(:label) - title = language(locale) + title = locale_language(locale) action_label(:locale, "#{url}?switch_locale=#{locale}", title, **options, id: locale, class: :edit, **options ) diff --git a/core/app/helpers/refinery/translation_helper.rb b/core/app/helpers/refinery/translation_helper.rb index d4767af80e4..959db854674 100644 --- a/core/app/helpers/refinery/translation_helper.rb +++ b/core/app/helpers/refinery/translation_helper.rb @@ -17,9 +17,9 @@ def translated_field(record, field) def locales_with_translated_field(record, field_name, include_current: true) field_name = field_name.to_sym - translations = record.translations.where.not(field_name, nil ) + translations = record.translations.where.not(field_name => [nil, ""]) translations = translations.where.not(locale: current_locale) unless include_current - translations.map(&:locale).sort_by { |t| Refinery::I18n.frontend_locales.index(t.to_sym) } + translations.map { |t| t.locale.to_sym }.sort_by { |locale| Refinery::I18n.frontend_locales.index(locale) } end def current_locale?(locale) = locale.to_sym == Refinery::I18n.current_locale diff --git a/core/lib/refinery/crud.rb b/core/lib/refinery/crud.rb index 6b01dfc8d70..38bf00a2563 100644 --- a/core/lib/refinery/crud.rb +++ b/core/lib/refinery/crud.rb @@ -60,7 +60,7 @@ def crudify(model_name, options = {}) def self.crudify_options #{options.inspect} end - + prepend_before_action :find_#{singular_name}, only: #{actions} prepend_before_action :merge_position_into_params!, :only => :create diff --git a/core/lib/refinery/generators/generated_attribute.rb b/core/lib/refinery/generators/generated_attribute.rb index 7595fb3051b..44f938b01ed 100644 --- a/core/lib/refinery/generators/generated_attribute.rb +++ b/core/lib/refinery/generators/generated_attribute.rb @@ -3,9 +3,22 @@ module Refinery module Generators class GeneratedAttribute < Rails::Generators::GeneratedAttribute + REFINERY_TYPES = %w(image resource radio select checkbox) + attr_accessor :refinery_type class << self + def parse(column_definition) + name, type, index_type = column_definition.split(":") + + # Handle Refinery's custom types before Rails validates them + if type && REFINERY_TYPES.include?(type) + new(name, type.to_sym, index_type) + else + super + end + end + def reference?(type) [:references, :belongs_to, :image, :resource].include? type end diff --git a/core/spec/lib/refinery/crud_spec.rb b/core/spec/lib/refinery/crud_spec.rb index 8b7df004008..a6503e3af94 100644 --- a/core/spec/lib/refinery/crud_spec.rb +++ b/core/spec/lib/refinery/crud_spec.rb @@ -15,7 +15,8 @@ class CrudDummy < ActiveRecord::Base end class CrudDummyController < ::ApplicationController - crudify :'refinery/crud_dummy' + skip_before_action :find_or_set_locale, raise: false + crudify :'refinery/crud_dummy', find_actions: [:update, :destroy, :edit] end end diff --git a/core/spec/system/refinery/admin/xhr_paging_spec.rb b/core/spec/system/refinery/admin/xhr_paging_spec.rb index 1a91f81a5ae..7ffafc7b8ab 100644 --- a/core/spec/system/refinery/admin/xhr_paging_spec.rb +++ b/core/spec/system/refinery/admin/xhr_paging_spec.rb @@ -21,8 +21,8 @@ module Refinery it 'performs ajax paging of index' do visit refinery.admin_images_path - expect(page).to have_selector('ul#image_grid li > img', count: 1) - expect(page).to have_css(%Q{img[alt="#{first_image.title}"]}) + expect(page).to have_selector('ul#image_index li.image', count: 1) + expect(page).to have_css(%Q{img[alt="#{first_image.alt}"]}) # placeholder which would disappear in a full page refresh. page.execute_script("node = document.createElement('i');") @@ -33,7 +33,7 @@ module Refinery click_link '2' end - expect(page).to have_css(%Q{img[alt="#{last_image.title}"]}) + expect(page).to have_css(%Q{img[alt="#{last_image.alt}"]}) expect(page.evaluate_script( %{$('i#has_not_refreshed_entire_page').length} )).to eq(1) diff --git a/images/app/controllers/refinery/admin/images_controller.rb b/images/app/controllers/refinery/admin/images_controller.rb index a58cdbdaec1..c2a0f6862f4 100644 --- a/images/app/controllers/refinery/admin/images_controller.rb +++ b/images/app/controllers/refinery/admin/images_controller.rb @@ -7,7 +7,8 @@ class ImagesController < ::Refinery::AdminController include: [:translations, :crops], order: "updated_at DESC", sortable: false, - conditions: 'parent_id IS NULL' + conditions: 'parent_id IS NULL', + find_actions: [:update, :destroy, :edit] before_action :change_view_if_specified, :init_dialog diff --git a/images/app/presenters/refinery/admin/image_presenter.rb b/images/app/presenters/refinery/admin/image_presenter.rb index f8bd9c389d4..06856f15d2c 100644 --- a/images/app/presenters/refinery/admin/image_presenter.rb +++ b/images/app/presenters/refinery/admin/image_presenter.rb @@ -67,6 +67,7 @@ class ImagePresenter < Refinery::BasePresenter attr_reader :image, :context, :index_keys, :i18n_scope attr_writer :title, :alt, :filename, :translations, :edit_attributes, :delete_attributes, :preview_attributes delegate_missing_to :image + delegate :t, to: :context IndexEntry = Struct.new('ImageEntry', :id, :edit_link, :text_elements, :locales, :actions) diff --git a/images/lib/refinery/images/configuration.rb b/images/lib/refinery/images/configuration.rb index e9ea01750c3..43fd67ec552 100644 --- a/images/lib/refinery/images/configuration.rb +++ b/images/lib/refinery/images/configuration.rb @@ -6,7 +6,7 @@ module Images config_accessor :max_image_size, :pages_per_dialog, :pages_per_admin_index, :pages_per_dialog_that_have_size_options, :user_image_sizes, :user_image_ratios, - :image_views, :preferred_image_view, :admin_image_sizes, + :index_views, :preferred_index_view, :admin_image_sizes, :allowed_mime_types, :allowed_mime_types_msg self.max_image_size = 5_242_880 @@ -29,8 +29,8 @@ module Images } self.allowed_mime_types = %w[image/jpeg image/png image/gif image/tiff] self.allowed_mime_types_msg = self.allowed_mime_types.to_sentence(last_word_connector: ' or ') - self.image_views = [:grid, :list] - self.preferred_image_view = :grid + self.index_views = [:grid, :list] + self.preferred_index_view = :grid # Images should always use these changes to the dragonfly defaults self.dragonfly_name = :refinery_images diff --git a/pages/app/controllers/refinery/pages/admin/preview_controller.rb b/pages/app/controllers/refinery/pages/admin/preview_controller.rb index 0f768681a37..3ae6442e35f 100644 --- a/pages/app/controllers/refinery/pages/admin/preview_controller.rb +++ b/pages/app/controllers/refinery/pages/admin/preview_controller.rb @@ -9,6 +9,7 @@ class PreviewController < Refinery::PagesController include Pages::RenderOptions skip_before_action :error_404, :set_canonical + skip_after_action :store_location?, raise: false layout :layout diff --git a/pages/app/models/refinery/page_part.rb b/pages/app/models/refinery/page_part.rb index 3c1c51767fa..c52ee9fe057 100644 --- a/pages/app/models/refinery/page_part.rb +++ b/pages/app/models/refinery/page_part.rb @@ -7,11 +7,18 @@ class PagePart < Refinery::Core::BaseModel validates :title, :presence => true validates :slug, :presence => true, :uniqueness => {:scope => :refinery_page_id, :case_sensitive => true} - alias_attribute :content, :body extend Mobility translates :body + def content + body + end + + def content=(value) + self.body = value + end + def to_param "page_part_#{slug.downcase.gsub(/\W/, '_')}" end diff --git a/pages/spec/controllers/refinery/pages_controller_spec.rb b/pages/spec/controllers/refinery/pages_controller_spec.rb index 252f0782fc7..b2b5083c954 100644 --- a/pages/spec/controllers/refinery/pages_controller_spec.rb +++ b/pages/spec/controllers/refinery/pages_controller_spec.rb @@ -9,7 +9,7 @@ module Refinery end describe "#home" do - context "when page path set to default ('/') " do + context "when page path set to default ('/')" do it "renders home template" do get :home expect(response).to render_template("home") From 9ba627759104241af449a0443a7cb2e983793a5c Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Wed, 31 Dec 2025 16:20:27 +1300 Subject: [PATCH 50/74] Images needs imagemagick --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f0c6fb3a11e..2092a3fe75d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,6 +35,10 @@ jobs: bundler-cache: true ruby-version: ${{ matrix.ruby }} + - name: "Install ImageMagick for images extension" + if: ${{ matrix.extension == 'images' }} + run: sudo apt-get install imagemagick -y + - name: "Set up MySQL using VM's server" if: ${{ env.DB == 'mysql' }} run: | From 2085dd98192edd0cf7b756608b27ac18996d0410 Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Wed, 31 Dec 2025 17:20:35 +1300 Subject: [PATCH 51/74] Try some dubious fixes --- .../refinery/sections/_layout.scss | 2 +- .../refinery/admin/page_parts_controller.rb | 1 + .../helpers/refinery/admin/pages_helper.rb | 4 +- .../views/refinery/admin/pages/_form.html.erb | 4 +- .../spec/system/refinery/admin/pages_spec.rb | 47 +++++++++++++------ 5 files changed, 39 insertions(+), 19 deletions(-) diff --git a/core/app/assets/stylesheets/refinery/sections/_layout.scss b/core/app/assets/stylesheets/refinery/sections/_layout.scss index 4a283761946..819facea2c4 100644 --- a/core/app/assets/stylesheets/refinery/sections/_layout.scss +++ b/core/app/assets/stylesheets/refinery/sections/_layout.scss @@ -895,7 +895,7 @@ label.stripped { padding-top: 4px; } -#records.tree li span.item { +#records.tree li span.icon { display: block; float: left; width: 16px; diff --git a/pages/app/controllers/refinery/admin/page_parts_controller.rb b/pages/app/controllers/refinery/admin/page_parts_controller.rb index c8fbfef0914..89ddbcdfb53 100644 --- a/pages/app/controllers/refinery/admin/page_parts_controller.rb +++ b/pages/app/controllers/refinery/admin/page_parts_controller.rb @@ -1,6 +1,7 @@ module Refinery module Admin class PagePartsController < ::Refinery::AdminController + skip_after_action :store_location?, raise: false def new render :partial => '/refinery/admin/pages/page_part_field', :locals => { diff --git a/pages/app/helpers/refinery/admin/pages_helper.rb b/pages/app/helpers/refinery/admin/pages_helper.rb index 1f903ca597a..48eec95ebf8 100644 --- a/pages/app/helpers/refinery/admin/pages_helper.rb +++ b/pages/app/helpers/refinery/admin/pages_helper.rb @@ -60,14 +60,14 @@ def page_icon(number_of_children) # 2. no_children 'page' # .toggle scss handles adding icons to pages with children - classes = [] + classes = ['icon'] case when number_of_children.zero? classes.push icon_class('page') title = ::I18n.t('edit', scope: 'refinery.admin.pages') else expanded_class = Refinery::Pages.auto_expand_admin_tree ? 'expanded' : '' - classes.push ['toggle', expanded_class] + classes.push 'toggle', expanded_class title = ::I18n.t('expand_collapse', scope: 'refinery.admin.pages') end diff --git a/pages/app/views/refinery/admin/pages/_form.html.erb b/pages/app/views/refinery/admin/pages/_form.html.erb index ffeeebed3b2..83d8e5b8093 100644 --- a/pages/app/views/refinery/admin/pages/_form.html.erb +++ b/pages/app/views/refinery/admin/pages/_form.html.erb @@ -12,7 +12,7 @@ <%= render 'form_fields_after_title', f: f %> -
        +
        <%= render 'form_page_parts', f: f %>
        @@ -64,7 +64,7 @@ action: prev_url, target: prev_target }); - }, 100); + }, 500); }); }); diff --git a/pages/spec/system/refinery/admin/pages_spec.rb b/pages/spec/system/refinery/admin/pages_spec.rb index eae4751072c..49c104f6c12 100644 --- a/pages/spec/system/refinery/admin/pages_spec.rb +++ b/pages/spec/system/refinery/admin/pages_spec.rb @@ -19,7 +19,7 @@ def switch_page_form_locale(locale) end # make sure that the locale change has taken effect - expect(page).to have_selector("#switch_locale_picker li.selected a##{locale.downcase}") + expect(page).to have_selector("#switch_locale_picker a.selected##{locale.downcase}") end module Refinery @@ -93,9 +93,14 @@ module Admin let!(:location) { locations.children.create title: 'New York' } context "with auto expand option turned off" do - before do - allow(Refinery::Pages).to receive(:auto_expand_admin_tree).and_return(false) + around do |example| + old_value = Refinery::Pages.auto_expand_admin_tree + Refinery::Pages.auto_expand_admin_tree = false + example.run + Refinery::Pages.auto_expand_admin_tree = old_value + end + before do visit refinery.admin_pages_path end @@ -110,25 +115,30 @@ module Admin end it "expands children", js: true do - find("#page_#{company.id} .item .toggle").click + find("#page_#{company.id} .icon.toggle").click expect(page).to have_content(team.title) expect(page).to have_content(locations.title) end it "expands children when nested multiple levels deep", js: true do - find("#page_#{company.id} .item .toggle").click - find("#page_#{locations.id} .item .toggle").click + find("#page_#{company.id} .icon.toggle").click + find("#page_#{locations.id} .icon.toggle").click expect(page).to have_content("New York") end end context "with auto expand option turned on" do - before do - allow(Refinery::Pages).to receive(:auto_expand_admin_tree).and_return(true) + around do |example| + old_value = Refinery::Pages.auto_expand_admin_tree + Refinery::Pages.auto_expand_admin_tree = true Rails.cache.clear + example.run + Refinery::Pages.auto_expand_admin_tree = old_value + end + before do visit refinery.admin_pages_path end @@ -359,6 +369,9 @@ module Admin window.close + # Wait for setTimeout to restore form action/target after preview + sleep 0.6 + click_button "Save & continue editing" expect(page).to have_content("'Save this' was successfully updated") end @@ -459,9 +472,14 @@ module Admin end context "with translations" do - before do - allow(Refinery::I18n).to receive(:frontend_locales).and_return([:en, :ru]) + around do |example| + old_locales = Refinery::I18n.config.frontend_locales + Refinery::I18n.config.frontend_locales = [:en, :ru] + example.run + Refinery::I18n.config.frontend_locales = old_locales + end + before do # Create a home page in both locales (needed to test menus) home_page = Mobility.with_locale(:en) do Page.create title: 'Home', @@ -529,8 +547,6 @@ module Admin let(:ru_page_title) { 'Новости' } let(:ru_page_slug_encoded) { '%D0%BD%D0%BE%D0%B2%D0%BE%D1%81%D1%82%D0%B8' } let!(:news_page) do - allow(Refinery::I18n).to receive(:frontend_locales).and_return([:en, :ru]) - _page = Mobility.with_locale(:en) { Page.create title: en_page_title } @@ -698,8 +714,11 @@ module Admin end describe "new page part" do - before do - allow(Refinery::Pages).to receive(:new_page_parts).and_return(true) + around do |example| + old_value = Refinery::Pages.new_page_parts + Refinery::Pages.new_page_parts = true + example.run + Refinery::Pages.new_page_parts = old_value end it "adds new page part", js: true do From 66c625b1b19141d40105f7ef6a937a202484a589 Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Thu, 1 Jan 2026 11:07:26 +1300 Subject: [PATCH 52/74] Tidy up dubious fixes --- .../spec/system/refinery/admin/pages_spec.rb | 40 ++++++++----------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/pages/spec/system/refinery/admin/pages_spec.rb b/pages/spec/system/refinery/admin/pages_spec.rb index 49c104f6c12..b05d6c5cf7d 100644 --- a/pages/spec/system/refinery/admin/pages_spec.rb +++ b/pages/spec/system/refinery/admin/pages_spec.rb @@ -93,15 +93,13 @@ module Admin let!(:location) { locations.children.create title: 'New York' } context "with auto expand option turned off" do - around do |example| - old_value = Refinery::Pages.auto_expand_admin_tree + before do Refinery::Pages.auto_expand_admin_tree = false - example.run - Refinery::Pages.auto_expand_admin_tree = old_value + visit refinery.admin_pages_path end - before do - visit refinery.admin_pages_path + after do + Refinery::Pages.auto_expand_admin_tree = true end it "show parent page" do @@ -130,15 +128,9 @@ module Admin end context "with auto expand option turned on" do - around do |example| - old_value = Refinery::Pages.auto_expand_admin_tree + before do Refinery::Pages.auto_expand_admin_tree = true Rails.cache.clear - example.run - Refinery::Pages.auto_expand_admin_tree = old_value - end - - before do visit refinery.admin_pages_path end @@ -369,8 +361,8 @@ module Admin window.close - # Wait for setTimeout to restore form action/target after preview - sleep 0.6 + # Wait for JavaScript setTimeout to restore form action after preview + expect(page).to have_no_css("form[action*='/preview/']", wait: 1) click_button "Save & continue editing" expect(page).to have_content("'Save this' was successfully updated") @@ -472,11 +464,12 @@ module Admin end context "with translations" do - around do |example| - old_locales = Refinery::I18n.config.frontend_locales + before do Refinery::I18n.config.frontend_locales = [:en, :ru] - example.run - Refinery::I18n.config.frontend_locales = old_locales + end + + after do + Refinery::I18n.config.frontend_locales = [:en] end before do @@ -714,11 +707,12 @@ module Admin end describe "new page part" do - around do |example| - old_value = Refinery::Pages.new_page_parts + before do Refinery::Pages.new_page_parts = true - example.run - Refinery::Pages.new_page_parts = old_value + end + + after do + Refinery::Pages.new_page_parts = false end it "adds new page part", js: true do From 836e099c4d046a801ebe28e5cc3c727eacc3d8af Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Thu, 1 Jan 2026 13:43:02 +1300 Subject: [PATCH 53/74] Tidy up a bit --- Gemfile | 26 ++++---- core/app/views/refinery/_site_bar.html.erb | 13 ++-- .../generators/refinery/cms/cms_generator.rb | 15 +++-- .../refinery/admin/images_controller.rb | 2 +- .../spec/system/refinery/admin/pages_spec.rb | 2 +- .../admin/resources/_resource.html.erb | 2 +- .../system/refinery/admin/resources_spec.rb | 32 +++++++-- routes.txt | 66 ------------------- spec/spec_helper.rb | 1 - spec/support/capybara.rb | 8 +-- .../refinery/generator_spec_describer.rb | 22 ++++--- 11 files changed, 73 insertions(+), 116 deletions(-) delete mode 100644 routes.txt diff --git a/Gemfile b/Gemfile index bb83bf48378..4c058a30264 100644 --- a/Gemfile +++ b/Gemfile @@ -1,17 +1,19 @@ +# frozen_string_literal: true + source 'https://rubygems.org' gemspec -gem 'net-smtp', require: false gem 'net-imap', require: false gem 'net-pop', require: false +gem 'net-smtp', require: false -path "./" do - gem "refinerycms-core" - gem "refinerycms-dragonfly" - gem "refinerycms-images" - gem "refinerycms-pages" - gem "refinerycms-resources" +path './' do + gem 'refinerycms-core' + gem 'refinerycms-dragonfly' + gem 'refinerycms-images' + gem 'refinerycms-pages' + gem 'refinerycms-resources' end gem 'refinerycms-i18n', github: 'anitagraham/refinerycms-i18n', branch: 'ruby3' @@ -34,7 +36,7 @@ end if !ENV['CI'] || ENV['DB'] == 'mysql' group :mysql do gem 'activerecord-jdbcmysql-adapter', '>= 1.3.0.rc1', platform: :jruby - gem 'mysql2', '~> 0.4', :platform => :ruby + gem 'mysql2', '~> 0.4', platform: :ruby end end @@ -49,18 +51,16 @@ group :development, :test do gem 'activejob' gem 'bootsnap', require: false gem 'listen', '~> 3.0' - gem 'rspec-rails' gem 'puma', require: false + gem 'rspec-rails' end group :test do - gem 'refinerycms-testing', path: './testing' + gem 'coveralls', require: false gem 'generator_spec', '~> 0.9.3' gem 'launchy' - gem 'coveralls', require: false + gem 'refinerycms-testing', path: './testing' gem 'rspec-retry' - gem 'falcon' - gem 'falcon-capybara' end # Load local gems according to Refinery developer preference. diff --git a/core/app/views/refinery/_site_bar.html.erb b/core/app/views/refinery/_site_bar.html.erb index db909de8599..4335b1f939d 100644 --- a/core/app/views/refinery/_site_bar.html.erb +++ b/core/app/views/refinery/_site_bar.html.erb @@ -3,22 +3,21 @@ <% content_for :stylesheets, stylesheet_link_tag('refinery/site_bar') unless !!local_assigns[:exclude_css] %> <%= yield(:stylesheets) unless local_assigns[:head] || local_assigns[:exclude_css] %> <% end -%> -
        -
        +
        +
        <%= link_to 'https://www.refinerycms.com', :id => 'site_bar_refinery_cms_logo', :target => '_blank' do %> <%= image_tag 'refinery/refinery-cms-logo.svg', alt: 'Refinery CMS™' %> <% end %> -
        +
        <%= site_bar_switch_link -%> <%= site_bar_edit_link -%>
        -
        - - <% Rails.logger.debug Refinery::Core.site_name %> - <%#= Refinery::Core.site_name %> +
        + + <%= Refinery::Core.site_name %> <%= link_to t('.log_out', **site_bar_translate_locale_args), diff --git a/core/lib/generators/refinery/cms/cms_generator.rb b/core/lib/generators/refinery/cms/cms_generator.rb index 1b37d643066..9c110cee27a 100644 --- a/core/lib/generators/refinery/cms/cms_generator.rb +++ b/core/lib/generators/refinery/cms/cms_generator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'pathname' require 'mkmf' @@ -329,8 +331,10 @@ def start_pretending? # Only pretend to do the next actions if this is Refinery to stay DRY if destination_path == Refinery.root say_status :'-- pretending to make changes that happen in an actual installation --', nil, :yellow - original_pretend = self.options[:pretend] - self.options.merge(pretend: true, original_pretend: original_pretend) + self.old_pretend = self.options[:pretend] + new_options = self.options.dup + new_options[:pretend] = true + self.options = new_options end end @@ -338,9 +342,12 @@ def stop_pretending? # Stop pretending if destination_path == Refinery.root say_status :'-- finished pretending --', nil, :yellow - original_pretend = self.options[:original_pretend] - self.options.merge(pretend: original_pretend).remove(:original_pretend) + new_options = options.dup.merge(pretend: old_pretend) + new_options[:pretend] = old_pretend + self.options = new_options end end + + private attr_accessor :old_pretend end end diff --git a/images/app/controllers/refinery/admin/images_controller.rb b/images/app/controllers/refinery/admin/images_controller.rb index c2a0f6862f4..69990bb9a97 100644 --- a/images/app/controllers/refinery/admin/images_controller.rb +++ b/images/app/controllers/refinery/admin/images_controller.rb @@ -189,7 +189,7 @@ def image_params def permitted_image_params [ - :image, :image_size, :image_title, :image_alt + { image: [] }, :image_size, :image_title, :image_alt ] end end diff --git a/pages/spec/system/refinery/admin/pages_spec.rb b/pages/spec/system/refinery/admin/pages_spec.rb index b05d6c5cf7d..07632c2d2ec 100644 --- a/pages/spec/system/refinery/admin/pages_spec.rb +++ b/pages/spec/system/refinery/admin/pages_spec.rb @@ -147,7 +147,7 @@ module Admin end fill_in "page_title", with: 'Las Vegas' - click_button "Save " + click_button "Save" expect(page).to have_content('Las Vegas') end end diff --git a/resources/app/views/refinery/admin/resources/_resource.html.erb b/resources/app/views/refinery/admin/resources/_resource.html.erb index 208e1424637..5225e6e623c 100644 --- a/resources/app/views/refinery/admin/resources/_resource.html.erb +++ b/resources/app/views/refinery/admin/resources/_resource.html.erb @@ -2,7 +2,7 @@ # setup params for various action links edit_url = refinery.edit_admin_resource_path(resource) delete_url = refinery.admin_resource_path(resource) - delete_options = {data: {confirm: t('message', scope: 'refinery.admin.delete', title: resource.resource_title)}} + delete_options = {data: {confirm: t('message', scope: 'refinery.admin.delete', title: translated_field(resource, :title))}} translated_locales = locales_with_translated_field(resource, 'resource_title') %>
      • diff --git a/resources/spec/system/refinery/admin/resources_spec.rb b/resources/spec/system/refinery/admin/resources_spec.rb index 6491b583b0b..a476674b898 100644 --- a/resources/spec/system/refinery/admin/resources_spec.rb +++ b/resources/spec/system/refinery/admin/resources_spec.rb @@ -132,16 +132,34 @@ def uploading_a_file end context 'destroy' do - let!(:resource) { FactoryBot.create(:resource) } + context 'when resource_title is empty' do + let!(:resource) { FactoryBot.create(:resource) } - it 'removes file' do - visit refinery.admin_resources_path - expect(page).to have_selector("a[href='/refinery/resources/#{resource.id}']") + it 'shows filename in confirmation', js: true do + visit refinery.admin_resources_path + delete_link = find('a[tooltip="Remove this file forever"]') + expect(delete_link['data-confirm']).to eq("Are you sure you want to remove 'Cape Town Tide Table'?") + end + + it 'removes file' do + visit refinery.admin_resources_path + expect(page).to have_selector("a[href='/refinery/resources/#{resource.id}']") - click_link 'Remove this file forever' + click_link 'Remove this file forever' - expect(page).to have_content("'Cape Town Tide Table' was successfully removed.") - expect(Refinery::Resource.count).to eq(0) + expect(page).to have_content("'Cape Town Tide Table' was successfully removed.") + expect(Refinery::Resource.count).to eq(0) + end + end + + context 'when resource_title is set' do + let!(:resource) { FactoryBot.create(:resource, resource_title: 'My Custom Title') } + + it 'shows resource_title in confirmation', js: true do + visit refinery.admin_resources_path + delete_link = find('a[tooltip="Remove this file forever"]') + expect(delete_link['data-confirm']).to eq("Are you sure you want to remove 'My Custom Title'?") + end end end diff --git a/routes.txt b/routes.txt deleted file mode 100644 index 24f6bbcebae..00000000000 --- a/routes.txt +++ /dev/null @@ -1,66 +0,0 @@ -Usage: - rails new APP_PATH [options] - -Options: - [--skip-namespace], [--no-skip-namespace] # Skip namespace (affects only isolated engines) - [--skip-collision-check], [--no-skip-collision-check] # Skip collision check - -r, [--ruby=PATH] # Path to the Ruby binary of your choice - # Default: /Users/anita/.rbenv/versions/3.1.2/bin/ruby - -m, [--template=TEMPLATE] # Path to some application template (can be a filesystem path or URL) - -d, [--database=DATABASE] # Preconfigure for selected database (options: mysql/postgresql/sqlite3/oracle/sqlserver/jdbcmysql/jdbcsqlite3/jdbcpostgresql/jdbc) - # Default: sqlite3 - -G, [--skip-git], [--no-skip-git] # Skip .gitignore file - [--skip-keeps], [--no-skip-keeps] # Skip source control .keep files - -M, [--skip-action-mailer], [--no-skip-action-mailer] # Skip Action Mailer files - [--skip-action-mailbox], [--no-skip-action-mailbox] # Skip Action Mailbox gem - [--skip-action-text], [--no-skip-action-text] # Skip Action Text gem - -O, [--skip-active-record], [--no-skip-active-record] # Skip Active Record files - [--skip-active-job], [--no-skip-active-job] # Skip Active Job - [--skip-active-storage], [--no-skip-active-storage] # Skip Active Storage files - -C, [--skip-action-cable], [--no-skip-action-cable] # Skip Action Cable files - -A, [--skip-asset-pipeline], [--no-skip-asset-pipeline] # Indicates when to generate skip asset pipeline - -a, [--asset-pipeline=ASSET_PIPELINE] # Choose your asset pipeline [options: sprockets (default), propshaft] - # Default: sprockets - -J, [--skip-javascript], [--no-skip-javascript] # Skip JavaScript files - [--skip-hotwire], [--no-skip-hotwire] # Skip Hotwire integration - [--skip-jbuilder], [--no-skip-jbuilder] # Skip jbuilder gem - -T, [--skip-test], [--no-skip-test] # Skip test files - [--skip-system-test], [--no-skip-system-test] # Skip system test files - [--skip-bootsnap], [--no-skip-bootsnap] # Skip bootsnap gem - [--dev], [--no-dev] # Set up the application with Gemfile pointing to your Rails checkout - [--edge], [--no-edge] # Set up the application with Gemfile pointing to Rails repository - --master, [--main], [--no-main] # Set up the application with Gemfile pointing to Rails repository main branch - [--rc=RC] # Path to file containing extra configuration options for rails command - [--no-rc], [--no-no-rc] # Skip loading of extra configuration options from .railsrc file - [--api], [--no-api] # Preconfigure smaller stack for API only apps - [--minimal], [--no-minimal] # Preconfigure a minimal rails app - -j, [--javascript=JAVASCRIPT] # Choose JavaScript approach [options: importmap (default), webpack, esbuild, rollup] - # Default: importmap - -c, [--css=CSS] # Choose CSS processor [options: tailwind, bootstrap, bulma, postcss, sass... check https://github.com/rails/cssbundling-rails] - -B, [--skip-bundle], [--no-skip-bundle] # Don't run bundle install - -Runtime options: - -f, [--force] # Overwrite files that already exist - -p, [--pretend], [--no-pretend] # Run but do not make any changes - -q, [--quiet], [--no-quiet] # Suppress status output - -s, [--skip], [--no-skip] # Skip files that already exist - -Rails options: - -h, [--help], [--no-help] # Show this help message and quit - -v, [--version], [--no-version] # Show Rails version number and quit - -Description: - The 'rails new' command creates a new Rails application with a default - directory structure and configuration at the path you specify. - - You can specify extra command-line arguments to be used every time - 'rails new' runs in the .railsrc configuration file in your home directory, - or in $XDG_CONFIG_HOME/rails/railsrc if XDG_CONFIG_HOME is set. - - Note that the arguments specified in the .railsrc file don't affect the - defaults values shown above in this help message. - -Example: - rails new ~/Code/Ruby/weblog - - This generates a skeletal Rails installation in ~/Code/Ruby/weblog. diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0e73df3cc0c..b5c1c4d13a9 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -16,7 +16,6 @@ require 'rack/test' require 'rspec/rails' -require 'falcon' # if testing on localhost diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 8161d769228..f5b4f3ff4d6 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -1,7 +1,6 @@ require 'rack/test' require 'rspec/rails' require 'capybara/rspec' -require 'falcon/capybara' require 'selenium/webdriver' Capybara.register_driver :local_selenium do |app| @@ -28,12 +27,7 @@ end Capybara.configure do |config| - # config.server = :puma, { Silent: true } - # config.app = Rack::Builder.parse_file( - # File.expand_path('../config.ru', __dir__) - # ).first - config.server = :falcon - # config.server = :falcon_https + config.server = :puma, { Silent: true } config.default_driver = :local_selenium # config.default_driver = :selenium_chrome_https config.javascript_driver = :local_selenium_headless diff --git a/spec/support/refinery/generator_spec_describer.rb b/spec/support/refinery/generator_spec_describer.rb index ff91c8b1391..6d4564620f9 100644 --- a/spec/support/refinery/generator_spec_describer.rb +++ b/spec/support/refinery/generator_spec_describer.rb @@ -1,11 +1,17 @@ -class GeneratorSpec::Matcher::File - def description - 'match a file' - end -end +# frozen_string_literal: true + +module GeneratorSpec + module Matcher + class Directory + def description + 'match a directory structure' + end + end -class GeneratorSpec::Matcher::Directory - def description - 'match a directory stucture' + class File + def description + 'match a file' + end + end end end From d766192b1aabd7f1bb9a76b1ed9359384caeb176 Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Thu, 1 Jan 2026 14:08:17 +1300 Subject: [PATCH 54/74] refactor(images): extract presenter refactoring to feature branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit extracts all presenter-pattern refactoring from the ruby3 branch to feature/images-presenter-refactor, resetting images/ to match main's structure. This allows ruby3 branch to focus solely on Ruby 3 and Rails compatibility fixes. Extracted to feature/images-presenter-refactor: - GridPresenter, GroupPresenter, ImagePresenter, ListPresenter classes - ImageObject view object - admin_image_sizes configuration - View template consolidation (_image.html.erb partial) - Naming changes (index_views → image_views, preferred_index_view, etc.) - Helper refactoring (other_index_views, localized, thumbnail URL format) - Spec context refactoring (grid_view, list_view shared contexts) Retained on ruby3 (will be reapplied in next commit): - None yet - this commit restores to main's baseline These changes were bundled in commit 082bc6003 "refactor(refinery-images): make ruby3 compatible" but represent UI/architecture improvements separate from Ruby 3 compatibility requirements. The feature branch tests pass (101 examples, 0 failures). Next commit will apply only the minimal fixes required for Ruby 3 compatibility. --- .../refinery/admin/images_controller.rb | 18 ++-- .../helpers/refinery/admin/images_helper.rb | 21 ++--- .../refinery/admin/images/_actions.html.erb | 8 +- .../admin/images/_existing_image.html.erb | 16 ++-- .../refinery/admin/images/_grid_view.html.erb | 16 ++++ .../refinery/admin/images/_images.html.erb | 18 +--- .../refinery/admin/images/_list_view.html.erb | 10 +++ .../admin/images/_list_view_image.html.erb | 30 +++++++ .../refinery/admin/images/_records.html.erb | 4 +- images/config/locales/en.yml | 6 -- .../initializers/refinery/images.rb.erb | 6 +- images/lib/refinery/images/configuration.rb | 12 +-- images/lib/refinery/images/engine.rb | 2 + .../images/validators/image_size_validator.rb | 15 ++-- .../validators/image_update_validator.rb | 2 +- images/spec/factories/image.rb | 6 +- .../shared_contexts/admin_images_tab.rb | 12 ++- .../support/shared_contexts/many_images.rb | 2 +- .../spec/support/shared_contexts/one_image.rb | 2 +- .../visual_editor_add_image.rb | 27 +++--- .../support/shared_examples/image_deleter.rb | 37 ++++---- .../support/shared_examples/image_editor.rb | 4 +- .../support/shared_examples/image_indexer.rb | 84 +++++++++++-------- .../support/shared_examples/image_inserter.rb | 2 +- .../shared_examples/image_previewer.rb | 6 +- .../shared_examples/image_translator.rb | 10 +-- .../support/shared_examples/image_uploader.rb | 26 +++--- images/spec/support/spec_helper.rb | 12 +-- .../spec/system/refinery/admin/images_spec.rb | 48 +++++------ 29 files changed, 247 insertions(+), 215 deletions(-) create mode 100644 images/app/views/refinery/admin/images/_grid_view.html.erb create mode 100644 images/app/views/refinery/admin/images/_list_view.html.erb create mode 100644 images/app/views/refinery/admin/images/_list_view_image.html.erb diff --git a/images/app/controllers/refinery/admin/images_controller.rb b/images/app/controllers/refinery/admin/images_controller.rb index 69990bb9a97..3caf6a93a8e 100644 --- a/images/app/controllers/refinery/admin/images_controller.rb +++ b/images/app/controllers/refinery/admin/images_controller.rb @@ -1,16 +1,14 @@ module Refinery module Admin - require 'will_paginate/array' class ImagesController < ::Refinery::AdminController crudify :'refinery/image', include: [:translations, :crops], order: "updated_at DESC", sortable: false, - conditions: 'parent_id IS NULL', - find_actions: [:update, :destroy, :edit] + conditions: 'parent_id IS NULL' - before_action :change_view_if_specified, :init_dialog + before_action :change_list_mode_if_specified, :init_dialog def new @image = ::Refinery::Image.new if @image.nil? @@ -160,12 +158,9 @@ def init_dialog @conditions = params[:conditions] end - def change_view_if_specified - return unless params[:view].present? - - view = params[:view].to_sym - if action_name == 'index' && view && Refinery::Images.index_views.include?(view) - Refinery::Images.preferred_index_view = view + def change_list_mode_if_specified + if action_name == 'index' && params[:view].present? && Refinery::Images.image_views.include?(params[:view].to_sym) + Refinery::Images.preferred_image_view = params[:view] end end @@ -189,10 +184,9 @@ def image_params def permitted_image_params [ - { image: [] }, :image_size, :image_title, :image_alt + :image, :image_size, :image_title, :image_alt ] end end - end end diff --git a/images/app/helpers/refinery/admin/images_helper.rb b/images/app/helpers/refinery/admin/images_helper.rb index 86e44465f4d..8f7dd1a1d1f 100644 --- a/images/app/helpers/refinery/admin/images_helper.rb +++ b/images/app/helpers/refinery/admin/images_helper.rb @@ -1,26 +1,23 @@ module Refinery module Admin module ImagesHelper - def other_index_views - Refinery::Images.index_views.reject do |view| - view.to_s == Refinery::Images.preferred_index_view.to_s + def other_image_views + Refinery::Images.image_views.reject do |image_view| + image_view.to_s == Refinery::Images.preferred_image_view.to_s end end def thumbnail_urls(image) - thumbnails = { - original: image_path(image.url), - grid: image_path(image.thumbnail(geometry: Refinery::Images.admin_image_sizes[:thumbnail]).url) + thumbnail_urls = { + :"data-original" => image_path(image.url), + :"data-grid" => image_path(image.thumbnail(:geometry => '135x135#c').url) } - Refinery::Images.user_image_sizes.sort_by { |key, geometry| geometry }.each do |size, pixels| - thumbnails[size.to_s.parameterize] = image_path(image.thumbnail(geometry: pixels).url) + Refinery::Images.user_image_sizes.sort_by{ |key, geometry| geometry}.each do |size, pixels| + thumbnail_urls[:"data-#{size.to_s.parameterize}"] = image_path(image.thumbnail(:geometry => pixels).url) end - { data: thumbnails } - end - def localized(date) - ::I18n.l(Date.parse(date)) + thumbnail_urls end end end diff --git a/images/app/views/refinery/admin/images/_actions.html.erb b/images/app/views/refinery/admin/images/_actions.html.erb index 28d0594f175..bc676070a98 100644 --- a/images/app/views/refinery/admin/images/_actions.html.erb +++ b/images/app/views/refinery/admin/images/_actions.html.erb @@ -3,11 +3,11 @@
      • <%= action_label :add, refinery.new_admin_image_path(dialog: true), t('.create_new_image') %>
      • - <% other_index_views.each do |view| %> + <% other_image_views.each do |image_view| %>
      • - <%= action_label :"switch_view_#{view}", # reference to the view icon - refinery.admin_images_path(view: view, page: params[:page]), - I18n.t('switch_to', view_name: view, scope: 'refinery.admin.images.index.view') + <%= action_label :"switch_view_#{image_view}", + refinery.admin_images_path(view: image_view, page: params[:page]), + t('switch_to', scope: 'refinery.admin.images.index.view', view_name: t("#{image_view}", scope: 'refinery.admin.images.index.view')) %>
      • <% end %> diff --git a/images/app/views/refinery/admin/images/_existing_image.html.erb b/images/app/views/refinery/admin/images/_existing_image.html.erb index 30dc0b26d4d..9d108d76769 100644 --- a/images/app/views/refinery/admin/images/_existing_image.html.erb +++ b/images/app/views/refinery/admin/images/_existing_image.html.erb @@ -1,28 +1,24 @@ -<% image_size = Refinery::Images.admin_image_sizes[:form] %>
        > - <%= render '/refinery/admin/search', url: refinery.insert_admin_images_path(params.to_unsafe_h.dup.except('image')) %> - + <%= render '/refinery/admin/search', :url => refinery.insert_admin_images_path(params.to_unsafe_h.dup.except('image')) %>
        <% if @images.any? %>
          <% @images.each do |image| -%> <%= content_tag :li, class: ('selected' if @image_id == image.id) do %> - <%= image_fu(image, image_size, + <%= image_fu(image, '106x106#c', { alt: image.title, title: image.title, - id: dom_id(image), - data: { - id: image.id, - **thumbnail_urls(image) }) - %> + id: "image_#{image.id}", + 'data-id': image.id + }.merge(thumbnail_urls(image)))-%> <% if image.crops.any? %>
            <% image.crops.each do |crop| %> <%= content_tag :li, class: ('selected' if @image_id == image.id) do %> - <%= image_fu(crop, image_size, { + <%= image_fu(crop, '106x106>', { alt: crop.parent.alt, title: crop.parent.title, id: "image_#{crop.id}", diff --git a/images/app/views/refinery/admin/images/_grid_view.html.erb b/images/app/views/refinery/admin/images/_grid_view.html.erb new file mode 100644 index 00000000000..eaf40f540ba --- /dev/null +++ b/images/app/views/refinery/admin/images/_grid_view.html.erb @@ -0,0 +1,16 @@ +
              + <% @images.each_with_index do |image, index| -%> +
            • + <%= image_fu image, '149x149#c', title: image.title %> + + + <%= action_icon :preview, image.url, t('view_live_html', scope: 'refinery.admin.images') %> + <%= action_icon :edit, refinery.edit_admin_image_path(image), t('edit', scope: 'refinery.admin.images') %> + <%= action_icon :delete, refinery.admin_image_path(image, params.to_unsafe_h.slice(:page)), t('delete', scope: 'refinery.admin.images'), class: 'confirm-delete', + data: { confirm: t('message', scope: 'refinery.admin.delete', title: image.title) } %> + <%= action_icon :info, '#', "Title: #{image.title} Alt text: #{image.alt}" %> + + +
            • + <% end -%> +
            diff --git a/images/app/views/refinery/admin/images/_images.html.erb b/images/app/views/refinery/admin/images/_images.html.erb index f312a72a029..64ed047d1f7 100644 --- a/images/app/views/refinery/admin/images/_images.html.erb +++ b/images/app/views/refinery/admin/images/_images.html.erb @@ -1,18 +1,2 @@ <%= will_paginate @images if Refinery::Admin::ImagesController.pageable? %> -<% - view = Refinery::Images.preferred_index_view || 'list' - presenter = Refinery::Admin.const_get("#{view}_presenter".camelize).new(self) - images = @images.map { |image| Refinery::Admin::ImagePresenter.new(image, self) } -%> - -<% groups = presenter.groups.call(images) %> -<%= tag.ul id: "image_index", class: ['pagination_frame', view, pagination_selector].compact do %> - - <% groups.each do |group_date, group_images| %> - <%= presenter.group_header(group_date) if presenter.group_headers %> - <% group_images.each do |image| %> - <%= render 'refinery/admin/images/image', image: image.index_entry(presenter.identity_keys) %> - <% end %> - <% end %> - -<% end %> +<%= render "#{Refinery::Images.preferred_image_view}_view" %> diff --git a/images/app/views/refinery/admin/images/_list_view.html.erb b/images/app/views/refinery/admin/images/_list_view.html.erb new file mode 100644 index 00000000000..f2479074e17 --- /dev/null +++ b/images/app/views/refinery/admin/images/_list_view.html.erb @@ -0,0 +1,10 @@ +
            + <% group_by_date(@images, :updated_at).each do |container| %> + <% date_time = (image_group = container.last).first.updated_at %> + <% date = Date.parse(date_time.to_s) %> +

            <%= l(date, format: :long ) %>

            +
              + <%= render partial: 'list_view_image', collection: image_group %> +
            + <% end %> +
            diff --git a/images/app/views/refinery/admin/images/_list_view_image.html.erb b/images/app/views/refinery/admin/images/_list_view_image.html.erb new file mode 100644 index 00000000000..cbe228c612b --- /dev/null +++ b/images/app/views/refinery/admin/images/_list_view_image.html.erb @@ -0,0 +1,30 @@ +
          • + + <%= translated_field(list_view_image, :title) %> + + + <% if Refinery::I18n.frontend_locales.many? %> + + <% list_view_image.translations.sort_by{ |t| Refinery::I18n.frontend_locales.index(t.locale)}.each do |translation| %> + <% if translation.image_title.present? %> + <%= link_to refinery.edit_admin_image_path(list_view_image, switch_locale: translation.locale), + class: 'locale', title: translation.locale.upcase do %> + +
            + <%= locale_text_icon(translation.locale.upcase) %> +
            + <% end %> + <% end %> + <% end %> +
            + <% end %> + + <%= list_view_image.image_name %> + + + <%= action_icon :preview, list_view_image.url, t('view_live_html', scope: 'refinery.admin.images') %> + <%= action_icon :edit, refinery.edit_admin_image_path(list_view_image), t('edit', scope: 'refinery.admin.images') %> + <%= action_icon :delete, refinery.admin_image_path(list_view_image), t('delete', scope: 'refinery.admin.images'), class: 'confirm-delete', + data: { confirm: t('message', scope: 'refinery.admin.delete', title: list_view_image.title)} %> + +
          • diff --git a/images/app/views/refinery/admin/images/_records.html.erb b/images/app/views/refinery/admin/images/_records.html.erb index bc833ba283d..4af5e907d29 100644 --- a/images/app/views/refinery/admin/images/_records.html.erb +++ b/images/app/views/refinery/admin/images/_records.html.erb @@ -1,11 +1,11 @@ -<%= render 'refinery/admin/search_header', url: refinery.admin_images_path %> +<%= render 'refinery/admin/search_header', :url => refinery.admin_images_path %>
            <% if @images.any? %> <%= render 'images' %> <% else %>

            <% if searching? %> - <%= t('no_results', scope: 'refinery.admin.search') %> + <%= t('no_results', :scope => 'refinery.admin.search') %> <% else %> <%= t('.no_images_yet') %> diff --git a/images/config/locales/en.yml b/images/config/locales/en.yml index 76bc2ae4ba3..b6cc910d9b8 100644 --- a/images/config/locales/en.yml +++ b/images/config/locales/en.yml @@ -48,15 +48,9 @@ en: insert: existing_image: Library new_image: Upload - alt: Description - filename: File name - title: Title activerecord: models: refinery/image: image - attributes: - image_title: Title - image_alt: Description errors: models: refinery/image: diff --git a/images/lib/generators/refinery/images/templates/config/initializers/refinery/images.rb.erb b/images/lib/generators/refinery/images/templates/config/initializers/refinery/images.rb.erb index 6c1c8fcd2a5..8a78c100beb 100644 --- a/images/lib/generators/refinery/images/templates/config/initializers/refinery/images.rb.erb +++ b/images/lib/generators/refinery/images/templates/config/initializers/refinery/images.rb.erb @@ -26,10 +26,10 @@ Refinery::Images.configure do |config| # config.allowed_mime_types = <%= Refinery::Images.allowed_mime_types.inspect %> # Configure image view options - # config.index_views = <%= Refinery::Images.index_views.inspect %> + # config.image_views = <%= Refinery::Images.image_views.inspect %> - # Configure default index view - # config.preferred_index_view = <%= Refinery::Images.preferred_index_view.inspect %> + # Configure default image view + # config.preferred_image_view = <%= Refinery::Images.preferred_image_view.inspect %> # Configure Dragonfly. # Refer to config/initializers/refinery/dragonfly.rb for the full list of dragonfly configurations which can be used. diff --git a/images/lib/refinery/images/configuration.rb b/images/lib/refinery/images/configuration.rb index 43fd67ec552..440a7ea4cdc 100644 --- a/images/lib/refinery/images/configuration.rb +++ b/images/lib/refinery/images/configuration.rb @@ -6,7 +6,7 @@ module Images config_accessor :max_image_size, :pages_per_dialog, :pages_per_admin_index, :pages_per_dialog_that_have_size_options, :user_image_sizes, :user_image_ratios, - :index_views, :preferred_index_view, :admin_image_sizes, + :image_views, :preferred_image_view, :allowed_mime_types, :allowed_mime_types_msg self.max_image_size = 5_242_880 @@ -23,14 +23,10 @@ module Images '4/3': '1.333', '1:1': 1 } - - self.admin_image_sizes = { - grid: '149x149#c' - } self.allowed_mime_types = %w[image/jpeg image/png image/gif image/tiff] self.allowed_mime_types_msg = self.allowed_mime_types.to_sentence(last_word_connector: ' or ') - self.index_views = [:grid, :list] - self.preferred_index_view = :grid + self.image_views = [:grid, :list] + self.preferred_image_view = :grid # Images should always use these changes to the dragonfly defaults self.dragonfly_name = :refinery_images @@ -39,7 +35,7 @@ module Images # Dragonfly processor to strip image of all profiles and comments (imagemagick conversion -strip) self.dragonfly_processors = [{ name: :strip, - block: -> (content) { ::Dragonfly::ImageMagick::Commands.convert(content, '-strip') } + block: -> (content) { content.process!(:convert, '-strip') } }] end diff --git a/images/lib/refinery/images/engine.rb b/images/lib/refinery/images/engine.rb index 1ae5fd43638..c910402d0df 100644 --- a/images/lib/refinery/images/engine.rb +++ b/images/lib/refinery/images/engine.rb @@ -6,6 +6,8 @@ class Engine < ::Rails::Engine isolate_namespace Refinery engine_name :refinery_images + config.autoload_paths += %W( #{config.root}/lib ) + initializer 'attach-refinery-images-with-dragonfly', :before => :finisher_hook do |app| ::Refinery::Dragonfly.configure!(::Refinery::Images) ::Refinery::Dragonfly.attach!(app, ::Refinery::Images) diff --git a/images/lib/refinery/images/validators/image_size_validator.rb b/images/lib/refinery/images/validators/image_size_validator.rb index e6aaecee7b8..ecfdd589cca 100644 --- a/images/lib/refinery/images/validators/image_size_validator.rb +++ b/images/lib/refinery/images/validators/image_size_validator.rb @@ -2,16 +2,17 @@ module Refinery module Images module Validators class ImageSizeValidator < ActiveModel::Validator + def validate(record) - record.errors.add(:image, ::I18n.t('too_big', - scope: 'activerecord.errors.models.refinery/image', - size: Images.max_image_size)) if too_big(record.image) - end + image = record.image - private - def too_big(image) - image.respond_to?(:length) && image.length > Images.max_image_size + if image.respond_to?(:length) && image.length > Images.max_image_size + record.errors[:image] << ::I18n.t('too_big', + :scope => 'activerecord.errors.models.refinery/image', + :size => Images.max_image_size) + end end + end end end diff --git a/images/lib/refinery/images/validators/image_update_validator.rb b/images/lib/refinery/images/validators/image_update_validator.rb index a2769bf2fde..0ab9fbb7248 100644 --- a/images/lib/refinery/images/validators/image_update_validator.rb +++ b/images/lib/refinery/images/validators/image_update_validator.rb @@ -7,7 +7,7 @@ def validate(record) if record.image_name_changed? record.errors.add :image_name, ::I18n.t("different_file_name", - scope: "activerecord.errors.models.refinery/image") + :scope => "activerecord.errors.models.refinery/image") end end diff --git a/images/spec/factories/image.rb b/images/spec/factories/image.rb index 6d201468180..212fb3425c3 100644 --- a/images/spec/factories/image.rb +++ b/images/spec/factories/image.rb @@ -1,13 +1,13 @@ FactoryBot.define do - factory :image, class: ::Refinery::Image do + factory :image, :class => ::Refinery::Image do image { Refinery.roots('refinery/images').join("spec/fixtures/beach.jpeg") } end - factory :alternate_image, class: ::Refinery::Image do + factory :alternate_image, :class => ::Refinery::Image do image { Refinery.roots('refinery/images').join("spec/fixtures/beach-alternate.jpeg") } end - factory :another_image, class: ::Refinery::Image do + factory :another_image, :class => ::Refinery::Image do image { Refinery.roots('refinery/images').join("spec/fixtures/fathead.png") } end end diff --git a/images/spec/support/shared_contexts/admin_images_tab.rb b/images/spec/support/shared_contexts/admin_images_tab.rb index f50684837fe..61fc059c116 100644 --- a/images/spec/support/shared_contexts/admin_images_tab.rb +++ b/images/spec/support/shared_contexts/admin_images_tab.rb @@ -1,13 +1,17 @@ shared_context 'admin images tab' do - let(:initial_path){refinery.admin_images_path} + let(:open_upload_dialog) { find('a', text: ::I18n.t('create_new_image', scope: 'refinery.admin.images.actions')).click } let(:select_upload) {} let(:initialize_context) {} let(:index_in_frame) {false} let(:dialog_frame_id) {'dialog_iframe'} + let(:initial_path) { refinery.admin_images_path(view: %w(grid list).sample) } let(:index_item_selector) {'#records li'} - let(:image_item) { [index_item_selector, img_selector].join(' ') } - let(:image_title) { [index_item_selector, title_selector].join(' ') } - let(:image_alt) { [index_item_selector, alt_selector ].join(' ') } + let(:gridview_img_selector) {' > img'} + let(:gridview_title_selector) {'[tooltip]'} + let(:gridview_alt_selector) {'[alt]'} + let(:listview_title_selector) {' > span.title'} + let(:listview_filename_selector) {' > span.preview'} + end diff --git a/images/spec/support/shared_contexts/many_images.rb b/images/spec/support/shared_contexts/many_images.rb index c51b1a3d9d1..466c4a20157 100644 --- a/images/spec/support/shared_contexts/many_images.rb +++ b/images/spec/support/shared_contexts/many_images.rb @@ -1,5 +1,5 @@ shared_context 'many images' do - let!(:image) { FactoryBot.create(:image, image_alt: "A Beach", image_title: "Peachy Beachy") } + let!(:image) { FactoryBot.create(:image) } let!(:alt_image) { FactoryBot.create(:alternate_image) } let!(:another_image) { FactoryBot.create(:another_image) } end diff --git a/images/spec/support/shared_contexts/one_image.rb b/images/spec/support/shared_contexts/one_image.rb index b80b0145698..a9cef818d67 100644 --- a/images/spec/support/shared_contexts/one_image.rb +++ b/images/spec/support/shared_contexts/one_image.rb @@ -1,3 +1,3 @@ shared_context "one image" do - let!(:image) { FactoryBot.create(:image, image_alt: "A Beach", image_title: "Peachy Beachy") } + let!(:image) { FactoryBot.create(:image) } end diff --git a/images/spec/support/shared_contexts/visual_editor_add_image.rb b/images/spec/support/shared_contexts/visual_editor_add_image.rb index 992f86806d7..b91f3c4158a 100644 --- a/images/spec/support/shared_contexts/visual_editor_add_image.rb +++ b/images/spec/support/shared_contexts/visual_editor_add_image.rb @@ -1,30 +1,31 @@ shared_context 'Visual Editor - add image' do + let(:page_for_image) { - page = Refinery::Page.create title: "Add Image to me" + page = Refinery::Page.create :title => "Add Image to me" # we need page parts so that there's a visual editor Refinery::Pages.default_parts.each_with_index do |default_page_part, index| - page.parts.create(title: default_page_part, slug: default_page_part, body: nil, position: index) + page.parts.create(:title => default_page_part, :slug => default_page_part, :body => nil, :position => index) end page } - let(:initial_path) { refinery.edit_admin_page_path(page_for_image) } + let(:initialize_context) {click_on('Add Image') } - let(:initialize_context) { click_link('Add Image') } - let(:add_image_selector) { '.page_part_body li.wym_tools_image > a' } let(:open_upload_dialog) {} - let(:select_upload) { choose(::I18n.t('new_image', scope: 'refinery.admin.images.insert')) } + let(:select_upload) {choose(::I18n.t('new_image', :scope => 'refinery.admin.images.insert')) } + + let(:initial_path) { refinery.edit_admin_page_path(page_for_image) } # let(:target_image) { find(:xpath, "//img[@title='Beach']")} # let(:url){target_image["data-#{size}"]} - let(:index_in_frame) { true } - let(:dialog_frame_id) { 'dialog_frame' } - let(:index_item_selector) { '#existing_image_area_content ul li img' } - let(:title_attribute_selector) { '[title]' } - let(:alt_attribute_selector) { '[alt]' } + let(:index_in_frame) {true} + let(:dialog_frame_id) {'dialog_frame'} + let(:index_item_selector) {'#existing_image_area_content ul li img'} + let(:title_attribute_selector) {'[title]'} + let(:alt_attribute_selector) {'[alt]'} - let(:image_title_selector) { |title| '//img[@alt="' << title << '"]' } - let(:image_size_selector) { |size| '//a[@href="#' << size << '"]' } + let(:image_title_selector) {|title| '//img[@alt="' << title << '"]'} + let(:image_size_selector) {|size| '//a[@href="#' << size << '"]'} end diff --git a/images/spec/support/shared_examples/image_deleter.rb b/images/spec/support/shared_examples/image_deleter.rb index 2598bc7000d..a8ff8cf90b1 100644 --- a/images/spec/support/shared_examples/image_deleter.rb +++ b/images/spec/support/shared_examples/image_deleter.rb @@ -1,34 +1,31 @@ -shared_examples_for 'Delete' do +shared_examples_for 'deletes an image' do before do raise "please set let(:initial_path)" if initial_path.blank? ensure_on(initial_path) end - def deleting_an_image - first("#records li").click_link(::I18n.t('delete', scope: 'refinery.admin.images')) - end + let(:image_count) {[Refinery::Image.count, Refinery::Images.pages_per_admin_index].min} + let(:deleting_an_image) { + -> { + first("#records li").click_link(::I18n.t('delete', scope: 'refinery.admin.images')) + } + } - def delete_an_image - first("#records li").click_link(::I18n.t('delete', scope: 'refinery.admin.images')) + it 'has a delete image link for each image' do + expect(page).to have_selector('#records.images li a.confirm-delete', count: image_count) end - let(:image_count) { [Refinery::Image.count, Refinery::Images.pages_per_admin_index].min } - - it 'has a delete image link for each image' do - expect(page).to have_selector('#records.images li a.delete', count: image_count) + it "removes an image" do + expect(deleting_an_image).to change(Refinery::Image, :count).by(-1) end + it 'says the image has been removed' do + image_title = page.has_selector?('#image_grid') ? find("#image_grid li:first").first("img")[:title] : + find("#image_list li:first").first("span.title").text - %i(grid list).each do |view| - init_index_view(view) do - it "removes an image and reports the result" do - expect(page).to have_selector(view_selector) - expect(page).to have_selector('#records li a.delete', count: image_count) + expect(image_title).to be_present - title = first('#records li').find(title_selector).text - expect { deleting_an_image }.to change(Refinery::Image, :count).by(-1) - expect(page).to have_content(::I18n.t('destroyed', scope: 'refinery.crudify', what: "'#{title}'")) - end - end + first(".images_list li:first").click_link(::I18n.t('delete', scope: 'refinery.admin.images')) + expect(page).to have_content(::I18n.t('destroyed', scope: 'refinery.crudify', what: "'#{image_title}'")) end end diff --git a/images/spec/support/shared_examples/image_editor.rb b/images/spec/support/shared_examples/image_editor.rb index 6e272ccd4ba..9a643a06cd7 100644 --- a/images/spec/support/shared_examples/image_editor.rb +++ b/images/spec/support/shared_examples/image_editor.rb @@ -1,3 +1,3 @@ -shared_examples 'Edit' do +shared_examples 'edits an image' do pending -end +end \ No newline at end of file diff --git a/images/spec/support/shared_examples/image_indexer.rb b/images/spec/support/shared_examples/image_indexer.rb index 0c405ef0ee4..0dc60b06e63 100644 --- a/images/spec/support/shared_examples/image_indexer.rb +++ b/images/spec/support/shared_examples/image_indexer.rb @@ -1,6 +1,6 @@ -shared_examples_for 'Index' do +shared_examples_for 'indexes images' do - let(:image_count) { [Refinery::Image.count, Refinery::Images.pages_per_admin_index].min } + let(:image_count) {[Refinery::Image.count, Refinery::Images.pages_per_admin_index].min} before do raise "please set let(:initial_path)" if initial_path.blank? @@ -17,47 +17,66 @@ expect(page).to have_selector(index_item_selector, count: image_count) end end -end -shared_examples_for 'All Index Views' do - it 'shows the alt attribute of each image if it different from the title', js: true do - expect(page).to have_selector(image_alt, text: 'A Beach', count: 1) - end +end # image index - it 'shows the title attribute of an image', js: true do - expect(page).to have_selector(image_title, count: image_count) - end +shared_examples_for 'shows list and grid views' do - it 'has an option to switch to another view', js: true do - expect(page).to have_content(::I18n.t('switch_to', view_name: alt_view, scope: 'refinery.admin.images.index.view')) - end -end + let(:image_count) {[Refinery::Image.count, Refinery::Images.pages_per_admin_index].min} -shared_examples_for 'Index Views' do - let(:image_count) { [Refinery::Image.count, Refinery::Images.pages_per_admin_index].min } + before do + raise "please set let(:initial_path)" if initial_path.blank? + ensure_on(initial_path) + end context "when in grid view" do - init_index_view('grid') do - it_behaves_like 'All Index Views' - it 'shows thumbnails', js: true do - expect(page).to have_selector(image_item, count: image_count) - end + + before { ensure_on(current_path + "?view=grid") } + + it 'shows the images with thumbnails', js: true do + expect(page).to have_selector(index_item_selector << gridview_img_selector, count: image_count) + end + + it 'makes the title attribute of each image available', js: true do + expect(page).to have_selector(index_item_selector << gridview_img_selector << gridview_title_selector, count: image_count) + end + + it 'makes the alt attribute of each image available', js: true do + expect(page).to have_selector(index_item_selector << gridview_img_selector << gridview_alt_selector, count: image_count) end - end # grid view + + it 'has an option to switch to list view' do + expect(page).to have_content(::I18n.t('switch_to', view_name: 'list', scope: 'refinery.admin.images.index.view')) + end + + end # grid view context "when in list view" do - init_index_view('list') do - it_behaves_like 'All Index Views' - it 'show filenames' do - expect(page).to have_selector(filename, count: image_count) - end + + before do + ensure_on(current_path + "?view=list") + end + + it 'makes the title attribute of each image available', js: true do + expect(page).to have_selector(index_item_selector << listview_title_selector, count: image_count) end - end + it 'makes the filename of each image available' do + expect(page).to have_selector(index_item_selector << listview_filename_selector, count: image_count) + end + + it 'has an option to switch to grid view' do + ensure_on(current_path + '?view=list') + + expect(page).to have_content(::I18n.t('switch_to', view_name: 'grid', scope: 'refinery.admin.images.index.view')) + end + + end # list view end -shared_examples_for 'Index Pagination' do - let(:image_count) { [Refinery::Image.count, Refinery::Images.pages_per_admin_index].min } +shared_examples_for 'paginates the list of images' do + + let(:image_count) {[Refinery::Image.count, Refinery::Images.pages_per_admin_index].min} before do raise "please set let(:initial_path)" if initial_path.blank? @@ -93,8 +112,7 @@ it 'has disabled the next link' do expect(page).to have_selector('.next_page.disabled') end - end # last page - - end # pagination + end + end # pagination end # image index diff --git a/images/spec/support/shared_examples/image_inserter.rb b/images/spec/support/shared_examples/image_inserter.rb index d4e22c6aec9..60d6e002ba6 100644 --- a/images/spec/support/shared_examples/image_inserter.rb +++ b/images/spec/support/shared_examples/image_inserter.rb @@ -1,5 +1,5 @@ -shared_examples 'Insert' do +shared_examples 'inserts images' do before do raise "please set let(:initial_path)" if initial_path.blank? ensure_on(initial_path) diff --git a/images/spec/support/shared_examples/image_previewer.rb b/images/spec/support/shared_examples/image_previewer.rb index 71206e6137c..9fe47e8ebc7 100644 --- a/images/spec/support/shared_examples/image_previewer.rb +++ b/images/spec/support/shared_examples/image_previewer.rb @@ -1,5 +1,5 @@ def preview_image - preview_window = Capybara.window_opened_by do + preview_window = window_opened_by do find(:linkhref, image_url).click end @@ -9,14 +9,14 @@ def preview_image preview_window.close end -shared_examples 'Preview' do +shared_examples 'shows an image preview' do before do raise "please set let(:initial_path)" if initial_path.blank? ensure_on(initial_path) end let(:image_url) { - uri = URI(first(:xpath, "//a[contains(concat(' ', @class, ' '), ' preview ')]")[:href]) + uri = URI(first(:xpath, "//a[@class='preview_icon']")[:href]) uri.path << '?' << uri.query } diff --git a/images/spec/support/shared_examples/image_translator.rb b/images/spec/support/shared_examples/image_translator.rb index c0c0dad24a2..8341544fd56 100644 --- a/images/spec/support/shared_examples/image_translator.rb +++ b/images/spec/support/shared_examples/image_translator.rb @@ -1,4 +1,4 @@ -shared_examples 'Translate' do +shared_examples 'translates an image' do before do allow(Refinery::I18n).to receive(:frontend_locales).and_return([:en, :fr]) ensure_on(initial_path) @@ -16,16 +16,16 @@ click_link "Edit this image" within "#switch_locale_picker" do - click_link "fr" + click_link "FR" end - fill_in "Title", with: "Titre de la première image" - fill_in "Alt", with: "Texte alternatif de la première image" + fill_in "Title", :with => "Titre de la première image" + fill_in "Alt", :with => "Texte alternatif de la première image" click_button "Save" expect(page).to have_content("'Titre de la première image' was successfully updated.") - expect(Refinery::Image::Translation.count).to eq(2) + expect(Refinery::Image::Translation.count).to eq(1) end end end diff --git a/images/spec/support/shared_examples/image_uploader.rb b/images/spec/support/shared_examples/image_uploader.rb index 927f30758a7..a55f6d45832 100644 --- a/images/spec/support/shared_examples/image_uploader.rb +++ b/images/spec/support/shared_examples/image_uploader.rb @@ -1,35 +1,37 @@ -shared_examples 'Upload' do +shared_examples 'uploads images' do before do raise "please set let(:initial_path)" if initial_path.blank? ensure_on(initial_path) initialize_context end - def uploading_an_image + let(:uploading_an_image) { + ->{ open_upload_dialog page.within_frame(dialog_frame_id) do select_upload attach_file 'image_image', image_path - fill_in 'image_image_title', with: 'Image With Dashes' - fill_in 'image_image_alt', with: "Alt description for image" + fill_in 'image_image_title', with: 'Image With Dashes' + fill_in 'image_image_alt', with: "Alt description for image" click_button ::I18n.t('save', scope: 'refinery.admin.form_actions') end - end + } + } context 'when the image type is acceptable' do - let(:image_path) { Refinery.roots('refinery/images').join("spec/fixtures/image-with-dashes.jpg") } - it 'the image is uploaded', js: true do - expect { uploading_an_image }.to change(Refinery::Image, :count).by(1) + let(:image_path) {Refinery.roots('refinery/images').join("spec/fixtures/image-with-dashes.jpg")} + it 'the image is uploaded', :js => true do + expect(uploading_an_image).to change(Refinery::Image, :count).by(1) end end context 'when the image type is not acceptable' do - let(:image_path) { Refinery.roots('refinery/images').join("spec/fixtures/cape-town-tide-table.pdf") } - it 'the image is rejected', js: true do - expect { uploading_an_image }.to_not change(Refinery::Image, :count) + let(:image_path) {Refinery.roots('refinery/images').join("spec/fixtures/cape-town-tide-table.pdf")} + it 'the image is rejected', :js => true do + expect(uploading_an_image).to_not change(Refinery::Image, :count) page.within_frame(dialog_frame_id) do expect(page).to have_content(::I18n.t('incorrect_format', - scope: 'activerecord.errors.models.refinery/image', + :scope => 'activerecord.errors.models.refinery/image', actual: 'application/pdf', valid_types: 'image/jpeg, image/png, image/gif or image/tiff')) end diff --git a/images/spec/support/spec_helper.rb b/images/spec/support/spec_helper.rb index 6c6796564f3..6cd287bc092 100644 --- a/images/spec/support/spec_helper.rb +++ b/images/spec/support/spec_helper.rb @@ -6,17 +6,7 @@ def has_image?(src) end add_selector(:linkhref) do - xpath { |href| ".//a[@href='#{href}']" } - end -end - -def init_index_view(view, &block) - include_context( "#{view} view") do - before do - ensure_on(initial_path) - initialize_context - end - yield + xpath {|href| ".//a[@href='#{href}']"} end end diff --git a/images/spec/system/refinery/admin/images_spec.rb b/images/spec/system/refinery/admin/images_spec.rb index 6afa06bd673..1dc1b269452 100644 --- a/images/spec/system/refinery/admin/images_spec.rb +++ b/images/spec/system/refinery/admin/images_spec.rb @@ -13,31 +13,31 @@ module Refinery expect(page).to have_content(::I18n.t('no_images_yet', scope: 'refinery.admin.images.records')) end - it_has_behaviour 'Upload' + it_has_behaviour 'uploads images' end context 'When there is one image' do include_context 'one image' - it_has_behaviour 'Index' - it_has_behaviour 'Index Views' - it_has_behaviour 'Preview' - it_has_behaviour 'Delete' - it_has_behaviour 'Edit' - it_has_behaviour 'Upload' - it_has_behaviour 'Translate' + it_has_behaviour 'indexes images' + it_has_behaviour 'shows list and grid views' + it_has_behaviour 'shows an image preview' + it_has_behaviour 'deletes an image' + it_has_behaviour 'edits an image' + it_has_behaviour 'uploads images' + it_has_behaviour 'translates an image' end context 'When there are many images' do include_context 'many images' - it_has_behaviour 'Index' - it_has_behaviour 'Index Views' - it_has_behaviour 'Index Pagination' - it_has_behaviour 'Preview' - it_has_behaviour 'Delete' - it_has_behaviour 'Upload' - it_has_behaviour 'Edit' + it_has_behaviour 'indexes images' + it_has_behaviour 'shows list and grid views' + it_has_behaviour 'paginates the list of images' + it_has_behaviour 'shows an image preview' + it_has_behaviour 'deletes an image' + it_has_behaviour 'uploads images' + it_has_behaviour 'edits an image' end end @@ -49,25 +49,25 @@ module Refinery context 'When there are no images' do include_context 'no existing images' - it_has_behaviour 'Upload' + it_has_behaviour 'uploads images' end context 'When there is one image' do include_context 'one image' - it_has_behaviour 'Index' - it_has_behaviour 'Index Pagination' - it_has_behaviour 'Upload' - it_has_behaviour 'Insert' + it_has_behaviour 'indexes images' + it_has_behaviour 'paginates the list of images' + it_has_behaviour 'uploads images' + it_has_behaviour 'inserts images' end context 'When there are many images' do include_context 'many images' - it_has_behaviour 'Index' - it_has_behaviour 'Index Pagination' - it_has_behaviour 'Upload' - it_has_behaviour 'Insert' + it_has_behaviour 'indexes images' + it_has_behaviour 'paginates the list of images' + it_has_behaviour 'uploads images' + it_has_behaviour 'inserts images' end end From b5cabb9a665648b7ac04b9428ef3b9737c12346b Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Thu, 1 Jan 2026 14:33:52 +1300 Subject: [PATCH 55/74] fix(images): add minimal Ruby 3 and Rails compatibility fixes Apply only the essential changes required for Ruby 3 and modern Rails compatibility, without any UI/architecture refactoring. Changes: - Controller: Add frozen_string_literal, require will_paginate/array - Controller: Add find_actions to crudify (required for updated CRUD) - Controller: Fix strong params to allow array uploads: { image: [] } - Helper: Add frozen_string_literal - Helper: Convert data attributes to Ruby 3 keyword args format - Old: :"data-original" => value - New: { data: { original: value } } - Helper: Convert :geometry => to geometry: keyword syntax - Helper: Add locale_text_icon() stub (flags removed in refinerycms-i18n PR#104) - View: Update data attributes to use new hash format with splat operator - View: Use dom_id() helper instead of manual string interpolation - Validators: Update to use errors.add() instead of deprecated errors[:key] << - Validators: Add frozen_string_literal, inline private methods - Validators: Convert hash rockets to keyword arguments - Specs: Fix RSpec expect blocks (explicit vs implicit) - Old: expect(lambda) - New: expect { lambda.call } - Specs: Fix XPath selector to use contains() for multi-class elements - Fixes preview icon detection with multiple CSS classes - Specs: Update locale picker test to use ID selector instead of text - Old: click_link "FR" (text no longer available without flag images) - New: find("a#fr").click Core changes: - TranslationHelper: Convert t() method to use keyword arguments (**options) - ActionHelper: Add locale_language() helper to get locale display name - Config: Restore main's locale_picker partial structure Test Results: - 99 examples, 0 failures, 8 pending All main branch specs now pass with Ruby 3.2.9. Related to commit 082bc6003 but extracts only compatibility requirements. --- core/app/helpers/refinery/action_helper.rb | 4 ++++ .../helpers/refinery/translation_helper.rb | 19 ------------------- .../refinery/admin/images_controller.rb | 9 +++++++-- .../helpers/refinery/admin/images_helper.rb | 18 ++++++++++++------ .../admin/images/_existing_image.html.erb | 18 +++++++++++------- .../images/validators/image_size_validator.rb | 16 ++++++++-------- .../validators/image_update_validator.rb | 6 +++--- .../support/shared_examples/image_deleter.rb | 2 +- .../shared_examples/image_previewer.rb | 2 +- .../shared_examples/image_translator.rb | 2 +- .../support/shared_examples/image_uploader.rb | 4 ++-- 11 files changed, 50 insertions(+), 50 deletions(-) diff --git a/core/app/helpers/refinery/action_helper.rb b/core/app/helpers/refinery/action_helper.rb index fb03eb92f47..37d533fe74c 100644 --- a/core/app/helpers/refinery/action_helper.rb +++ b/core/app/helpers/refinery/action_helper.rb @@ -37,6 +37,10 @@ def edit_in_current_locale(url:, title:, **options) action_icon(:edit, url, title, class: :edit, **options ) end + def locale_language(locale) + Refinery::I18n.locales[locale] + end + def edit_in_locale(locale, url:, title:, **options) if options.delete(:label) title = locale_language(locale) diff --git a/core/app/helpers/refinery/translation_helper.rb b/core/app/helpers/refinery/translation_helper.rb index 959db854674..d211543bb75 100644 --- a/core/app/helpers/refinery/translation_helper.rb +++ b/core/app/helpers/refinery/translation_helper.rb @@ -14,24 +14,5 @@ def t(key, **options) def translated_field(record, field) Refinery::TranslatedFieldPresenter.new(record).call(field) end - - def locales_with_translated_field(record, field_name, include_current: true) - field_name = field_name.to_sym - translations = record.translations.where.not(field_name => [nil, ""]) - translations = translations.where.not(locale: current_locale) unless include_current - translations.map { |t| t.locale.to_sym }.sort_by { |locale| Refinery::I18n.frontend_locales.index(locale) } - end - - def current_locale?(locale) = locale.to_sym == Refinery::I18n.current_locale - def current_locale = Refinery::I18n.current_locale.to_s - def current_language = Refinery::I18n.locales[Refinery::I18n.current_locale] - def locale_language(locale) = Refinery::I18n.locales[locale.to_sym] - - private - - def sorted_locales(locales, frontend_locales) - locales.map(&:to_sym).sort_by { |locale| frontend_locales.index(locale) } - end - end end diff --git a/images/app/controllers/refinery/admin/images_controller.rb b/images/app/controllers/refinery/admin/images_controller.rb index 3caf6a93a8e..5d5596da7b3 100644 --- a/images/app/controllers/refinery/admin/images_controller.rb +++ b/images/app/controllers/refinery/admin/images_controller.rb @@ -1,3 +1,7 @@ +# frozen_string_literal: true + +require 'will_paginate/array' + module Refinery module Admin class ImagesController < ::Refinery::AdminController @@ -6,7 +10,8 @@ class ImagesController < ::Refinery::AdminController include: [:translations, :crops], order: "updated_at DESC", sortable: false, - conditions: 'parent_id IS NULL' + conditions: 'parent_id IS NULL', + find_actions: [:update, :destroy, :edit] before_action :change_list_mode_if_specified, :init_dialog @@ -184,7 +189,7 @@ def image_params def permitted_image_params [ - :image, :image_size, :image_title, :image_alt + { image: [] }, :image_size, :image_title, :image_alt ] end end diff --git a/images/app/helpers/refinery/admin/images_helper.rb b/images/app/helpers/refinery/admin/images_helper.rb index 8f7dd1a1d1f..c62a0bb5301 100644 --- a/images/app/helpers/refinery/admin/images_helper.rb +++ b/images/app/helpers/refinery/admin/images_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Refinery module Admin module ImagesHelper @@ -7,17 +9,21 @@ def other_image_views end end + def locale_text_icon(text) + text + end + def thumbnail_urls(image) - thumbnail_urls = { - :"data-original" => image_path(image.url), - :"data-grid" => image_path(image.thumbnail(:geometry => '135x135#c').url) + thumbnails = { + original: image_path(image.url), + grid: image_path(image.thumbnail(geometry: '135x135#c').url) } - Refinery::Images.user_image_sizes.sort_by{ |key, geometry| geometry}.each do |size, pixels| - thumbnail_urls[:"data-#{size.to_s.parameterize}"] = image_path(image.thumbnail(:geometry => pixels).url) + Refinery::Images.user_image_sizes.sort_by { |key, geometry| geometry }.each do |size, pixels| + thumbnails[size.to_s.parameterize] = image_path(image.thumbnail(geometry: pixels).url) end - thumbnail_urls + { data: thumbnails } end end end diff --git a/images/app/views/refinery/admin/images/_existing_image.html.erb b/images/app/views/refinery/admin/images/_existing_image.html.erb index 9d108d76769..0682ea6496d 100644 --- a/images/app/views/refinery/admin/images/_existing_image.html.erb +++ b/images/app/views/refinery/admin/images/_existing_image.html.erb @@ -6,24 +6,28 @@

              <% @images.each do |image| -%> <%= content_tag :li, class: ('selected' if @image_id == image.id) do %> - <%= image_fu(image, '106x106#c', { + <%= image_fu(image, '106x106#c', alt: image.title, title: image.title, - id: "image_#{image.id}", - 'data-id': image.id - }.merge(thumbnail_urls(image)))-%> + id: dom_id(image), + data: { + id: image.id, + **thumbnail_urls(image)[:data] + })-%> <% if image.crops.any? %>
                <% image.crops.each do |crop| %> <%= content_tag :li, class: ('selected' if @image_id == image.id) do %> - <%= image_fu(crop, '106x106>', { + <%= image_fu(crop, '106x106>', alt: crop.parent.alt, title: crop.parent.title, id: "image_#{crop.id}", - 'data-id': crop.id - }.merge(thumbnail_urls(crop))) %> + data: { + id: crop.id, + **thumbnail_urls(crop)[:data] + }) %> <% end %> <% end %>
              diff --git a/images/lib/refinery/images/validators/image_size_validator.rb b/images/lib/refinery/images/validators/image_size_validator.rb index ecfdd589cca..9236f0a902b 100644 --- a/images/lib/refinery/images/validators/image_size_validator.rb +++ b/images/lib/refinery/images/validators/image_size_validator.rb @@ -1,18 +1,18 @@ +# frozen_string_literal: true + module Refinery module Images module Validators class ImageSizeValidator < ActiveModel::Validator - def validate(record) - image = record.image - - if image.respond_to?(:length) && image.length > Images.max_image_size - record.errors[:image] << ::I18n.t('too_big', - :scope => 'activerecord.errors.models.refinery/image', - :size => Images.max_image_size) - end + record.errors.add(:image, ::I18n.t('too_big', + scope: 'activerecord.errors.models.refinery/image', + size: Images.max_image_size)) if too_big(record.image) end + private def too_big(image) + image.respond_to?(:length) && image.length > Images.max_image_size + end end end end diff --git a/images/lib/refinery/images/validators/image_update_validator.rb b/images/lib/refinery/images/validators/image_update_validator.rb index 0ab9fbb7248..eec38506a46 100644 --- a/images/lib/refinery/images/validators/image_update_validator.rb +++ b/images/lib/refinery/images/validators/image_update_validator.rb @@ -1,16 +1,16 @@ +# frozen_string_literal: true + module Refinery module Images module Validators class ImageUpdateValidator < ActiveModel::Validator - def validate(record) if record.image_name_changed? record.errors.add :image_name, ::I18n.t("different_file_name", - :scope => "activerecord.errors.models.refinery/image") + scope: "activerecord.errors.models.refinery/image") end end - end end end diff --git a/images/spec/support/shared_examples/image_deleter.rb b/images/spec/support/shared_examples/image_deleter.rb index a8ff8cf90b1..aa5b2d5efcf 100644 --- a/images/spec/support/shared_examples/image_deleter.rb +++ b/images/spec/support/shared_examples/image_deleter.rb @@ -16,7 +16,7 @@ end it "removes an image" do - expect(deleting_an_image).to change(Refinery::Image, :count).by(-1) + expect { deleting_an_image.call }.to change(Refinery::Image, :count).by(-1) end it 'says the image has been removed' do diff --git a/images/spec/support/shared_examples/image_previewer.rb b/images/spec/support/shared_examples/image_previewer.rb index 9fe47e8ebc7..895a94fa87d 100644 --- a/images/spec/support/shared_examples/image_previewer.rb +++ b/images/spec/support/shared_examples/image_previewer.rb @@ -16,7 +16,7 @@ def preview_image end let(:image_url) { - uri = URI(first(:xpath, "//a[@class='preview_icon']")[:href]) + uri = URI(first(:xpath, "//a[contains(@class, 'preview_icon')]")[:href]) uri.path << '?' << uri.query } diff --git a/images/spec/support/shared_examples/image_translator.rb b/images/spec/support/shared_examples/image_translator.rb index 8341544fd56..382db2ac451 100644 --- a/images/spec/support/shared_examples/image_translator.rb +++ b/images/spec/support/shared_examples/image_translator.rb @@ -16,7 +16,7 @@ click_link "Edit this image" within "#switch_locale_picker" do - click_link "FR" + find("a#fr").click end fill_in "Title", :with => "Titre de la première image" diff --git a/images/spec/support/shared_examples/image_uploader.rb b/images/spec/support/shared_examples/image_uploader.rb index a55f6d45832..824c433e7d5 100644 --- a/images/spec/support/shared_examples/image_uploader.rb +++ b/images/spec/support/shared_examples/image_uploader.rb @@ -21,14 +21,14 @@ context 'when the image type is acceptable' do let(:image_path) {Refinery.roots('refinery/images').join("spec/fixtures/image-with-dashes.jpg")} it 'the image is uploaded', :js => true do - expect(uploading_an_image).to change(Refinery::Image, :count).by(1) + expect { uploading_an_image.call }.to change(Refinery::Image, :count).by(1) end end context 'when the image type is not acceptable' do let(:image_path) {Refinery.roots('refinery/images').join("spec/fixtures/cape-town-tide-table.pdf")} it 'the image is rejected', :js => true do - expect(uploading_an_image).to_not change(Refinery::Image, :count) + expect { uploading_an_image.call }.to_not change(Refinery::Image, :count) page.within_frame(dialog_frame_id) do expect(page).to have_content(::I18n.t('incorrect_format', :scope => 'activerecord.errors.models.refinery/image', From 7258aab22fea9505aa34df836939689ac7582c73 Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Thu, 1 Jan 2026 14:40:43 +1300 Subject: [PATCH 56/74] Fix locale picker to display locale codes instead of language names refinerycms-i18n PR#104 removed flag images, requiring text-based locale indicators. Changed edit_in_locale helper to display uppercase locale codes (e.g., "FR", "EN") instead of full language names when in label mode. This allows the translation spec to test locale switching by clicking the locale code link text. --- core/app/helpers/refinery/action_helper.rb | 2 +- images/spec/support/shared_examples/image_translator.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/app/helpers/refinery/action_helper.rb b/core/app/helpers/refinery/action_helper.rb index 37d533fe74c..734ac5ed625 100644 --- a/core/app/helpers/refinery/action_helper.rb +++ b/core/app/helpers/refinery/action_helper.rb @@ -43,7 +43,7 @@ def locale_language(locale) def edit_in_locale(locale, url:, title:, **options) if options.delete(:label) - title = locale_language(locale) + title = locale.to_s.upcase action_label(:locale, "#{url}?switch_locale=#{locale}", title, **options, id: locale, class: :edit, **options ) diff --git a/images/spec/support/shared_examples/image_translator.rb b/images/spec/support/shared_examples/image_translator.rb index 382db2ac451..8341544fd56 100644 --- a/images/spec/support/shared_examples/image_translator.rb +++ b/images/spec/support/shared_examples/image_translator.rb @@ -16,7 +16,7 @@ click_link "Edit this image" within "#switch_locale_picker" do - find("a#fr").click + click_link "FR" end fill_in "Title", :with => "Titre de la première image" From 3551fe0cd5920f17d7282925c6ec4ba7b04b365f Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Thu, 1 Jan 2026 14:46:27 +1300 Subject: [PATCH 57/74] disable selenium telemetry --- spec/support/capybara.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index f5b4f3ff4d6..37c2704b621 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -3,6 +3,9 @@ require 'capybara/rspec' require 'selenium/webdriver' +# Disable Selenium Manager telemetry +ENV['SE_AVOID_STATS'] = 'true' + Capybara.register_driver :local_selenium do |app| options = Selenium::WebDriver::Options.firefox # options = Selenium::WebDriver::Chrome::Options.new From c3c3cbca2c562158932f4ab53be4b275911897e2 Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Thu, 1 Jan 2026 19:02:10 +1300 Subject: [PATCH 58/74] Add locales_with_translated_field helper for locale picker views The refactored pages and resources views use locales_with_translated_field helper to display locale indicators. This helper queries translations to find which locales have content for a given field. Also revert xhr_paging spec to use image_grid selector instead of image_index, which was part of the UI refactoring that should stay in the feature branch. --- core/app/helpers/refinery/translation_helper.rb | 11 +++++++++++ core/spec/system/refinery/admin/xhr_paging_spec.rb | 6 +++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/core/app/helpers/refinery/translation_helper.rb b/core/app/helpers/refinery/translation_helper.rb index d211543bb75..e5e3af598ba 100644 --- a/core/app/helpers/refinery/translation_helper.rb +++ b/core/app/helpers/refinery/translation_helper.rb @@ -14,5 +14,16 @@ def t(key, **options) def translated_field(record, field) Refinery::TranslatedFieldPresenter.new(record).call(field) end + + def locales_with_translated_field(record, field_name, include_current: true) + field_name = field_name.to_sym + translations = record.translations.where.not(field_name => [nil, ""]) + translations = translations.where.not(locale: current_locale) unless include_current + translations.map { |t| t.locale.to_sym }.sort_by { |locale| Refinery::I18n.frontend_locales.index(locale) } + end + + private def current_locale + Refinery::I18n.current_locale.to_s + end end end diff --git a/core/spec/system/refinery/admin/xhr_paging_spec.rb b/core/spec/system/refinery/admin/xhr_paging_spec.rb index 7ffafc7b8ab..1a91f81a5ae 100644 --- a/core/spec/system/refinery/admin/xhr_paging_spec.rb +++ b/core/spec/system/refinery/admin/xhr_paging_spec.rb @@ -21,8 +21,8 @@ module Refinery it 'performs ajax paging of index' do visit refinery.admin_images_path - expect(page).to have_selector('ul#image_index li.image', count: 1) - expect(page).to have_css(%Q{img[alt="#{first_image.alt}"]}) + expect(page).to have_selector('ul#image_grid li > img', count: 1) + expect(page).to have_css(%Q{img[alt="#{first_image.title}"]}) # placeholder which would disappear in a full page refresh. page.execute_script("node = document.createElement('i');") @@ -33,7 +33,7 @@ module Refinery click_link '2' end - expect(page).to have_css(%Q{img[alt="#{last_image.alt}"]}) + expect(page).to have_css(%Q{img[alt="#{last_image.title}"]}) expect(page.evaluate_script( %{$('i#has_not_refreshed_entire_page').length} )).to eq(1) From 43ff94537ed58297b9ef5138970d4befcebcf075 Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Thu, 1 Jan 2026 20:55:56 +1300 Subject: [PATCH 59/74] Fix xhr_paging spec to ensure grid view is used Use allow() to stub preferred_image_view to :grid, consistent with the existing mocking pattern for per_page. This ensures the spec tests grid view without side effects on other tests. --- core/spec/system/refinery/admin/xhr_paging_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/core/spec/system/refinery/admin/xhr_paging_spec.rb b/core/spec/system/refinery/admin/xhr_paging_spec.rb index 1a91f81a5ae..9a8aa9da8fb 100644 --- a/core/spec/system/refinery/admin/xhr_paging_spec.rb +++ b/core/spec/system/refinery/admin/xhr_paging_spec.rb @@ -16,6 +16,7 @@ module Refinery before do allow(Image).to receive(:per_page).and_return(1) + allow(Refinery::Images).to receive(:preferred_image_view).and_return(:grid) end it 'performs ajax paging of index' do From 386ae33f5ab694a4993739e0c4a0aa48249c0cf7 Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Thu, 1 Jan 2026 21:44:08 +1300 Subject: [PATCH 60/74] Use allow() for config stubs instead of setting/unsetting in after blocks Replace before/after pattern with allow() stubs for: - Refinery::Pages.auto_expand_admin_tree - Refinery::I18n.frontend_locales - Refinery::Pages.new_page_parts This is more consistent with the existing mocking pattern, automatically cleans up after each test, and avoids potential side effects from mutating global configuration state. --- pages/spec/system/refinery/admin/pages_spec.rb | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/pages/spec/system/refinery/admin/pages_spec.rb b/pages/spec/system/refinery/admin/pages_spec.rb index 07632c2d2ec..16f4ac1bd36 100644 --- a/pages/spec/system/refinery/admin/pages_spec.rb +++ b/pages/spec/system/refinery/admin/pages_spec.rb @@ -94,14 +94,10 @@ module Admin context "with auto expand option turned off" do before do - Refinery::Pages.auto_expand_admin_tree = false + allow(Refinery::Pages).to receive(:auto_expand_admin_tree).and_return(false) visit refinery.admin_pages_path end - after do - Refinery::Pages.auto_expand_admin_tree = true - end - it "show parent page" do expect(page).to have_content(company.title) @@ -465,11 +461,7 @@ module Admin context "with translations" do before do - Refinery::I18n.config.frontend_locales = [:en, :ru] - end - - after do - Refinery::I18n.config.frontend_locales = [:en] + allow(Refinery::I18n).to receive(:frontend_locales).and_return([:en, :ru]) end before do @@ -708,11 +700,7 @@ module Admin describe "new page part" do before do - Refinery::Pages.new_page_parts = true - end - - after do - Refinery::Pages.new_page_parts = false + allow(Refinery::Pages).to receive(:new_page_parts).and_return(true) end it "adds new page part", js: true do From 83d9d017e3f693b7dbc438b7e98de2757a9ac66d Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Thu, 1 Jan 2026 21:53:23 +1300 Subject: [PATCH 61/74] Add specs for flash notice after successful upload Test that flash notice appears after successfully uploading an image or resource through the main interface (not dialog). This ensures the flash message displays the correct title and doesn't persist across page reloads. These specs serve as regression tests to prevent flash message issues. --- images/spec/system/refinery/admin/images_spec.rb | 12 ++++++++++++ .../spec/system/refinery/admin/resources_spec.rb | 11 +++++++++++ 2 files changed, 23 insertions(+) diff --git a/images/spec/system/refinery/admin/images_spec.rb b/images/spec/system/refinery/admin/images_spec.rb index 1dc1b269452..86a8e88df3d 100644 --- a/images/spec/system/refinery/admin/images_spec.rb +++ b/images/spec/system/refinery/admin/images_spec.rb @@ -13,6 +13,18 @@ module Refinery expect(page).to have_content(::I18n.t('no_images_yet', scope: 'refinery.admin.images.records')) end + it 'shows flash notice after successful upload' do + visit refinery.new_admin_image_path + + attach_file 'image_image', Refinery.roots('refinery/images').join("spec/fixtures/beach.jpeg") + fill_in 'image_image_title', with: 'Beach Photo' + fill_in 'image_image_alt', with: 'A beach' + click_button 'Save' + + expect(page).to have_content("'Beach Photo' was successfully added.") + expect(page).to have_current_path(refinery.admin_images_path) + end + it_has_behaviour 'uploads images' end diff --git a/resources/spec/system/refinery/admin/resources_spec.rb b/resources/spec/system/refinery/admin/resources_spec.rb index a476674b898..7d8dacc4ca7 100644 --- a/resources/spec/system/refinery/admin/resources_spec.rb +++ b/resources/spec/system/refinery/admin/resources_spec.rb @@ -12,6 +12,17 @@ module Admin visit refinery.admin_resources_path expect(page).to have_content('There are no files yet. Click "Upload new file" to add your first file.') end + + it 'shows flash notice after successful upload' do + visit refinery.new_admin_resource_path + + attach_file 'resource_file', Refinery.roots('refinery/resources').join('spec/fixtures/cape-town-tide-table.pdf') + fill_in 'resource_resource_title', with: 'Tide Table' + click_button 'Save' + + expect(page).to have_content("'Tide Table' was successfully added.") + expect(page).to have_current_path(refinery.admin_resources_path) + end end it 'shows upload file link' do From 1463c7c470617aa1889f269a28f77c81e5a7be37 Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Fri, 2 Jan 2026 10:27:37 +1300 Subject: [PATCH 62/74] misc fixes --- .gitignore | 1 - Gemfile | 4 +- images/app/viewobjects/image_object.rb | 56 ------------ .../views/refinery/admin/pages/_page.html.erb | 12 ++- pages/spec/support/selector_helpers.rb | 85 ++++++++++--------- 5 files changed, 51 insertions(+), 107 deletions(-) delete mode 100644 images/app/viewobjects/image_object.rb diff --git a/.gitignore b/.gitignore index b35ceabdd93..d9073f40c3c 100644 --- a/.gitignore +++ b/.gitignore @@ -97,4 +97,3 @@ Gemfile.lock # rspec failures .rspec_failures -Refinery Review.md diff --git a/Gemfile b/Gemfile index 4c058a30264..5305f4dd387 100644 --- a/Gemfile +++ b/Gemfile @@ -19,7 +19,9 @@ end gem 'refinerycms-i18n', github: 'anitagraham/refinerycms-i18n', branch: 'ruby3' # Add support for refinerycms-acts-as-indexed -gem 'refinerycms-acts-as-indexed', '~> 4.0', '>= 4.0.0', github: 'refinery/refinerycms-acts-as-indexed', branch: 'master' +gem 'refinerycms-acts-as-indexed', '~> 4.0', '>= 4.0.0', + github: 'refinery/refinerycms-acts-as-indexed', + branch: 'master' # Add the default visual editor, for now. gem 'refinerycms-wymeditor', ['~> 3.0', '>= 3.0.0'] diff --git a/images/app/viewobjects/image_object.rb b/images/app/viewobjects/image_object.rb deleted file mode 100644 index fa5cdebe7e9..00000000000 --- a/images/app/viewobjects/image_object.rb +++ /dev/null @@ -1,56 +0,0 @@ -class ImageObject - include Refinery::TranslationHelper - include Refinery::ImageHelper - - attr_reader :image, :context, :i18n_scope - attr_writer :title, :alt, :filename, :translations, :edit_attributes, :delete_attributes, :preview_attributes - delegate_missing_to :image - - Action = Struct.new('Action', :href, :title, :text, :data, :options, keyword_init: true) - - def initialize(image, context, scope=nil) - @image = image - @context = context - @i18n_scope = scope || 'refinery.admin.images' - end - - def title - translated_field(image, :title) - end - - def alt - translated_field(image, :alt) - end - - def img_element - image_fu image, '149x149#c', title: ::I18n.t('edit_title', scope: i18n_scope, title: image.image_title || image.image_alt) - end - - def filename - image.image_name - end - def locales_with_titles - image.translations - .reject { |i| i.image_title.blank? } - .map(&:locale).sort_by { |t| Refinery::I18n.frontend_locales.index(t.to_sym) } - end - - def edit_action(options = {}) - Action.new(href: context.edit_admin_image_path(image, switch_locale: options.delete(:switch_locale)), - title: I18n.t('.edit', scope: i18n_scope), - text: I18n.t('.edit_title', title: image.title, scope: i18n_scope), - options: options) - end - - def preview_action(options = {}) - Action.new(href: image.url, title: I18n.t('.view_live_html', scope: i18n_scope), options: options) - end - - def delete_action(options = {}) - Action.new(href: context.admin_image_path(image), - title: I18n.t('.delete', scope: i18n_scope), - data:{ confirm: I18n.t('message', scope: 'refinery.admin.delete', title: image.title) }, - options: { class: [:delete, 'confirm-delete'], **options }) - end - -end diff --git a/pages/app/views/refinery/admin/pages/_page.html.erb b/pages/app/views/refinery/admin/pages/_page.html.erb index 47eb95a4e34..b77cead926c 100644 --- a/pages/app/views/refinery/admin/pages/_page.html.erb +++ b/pages/app/views/refinery/admin/pages/_page.html.erb @@ -1,16 +1,14 @@ -<% # setup params for various action links - edit_url = refinery.admin_edit_page_path(page.nested_url) - translated_locales = locales_with_translated_field(page, 'title') -%> +<% edit_url = refinery.admin_edit_page_path(page.nested_url) %> +<% translated_locales = locales_with_translated_field(page, 'title') %> -
            • ' id='<%= dom_id(page) %>'> -
              +
            • +
              <%= page_icon(page.children.size) %> <%= link_to translated_field(page, :title), edit_url, class: [:title, :edit], title: t('edit') %> <%= page_meta_information(page) %> <%= edit_in_locales(edit_url, translated_locales) %> - + <%= action_icon(:preview, page.url, t('.view_live_html')) %> <%= action_icon(:add, refinery.new_admin_page_path(parent_id: page.id), t('.new')) %> <%= action_icon(:edit, edit_url, t('.edit')) %> diff --git a/pages/spec/support/selector_helpers.rb b/pages/spec/support/selector_helpers.rb index 2ba883ac83e..fd3a0bf9084 100644 --- a/pages/spec/support/selector_helpers.rb +++ b/pages/spec/support/selector_helpers.rb @@ -1,42 +1,43 @@ - - def index_entry - 'li.record.page' - end - - def index_item(id) - [id, '.item'].join(' ') - end - - # various edit actions selectors: there are several in a single index entry - # 1. the page title is wrapped in an edit link - # page title - # 2. if there are several locales there is an edit_in_locale link for each locale - # - # 3. the actions group has an edit icon with a link to edit the page (equivalent to 1) - # - # - def edit_selector(class_name: nil, locale: nil, slug: nil) - class_selector = [class_name, 'edit'].compact.join('.') - query_selector = locale.nil? ? '' : "?switch_locale=#{locale}" - "a.#{class_selector}[href$='#{slug}/edit#{query_selector}']" - end - - def title_link_selector(slug: '') - edit_selector(slug: slug, class_name: :title) - end - - def icon_link_selector(slug: '') - edit_selector(slug: slug, class_name: :edit_icon) - end - - def locale_link_selector(locale:, slug: '') - edit_selector(slug: slug, class_name: :locale, locale: locale.downcase) - end - - def locales - "span.locales" - end - - def locale_picker(locale) - ".locales ##{locale}" - end +# frozen_string_literal: true + +def index_entry + 'li.record.page' +end + +def index_item(id) + [id, '.item'].join(' ') +end + +# various edit actions selectors: there are several in a single index entry +# 1. the page title is wrapped in an edit link +# page title +# 2. if there are several locales there is an edit_in_locale link for each locale +# +# 3. the actions group has an edit icon with a link to edit the page (equivalent to 1) +# +# +def edit_selector(class_name: nil, locale: nil, slug: nil) + class_selector = [class_name, 'edit'].compact.join('.') + query_selector = ("?switch_locale=#{locale}" if locale.present?) + "a.#{class_selector}[href$='#{slug}/edit#{query_selector}']" +end + +def title_link_selector(slug: '') + edit_selector(slug: slug, class_name: :title) +end + +def icon_link_selector(slug: '') + edit_selector(slug: slug, class_name: :edit_icon) +end + +def locale_link_selector(locale:, slug: '') + edit_selector(slug: slug, class_name: :locale, locale: locale.downcase) +end + +def locales + "span.locales" +end + +def locale_picker(locale) + ".locales ##{locale}" +end From b0a22acdc7228204e58adcf2d700a5817c62f7dc Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Fri, 2 Jan 2026 10:47:20 +1300 Subject: [PATCH 63/74] bring back image grid --- .../refinery/sections/_layout.scss | 65 ++++++++++++++----- core/app/helpers/refinery/action_helper.rb | 28 ++++---- .../views/refinery/admin/pages/_page.html.erb | 2 +- 3 files changed, 68 insertions(+), 27 deletions(-) diff --git a/core/app/assets/stylesheets/refinery/sections/_layout.scss b/core/app/assets/stylesheets/refinery/sections/_layout.scss index 819facea2c4..017b2762b64 100644 --- a/core/app/assets/stylesheets/refinery/sections/_layout.scss +++ b/core/app/assets/stylesheets/refinery/sections/_layout.scss @@ -917,6 +917,11 @@ label.stripped { @include icon('spinner'); } +#image_grid .actions a img { + padding: 4px 4px 1px 4px; + vertical-align: top; +} + .actions a.reorder_anchor:hover { background: inherit !important; cursor: move; @@ -1399,24 +1404,54 @@ body.dialog #content .search_form { padding: 12px; } -#records.images { - #image_index { - .image_group.grid { - @include imageGridLayout; +ul#image_grid, .pagination_container > ul#image_grid { + width: 100%; + padding: 0px; + margin: 10px 0px 15px 0px; +} - li { - @include gridImage - } - } +ul#image_grid li, .pagination_container > ul#image_grid li { + position: relative; + float: left; + margin: 0px 12px 12px 0px; + padding: 0px; + width: 149px; + height: 186px; + max-width: 149px; + max-height: 186px; + text-align: center; + overflow: hidden; +} - .image_group.list { - @include imageListLayout; +ul#image_grid li.image_3 { + margin-left: 0px; + margin-right: 0px; +} - li { - @include listImage - } - } - } +#records ul#image_grid li .actions { + top: auto; + bottom: 0px; +} + +#records ul#image_grid li .actions a { + line-height: 24px; +} + +ul#image_grid li.row-end { + margin-right: 0px; + float: right; +} + +ul#image_grid li a { + border: 0 none; +} + +ul#image_grid li p { + margin: 3px 0; +} + +ul#image_grid li span.actions { + width: 100%; } ul.checkboxes { diff --git a/core/app/helpers/refinery/action_helper.rb b/core/app/helpers/refinery/action_helper.rb index 734ac5ed625..c5bd386a40d 100644 --- a/core/app/helpers/refinery/action_helper.rb +++ b/core/app/helpers/refinery/action_helper.rb @@ -34,33 +34,39 @@ def action_icon_label(action, url, title, options={}, label = true) def edit_in_current_locale(url:, title:, **options) - action_icon(:edit, url, title, class: :edit, **options ) + action_icon(:edit, url, title, class: :edit, **options) end def locale_language(locale) Refinery::I18n.locales[locale] end - def edit_in_locale(locale, url:, title:, **options) + def edit_in_locale(locale, url:, title: nil, **options) if options.delete(:label) - title = locale.to_s.upcase - action_label(:locale, "#{url}?switch_locale=#{locale}", title, **options, - id: locale, class: :edit, **options + action_label( + :locale, "#{url}?switch_locale=#{locale}", + title || locale.to_s.upcase, + **options, + id: locale, + class: :edit, **options ) else - title = ::I18n.t('.edit_in_language', language: locale_language(locale), scope: 'refinery.admin.locale_picker') - action_icon(:locale, "#{url}?switch_locale=#{locale}", title, - id: locale, class: :edit, **options + action_icon( + :locale, "#{url}?switch_locale=#{locale}", + title || ::I18n.t('.edit_in_language', language: locale_language(locale), scope: 'refinery.admin.locale_picker'), + **options, + id: locale, + class: :edit ) end end - def edit_in_locales(edit_url, locales=[]) - return if locales.empty? + def edit_in_locales(edit_url, locales = []) + return if locales.empty? || locales.one? edit_links = locales.map do |locale| - edit_in_locale(locale, url: edit_url, title: t('.edit_in_locale', locale: locale) ) + edit_in_locale(locale, url: edit_url, title: t('.edit_in_locale', locale: locale)) end tag.span edit_links.compact.join(' ').html_safe, class: :locales end diff --git a/pages/app/views/refinery/admin/pages/_page.html.erb b/pages/app/views/refinery/admin/pages/_page.html.erb index b77cead926c..c61d1c182a2 100644 --- a/pages/app/views/refinery/admin/pages/_page.html.erb +++ b/pages/app/views/refinery/admin/pages/_page.html.erb @@ -6,7 +6,7 @@ <%= page_icon(page.children.size) %> <%= link_to translated_field(page, :title), edit_url, class: [:title, :edit], title: t('edit') %> <%= page_meta_information(page) %> - <%= edit_in_locales(edit_url, translated_locales) %> + <% edit_in_locales(edit_url, translated_locales) %> <%= action_icon(:preview, page.url, t('.view_live_html')) %> From 4d38710b3e31543134ea201cc7a646e65cb9dfa5 Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Fri, 2 Jan 2026 11:59:21 +1300 Subject: [PATCH 64/74] Fixups to have new things and old things still working --- .../stylesheets/refinery/mixins/_labels.scss | 17 --------- .../assets/stylesheets/refinery/refinery.scss | 1 - .../refinery/sections/_layout.scss | 33 ++++++++-------- core/app/helpers/refinery/action_helper.rb | 9 +++-- core/app/helpers/refinery/site_bar_helper.rb | 11 ++++-- core/app/helpers/refinery/tag_helper.rb | 5 ++- .../helpers/refinery/translation_helper.rb | 10 ++--- .../refinery/admin/_locale_picker.html.erb | 4 +- core/config/locales/en.yml | 2 +- core/spec/system/refinery/site_bar_spec.rb | 6 ++- .../refinery/admin/images/_form.html.erb | 26 ++++++------- .../helpers/refinery/admin/pages_helper.rb | 7 ++-- .../views/refinery/admin/pages/_page.html.erb | 5 +-- pages/config/locales/en.yml | 6 +-- .../spec/system/refinery/admin/pages_spec.rb | 38 +++++++++---------- .../system/refinery/admin/resources_spec.rb | 4 +- 16 files changed, 81 insertions(+), 103 deletions(-) delete mode 100644 core/app/assets/stylesheets/refinery/mixins/_labels.scss diff --git a/core/app/assets/stylesheets/refinery/mixins/_labels.scss b/core/app/assets/stylesheets/refinery/mixins/_labels.scss deleted file mode 100644 index a77c95504ce..00000000000 --- a/core/app/assets/stylesheets/refinery/mixins/_labels.scss +++ /dev/null @@ -1,17 +0,0 @@ -@mixin label_with_help { - margin-bottom: 5px; - font-weight: bold; - margin-top: 20px; - display: block; - vertical-align: middle; - - > label, > .help, { - display: inline; - vertical-align: middle; - } - > .help { - font-weight: normal; - font-style: italic; - margin-left: 3px; - } -} diff --git a/core/app/assets/stylesheets/refinery/refinery.scss b/core/app/assets/stylesheets/refinery/refinery.scss index 3bc81910f39..ce57fdf8d4e 100644 --- a/core/app/assets/stylesheets/refinery/refinery.scss +++ b/core/app/assets/stylesheets/refinery/refinery.scss @@ -8,7 +8,6 @@ @import 'mixins/icon_base'; @import 'mixins/icon'; @import 'mixins/rounded'; -@import 'mixins/labels'; @import 'mixins/locales'; @import 'mixins/images'; diff --git a/core/app/assets/stylesheets/refinery/sections/_layout.scss b/core/app/assets/stylesheets/refinery/sections/_layout.scss index 017b2762b64..18ec49bbaf5 100644 --- a/core/app/assets/stylesheets/refinery/sections/_layout.scss +++ b/core/app/assets/stylesheets/refinery/sections/_layout.scss @@ -742,9 +742,7 @@ textarea { width: auto; height: 25px; } - -//label, .label_with_help { -label { +label, .label_with_help { margin-bottom: 5px; font-weight: bold; margin-top: 20px; @@ -1631,21 +1629,20 @@ input.button.close_dialog:active, a.button.close_dialog:active, #content a.butto } .label_with_help { - @include label_with_help -} - -//.label_with_help label, .label_with_help span.help, .label_with_help img.help { -// display: inline; -//} -//.label_with_help img.help { -// vertical-align: middle; -//} -//.label_with_help span.help { -// font-weight: normal; -// margin-left: 3px; -// border-bottom: 1px dotted #727272; -// cursor: help; -//} + vertical-align: middle; +} +.label_with_help label, .label_with_help span.help, .label_with_help img.help { + display: inline; +} +.label_with_help img.help { + vertical-align: middle; +} +.label_with_help span.help { + font-weight: normal; + margin-left: 3px; + border-bottom: 1px dotted #727272; + cursor: help; +} #more_options_field { position: relative; } diff --git a/core/app/helpers/refinery/action_helper.rb b/core/app/helpers/refinery/action_helper.rb index c5bd386a40d..2c24ce67480 100644 --- a/core/app/helpers/refinery/action_helper.rb +++ b/core/app/helpers/refinery/action_helper.rb @@ -61,13 +61,14 @@ def edit_in_locale(locale, url:, title: nil, **options) end end - - def edit_in_locales(edit_url, locales = []) - return if locales.empty? || locales.one? + def edit_in_locales(edit_url, locales = [], i18n_scope: %i[refinery admin locale_picker]) + return if locales.empty? edit_links = locales.map do |locale| - edit_in_locale(locale, url: edit_url, title: t('.edit_in_locale', locale: locale)) + language = ::Refinery::I18n.config.locales.fetch(locale, locale) + edit_in_locale(locale, url: edit_url, title: t('edit_in_language', language: language, scope: i18n_scope)) end + tag.span edit_links.compact.join(' ').html_safe, class: :locales end end diff --git a/core/app/helpers/refinery/site_bar_helper.rb b/core/app/helpers/refinery/site_bar_helper.rb index 96d03390779..46d4eb3e154 100644 --- a/core/app/helpers/refinery/site_bar_helper.rb +++ b/core/app/helpers/refinery/site_bar_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Refinery module SiteBarHelper @@ -5,7 +7,7 @@ module SiteBarHelper def site_bar_switch_link link_to_if(admin?, t('.switch_to_your_website', **site_bar_translate_locale_args), refinery.root_path(site_bar_translate_locale_args), - 'data-turbolinks' => false) do + data: {turbolinks: false}) do link_to t('.switch_to_your_website_editor', **site_bar_translate_locale_args), Refinery::Core.backend_path, 'data-turbolinks' => false end @@ -13,10 +15,11 @@ def site_bar_switch_link def site_bar_edit_link return nil if admin? || @page.nil? - link_to t('refinery.admin.pages.edit', **site_bar_translate_locale_args), + + link_to t('refinery.admin.pages.page.edit', **site_bar_translate_locale_args), refinery.admin_edit_page_path(@page.nested_url, - :switch_locale => (@page.translations.first.locale unless @page.translated_to_default_locale?)), - 'data-turbolinks' => false + switch_locale: (@page.translations.first.locale unless @page.translated_to_default_locale?)), + data: {turbolinks: false} end def site_bar_translate_locale_args diff --git a/core/app/helpers/refinery/tag_helper.rb b/core/app/helpers/refinery/tag_helper.rb index 92ed89f5778..3a5e82676db 100644 --- a/core/app/helpers/refinery/tag_helper.rb +++ b/core/app/helpers/refinery/tag_helper.rb @@ -1,12 +1,13 @@ module Refinery module TagHelper + include ActionHelper # Returns # Remember to wrap your block with if you're using a label next to the help tag. def refinery_help_tag(title='Tip') title = title.html_safe? ? title : h(title) - tag.span title, class: :help + action_icon(:info, '#', title, {tooltip: title}) end # This is just a quick wrapper to render an image tag that lives inside refinery/icons. @@ -16,7 +17,7 @@ def refinery_icon_tag(filename, options = {}) filename = "#{filename}.png" unless filename.split('.').many? path = image_path "refinery/icons/#{filename}", skip_pipeline: true - image_tag path, {:width => 16, :height => 16}.merge(options) + image_tag path, {width: 16, height: 16}.merge(options) end end diff --git a/core/app/helpers/refinery/translation_helper.rb b/core/app/helpers/refinery/translation_helper.rb index e5e3af598ba..f44e7c68f82 100644 --- a/core/app/helpers/refinery/translation_helper.rb +++ b/core/app/helpers/refinery/translation_helper.rb @@ -18,12 +18,12 @@ def translated_field(record, field) def locales_with_translated_field(record, field_name, include_current: true) field_name = field_name.to_sym translations = record.translations.where.not(field_name => [nil, ""]) - translations = translations.where.not(locale: current_locale) unless include_current - translations.map { |t| t.locale.to_sym }.sort_by { |locale| Refinery::I18n.frontend_locales.index(locale) } - end + translations = translations.where.not(locale: Refinery::I18n.default_frontend_locale.to_s) unless include_current - private def current_locale - Refinery::I18n.current_locale.to_s + translations.pluck(:locale).map(&:to_sym).sort_by do |locale| + index = Refinery::I18n.frontend_locales.index(locale) + index ? [0, index] : [1, locale] + end end end end diff --git a/core/app/views/refinery/admin/_locale_picker.html.erb b/core/app/views/refinery/admin/_locale_picker.html.erb index 3adb102aa86..7738a921af6 100644 --- a/core/app/views/refinery/admin/_locale_picker.html.erb +++ b/core/app/views/refinery/admin/_locale_picker.html.erb @@ -1,7 +1,7 @@ - + <% if Refinery::I18n.frontend_locales.many? %> -
              +
              <% Refinery::I18n.frontend_locales.each do |locale, language| %> <% classes = locale.to_s == local_assigns[:current_locale].to_s ? 'selected' : '' %> <%= edit_in_locale(locale, diff --git a/core/config/locales/en.yml b/core/config/locales/en.yml index ca8bea18532..9fc630ec862 100644 --- a/core/config/locales/en.yml +++ b/core/config/locales/en.yml @@ -44,7 +44,7 @@ en: show: Show locale_picker: language: Language, - edit_in_locale: Edit in %{locale} + edit_in_locale: Edit in '%{locale_code}' edit_in_language: Edit in %{language} resource_picker: download_current: Download current file diff --git a/core/spec/system/refinery/site_bar_spec.rb b/core/spec/system/refinery/site_bar_spec.rb index 0d05f6ec00e..a18a7a7cc11 100644 --- a/core/spec/system/refinery/site_bar_spec.rb +++ b/core/spec/system/refinery/site_bar_spec.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + require "spec_helper" module Refinery - describe "site bar", :type => :system do + describe "site bar", type: :system do refinery_login describe "logout link" do @@ -62,7 +64,7 @@ module Refinery end it "has an 'edit this page' button" do - expect(page).to have_link("Edit this page", :href => refinery.edit_admin_page_path(root_page)) + expect(page).to have_link("Edit this page", href: refinery.edit_admin_page_path(root_page)) end end diff --git a/images/app/views/refinery/admin/images/_form.html.erb b/images/app/views/refinery/admin/images/_form.html.erb index bdcdf4dca55..7e76ce47c95 100644 --- a/images/app/views/refinery/admin/images/_form.html.erb +++ b/images/app/views/refinery/admin/images/_form.html.erb @@ -17,7 +17,7 @@ <%= f.file_field :image %>

              <% else %> - <% # we must only hint at multiple when it's a new record otherwise update fails. %> + <%# we must only hint at multiple when it's a new record otherwise update fails. %> <%= f.file_field :image, multiple: true %> <% end %>
              @@ -26,7 +26,7 @@
              - + <%= f.label :image_title, t('.image_title') %> <%= refinery_help_tag t('.image_title_help') %> @@ -34,7 +34,7 @@
              - + <%= f.label :image_alt, t('.image_alt') %> <%= refinery_help_tag t('.image_alt_help') %> @@ -51,12 +51,12 @@ cancel_url: refinery.admin_images_path -%> <% if @app_dialog %> - - - - - - + + + + + + <% end %> <% end %> @@ -79,13 +79,13 @@ class: 'crop_image' } do |f| %>
              - + <%= f.label :ratio, t('.ratios') %> <%= refinery_help_tag t('.choose_crop') %> <% ::Refinery::Images.user_image_ratios.each do |ratio, value| %> - <%= f.button ratio, type: 'button', data: { value: value, ratio: ratio.to_s.parameterize } %> + <%= f.button value, type: 'button', data: { value: value, ratio: ratio.to_s.parameterize } %> <% end %> <%= f.submit t('.save'), id: 'save_crop' %> @@ -101,7 +101,7 @@ <%= render partial: 'crop', collection: @image.crops %>
            <% else %> -

            <%=t('.no_crops_yet')%>

            +

            <%= t('.no_crops_yet') %>

            <% end %>
            @@ -113,4 +113,4 @@ <% end %> <% content_for :stylesheets, stylesheet_link_tag('cropper') -%> -<% end %> \ No newline at end of file +<% end %> diff --git a/pages/app/helpers/refinery/admin/pages_helper.rb b/pages/app/helpers/refinery/admin/pages_helper.rb index 48eec95ebf8..ece7c7a8fff 100644 --- a/pages/app/helpers/refinery/admin/pages_helper.rb +++ b/pages/app/helpers/refinery/admin/pages_helper.rb @@ -61,14 +61,13 @@ def page_icon(number_of_children) # .toggle scss handles adding icons to pages with children classes = ['icon'] - case - when number_of_children.zero? + if number_of_children.zero? classes.push icon_class('page') - title = ::I18n.t('edit', scope: 'refinery.admin.pages') + title = ::I18n.t('edit', scope: 'refinery.admin.pages.page') else expanded_class = Refinery::Pages.auto_expand_admin_tree ? 'expanded' : '' classes.push 'toggle', expanded_class - title = ::I18n.t('expand_collapse', scope: 'refinery.admin.pages') + title = ::I18n.t('expand_collapse', scope: 'refinery.admin.pages') end tag.span(class: classes.join(' '), title: title) diff --git a/pages/app/views/refinery/admin/pages/_page.html.erb b/pages/app/views/refinery/admin/pages/_page.html.erb index c61d1c182a2..d664066c1f4 100644 --- a/pages/app/views/refinery/admin/pages/_page.html.erb +++ b/pages/app/views/refinery/admin/pages/_page.html.erb @@ -1,12 +1,11 @@ <% edit_url = refinery.admin_edit_page_path(page.nested_url) %> -<% translated_locales = locales_with_translated_field(page, 'title') %>
          • <%= page_icon(page.children.size) %> - <%= link_to translated_field(page, :title), edit_url, class: [:title, :edit], title: t('edit') %> + <%= link_to translated_field(page, :title), edit_url, class: [:title, :edit], title: t('.edit') %> <%= page_meta_information(page) %> - <% edit_in_locales(edit_url, translated_locales) %> + <%= edit_in_locales(edit_url, locales_with_translated_field(page, 'title', include_current: false)) %> <%= action_icon(:preview, page.url, t('.view_live_html')) %> diff --git a/pages/config/locales/en.yml b/pages/config/locales/en.yml index a61bee750c8..47b01f635e6 100644 --- a/pages/config/locales/en.yml +++ b/pages/config/locales/en.yml @@ -32,7 +32,7 @@ en: tab_name: Your file link_to_this_resource: Link to this file pages: - edit: Edit this page + expand_collapse: Expand or collapse sub pages page: draft: draft hidden: hidden @@ -40,11 +40,7 @@ en: skip_to_first_child: Skip to first child delete: Remove this page forever edit: Edit this page - edit_locale: Edit this page in %{language} - edit_title: Edit "%{title}" - edit_title_locale: Edit "%{title}" in %{language} new: Add a new child page - expand_collapse: Expand or collapse sub pages view_live_html: View this page live
            (opens in a new window) form: preview: Preview diff --git a/pages/spec/system/refinery/admin/pages_spec.rb b/pages/spec/system/refinery/admin/pages_spec.rb index 16f4ac1bd36..144b44cec2d 100644 --- a/pages/spec/system/refinery/admin/pages_spec.rb +++ b/pages/spec/system/refinery/admin/pages_spec.rb @@ -491,23 +491,21 @@ module Admin expect(Refinery::Page.count).to eq(2) end - it "shows locale for page" do - p = ::Refinery::Page.by_slug('news').first - within "#page_#{p.id} .locales" do - expect(page).to have_selector(locale_link_selector(locale: 'en')) + it "doesn't show locale for default frontend locale" do + within "#page_#{::Refinery::Page.by_slug('news').first.id}" do + expect(page).not_to have_selector(".locales") + expect(page).not_to have_selector(locale_link_selector(locale: 'en')) end end it "shows title in the admin menu" do - p = ::Refinery::Page.by_slug('news').first - within "#page_#{p.id}" do + within "#page_#{::Refinery::Page.by_slug('news').first.id}" do expect(page).to have_content('News') expect(page.first(title_link_selector)[:text]).to have_content('News') end end it "shows in frontend menu for 'en' locale" do - # page.driver.debug visit "/" within "#menu" do @@ -532,15 +530,12 @@ module Admin let(:ru_page_title) { 'Новости' } let(:ru_page_slug_encoded) { '%D0%BD%D0%BE%D0%B2%D0%BE%D1%81%D1%82%D0%B8' } let!(:news_page) do - _page = Mobility.with_locale(:en) { - Page.create title: en_page_title - } - Mobility.with_locale(:ru) do - _page.title = ru_page_title - _page.save + Mobility.with_locale(:en) { Page.create(title: en_page_title) }.tap do |localised_page| + Mobility.with_locale(:ru) do + localised_page.title = ru_page_title + localised_page.save + end end - - _page end it "can have a title for each locale" do @@ -566,13 +561,16 @@ module Admin expect(Refinery::Page.count).to eq(2) end - it "is shown with both locales in the index" do + it "is shown with only the non-default locale in the index" do visit refinery.admin_pages_path - within "#page_#{news_page.id} .locales" do - # expect(page).to have_selector(edit_in_locale, count: 2) - expect(page).to have_selector(locale_link_selector(locale: 'en')) - expect(page).to have_selector(locale_link_selector(locale: 'ru')) + within "#page_#{news_page.id}" do + expect(page).to have_selector(".locales") + + within ".locales" do + expect(page).to have_selector(locale_link_selector(locale: 'ru')) + expect(page).to have_no_selector(locale_link_selector(locale: 'en')) + end end end diff --git a/resources/spec/system/refinery/admin/resources_spec.rb b/resources/spec/system/refinery/admin/resources_spec.rb index 7d8dacc4ca7..b27a3f0d55a 100644 --- a/resources/spec/system/refinery/admin/resources_spec.rb +++ b/resources/spec/system/refinery/admin/resources_spec.rb @@ -75,7 +75,7 @@ def uploading_a_file click_link 'Upload new file' within '#file' do - expect(page).to have_text "The maximum file size is 1.2 KB." + expect(page).to have_selector('a[tooltip="The maximum file size is 1.2 KB."]') end end end @@ -90,7 +90,7 @@ def uploading_a_file click_link 'Tilføj en ny fil' within '#file' do - expect(page).to have_text "Filen må maksimalt fylde 1,2 kB." + expect(page).to have_selector('a[tooltip="Filen må maksimalt fylde 1,2 kB."]') end end end From d0d6e683e3258eb4d518445cfdf591cfc02da8ec Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Fri, 2 Jan 2026 12:33:39 +1300 Subject: [PATCH 65/74] Test all the ruby/rails versions we can Try to support the older versions --- .github/workflows/legacy.yml | 63 +++++++++++++++++++ .github/workflows/main.yml | 4 +- Gemfile | 13 +++- Rakefile | 2 + .../refinery/dummy/dummy_generator.rb | 2 + .../dummy/templates/rails/application.rb.erb | 2 +- .../dummy/templates/rails/boot.rb.erb | 1 + .../dummy/templates/rails/database.yml | 61 +++++++++++++++--- .../dummy/templates/rails/manifest.js | 3 + .../dummy/templates/rails/storage.yml | 7 +++ core/refinerycms-core.gemspec | 2 +- .../helpers/refinery/admin/pages_helper.rb | 6 +- pages/refinerycms-pages.gemspec | 2 +- refinerycms.gemspec | 4 +- 14 files changed, 154 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/legacy.yml create mode 100644 core/lib/generators/refinery/dummy/templates/rails/manifest.js create mode 100644 core/lib/generators/refinery/dummy/templates/rails/storage.yml diff --git a/.github/workflows/legacy.yml b/.github/workflows/legacy.yml new file mode 100644 index 00000000000..997b9f6a167 --- /dev/null +++ b/.github/workflows/legacy.yml @@ -0,0 +1,63 @@ +name: Legacy + +on: + push: + branches: + - main + pull_request: + +jobs: + test: + strategy: + matrix: + database: [ sqlite3, mysql, postgresql ] + extension: [ core, dragonfly, images, pages, resources ] + ruby: [ 3.2, 3.3, 3.4, 4.0 ] + rails: [ '~> 6.1.0', '~> 7.0.0', '~> 7.1.0' ] + exclude: + - ruby: 3.4 + rails: '~> 6.1.0' + - ruby: 4.0 + rails: '~> 6.1.0' + fail-fast: false + runs-on: ubuntu-latest + + env: + CI: true + DB: ${{ matrix.database }} + MYSQL_PASSWORD: root + PGHOST: localhost + PGPASSWORD: runner + PGUSER: runner + RAILS_ENV: test + RAILS_VERSION: ${{ matrix.rails }} + RETRY_COUNT: 3 + + name: rails-${{ matrix.rails }} ruby-${{ matrix.ruby }} refinerycms-${{ matrix.extension }} ${{ matrix.database }} + steps: + - run: sudo apt-get update && sudo apt-get install libsqlite3-dev -y + - uses: actions/checkout@v6 + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + ruby-version: ${{ matrix.ruby }} + + - name: "Install ImageMagick for images extension" + if: ${{ matrix.extension == 'images' }} + run: sudo apt-get install imagemagick -y + + - name: "Set up MySQL using VM's server" + if: ${{ env.DB == 'mysql' }} + run: | + sudo apt-get install libmysqlclient-dev -y + sudo systemctl start mysql.service + + - name: "Set up PostgreSQL using VM's server" + if: ${{ env.DB == 'postgresql' }} + run: | + sudo apt-get install libpq-dev -y + sudo systemctl start postgresql.service + sudo -u postgres psql -c "CREATE USER runner WITH SUPERUSER PASSWORD 'runner'" + + - run: bin/rake refinery:testing:dummy_app + - run: bin/rspec ${{ matrix.extension }}/spec diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2092a3fe75d..da5d9a35043 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,6 +13,7 @@ jobs: database: [ sqlite3, mysql, postgresql ] extension: [ core, dragonfly, images, pages, resources ] ruby: [ 3.2, 3.3, 3.4, 4.0 ] + rails: [ '~> 7.2.0', '~> 8.0.0', '~> 8.1.0' ] fail-fast: false runs-on: ubuntu-latest @@ -24,9 +25,10 @@ jobs: PGPASSWORD: runner PGUSER: runner RAILS_ENV: test + RAILS_VERSION: ${{ matrix.rails }} RETRY_COUNT: 3 - name: ${{ matrix.extension }} ${{ matrix.ruby }} ${{ matrix.database }} + name: rails-${{ matrix.rails }} ruby-${{ matrix.ruby }} refinerycms-${{ matrix.extension }} ${{ matrix.database }} steps: - run: sudo apt-get update && sudo apt-get install libsqlite3-dev -y - uses: actions/checkout@v6 diff --git a/Gemfile b/Gemfile index 5305f4dd387..5022415cef5 100644 --- a/Gemfile +++ b/Gemfile @@ -4,6 +4,12 @@ source 'https://rubygems.org' gemspec +# Allow CI and us to test specific Rails versions +gem 'rails', ENV['RAILS_VERSION'] if ENV['RAILS_VERSION'] + +# Ruby 3.4+ extracted mutex_m from stdlib, but older Rails versions need it +gem 'mutex_m' if RUBY_VERSION >= '3.4' && ENV['RAILS_VERSION']&.match?(/^~> [67]\./) + gem 'net-imap', require: false gem 'net-pop', require: false gem 'net-smtp', require: false @@ -32,7 +38,12 @@ gem 'decorators', github: 'parndt/decorators', branch: 'zeitwerk' # Database Configuration if !ENV['CI'] || ENV['DB'] == 'sqlite3' gem 'activerecord-jdbcsqlite3-adapter', '>= 1.3.0.rc1', platform: :jruby - gem 'sqlite3', platform: :ruby + # Rails 6.1 and 7.0 require sqlite3 ~> 1.4, Rails 7.1+ can use newer versions + if ENV['RAILS_VERSION']&.match?(/[67]\.[01]/) + gem 'sqlite3', '~> 1.4.0', platform: :ruby + else + gem 'sqlite3', platform: :ruby + end end if !ENV['CI'] || ENV['DB'] == 'mysql' diff --git a/Rakefile b/Rakefile index e294ee1fc12..b7f03836981 100644 --- a/Rakefile +++ b/Rakefile @@ -1,4 +1,6 @@ #!/usr/bin/env rake +require 'logger' + begin require 'bundler/setup' rescue LoadError diff --git a/core/lib/generators/refinery/dummy/dummy_generator.rb b/core/lib/generators/refinery/dummy/dummy_generator.rb index 676d1240054..805ba3ba11b 100644 --- a/core/lib/generators/refinery/dummy/dummy_generator.rb +++ b/core/lib/generators/refinery/dummy/dummy_generator.rb @@ -55,8 +55,10 @@ def test_dummy_config template "rails/boot.rb.erb", "#{dummy_path}/config/boot.rb", force: true template "rails/application.rb.erb", "#{dummy_path}/config/application.rb", force: true template "rails/routes.rb", "#{dummy_path}/config/routes.rb", force: true + template "rails/storage.yml", "#{dummy_path}/config/storage.yml", force: true template "rails/Rakefile", "#{dummy_path}/Rakefile", force: true template "rails/application.js", "#{dummy_path}/app/assets/javascripts/application.js", force: true + template "rails/manifest.js", "#{dummy_path}/app/assets/config/manifest.js", force: true template 'rails/blank.png', "#{dummy_path}/public/apple-touch-icon.png", force: true template 'rails/blank.png', "#{dummy_path}/public/apple-touch-icon-precomposed.png", force: true end diff --git a/core/lib/generators/refinery/dummy/templates/rails/application.rb.erb b/core/lib/generators/refinery/dummy/templates/rails/application.rb.erb index 442700b0b4c..647139b94c3 100644 --- a/core/lib/generators/refinery/dummy/templates/rails/application.rb.erb +++ b/core/lib/generators/refinery/dummy/templates/rails/application.rb.erb @@ -10,7 +10,7 @@ Bundler.require(*Rails.groups(assets: %w(development test))) module Dummy class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 7.0 + config.load_defaults 6.1 # Settings in config/environments/* take precedence over those specified here. # Application configuration can go into files in config/initializers diff --git a/core/lib/generators/refinery/dummy/templates/rails/boot.rb.erb b/core/lib/generators/refinery/dummy/templates/rails/boot.rb.erb index c61f04db8c2..4fc50e85117 100644 --- a/core/lib/generators/refinery/dummy/templates/rails/boot.rb.erb +++ b/core/lib/generators/refinery/dummy/templates/rails/boot.rb.erb @@ -1,4 +1,5 @@ require 'rubygems' +require 'logger' gemfile = File.expand_path("<%= gemfile_path %>", __FILE__) ENV['BUNDLE_GEMFILE'] = gemfile diff --git a/core/lib/generators/refinery/dummy/templates/rails/database.yml b/core/lib/generators/refinery/dummy/templates/rails/database.yml index 224fb9d7de6..5323f9b56e7 100644 --- a/core/lib/generators/refinery/dummy/templates/rails/database.yml +++ b/core/lib/generators/refinery/dummy/templates/rails/database.yml @@ -1,34 +1,75 @@ -login: &login <% if /mysql$/ === @database %> +development: adapter: mysql<%= '2' unless /jdbc/ === @database %> - encoding: utf8 + encoding: utf8mb4 reconnect: false pool: 5 username: root password: <%%= ENV['MYSQL_PASSWORD'] %> - #socket: /tmp/mysql.sock + database: dummy_dev + +test: + adapter: mysql<%= '2' unless /jdbc/ === @database %> + encoding: utf8mb4 + reconnect: false + pool: 5 + username: root + password: <%%= ENV['MYSQL_PASSWORD'] %> + database: dummy_test + +production: + adapter: mysql<%= '2' unless /jdbc/ === @database %> + encoding: utf8mb4 + reconnect: false + pool: 5 + username: root + password: <%%= ENV['MYSQL_PASSWORD'] %> + database: dummy_prod + <% elsif /postgresql/ === @database %> +development: adapter: postgresql encoding: unicode - database: refinery_database_development pool: 5 username: <%%= ENV.fetch('PGUSER', 'postgres') %> password: <%%= ENV.fetch('PGPASSWORD', 'postgres') %> min_messages: warning + database: dummy_dev + +test: + adapter: postgresql + encoding: unicode + pool: 5 + username: <%%= ENV.fetch('PGUSER', 'postgres') %> + password: <%%= ENV.fetch('PGPASSWORD', 'postgres') %> + min_messages: warning + database: dummy_test + +production: + adapter: postgresql + encoding: unicode + pool: 5 + username: <%%= ENV.fetch('PGUSER', 'postgres') %> + password: <%%= ENV.fetch('PGPASSWORD', 'postgres') %> + min_messages: warning + database: dummy_prod + <% else %> +development: adapter: sqlite3 pool: 5 timeout: 5000 -<% end %> - -development: - <<: *login database: dummy_dev test: - <<: *login + adapter: sqlite3 + pool: 5 + timeout: 5000 database: dummy_test production: - <<: *login + adapter: sqlite3 + pool: 5 + timeout: 5000 database: dummy_prod +<% end %> \ No newline at end of file diff --git a/core/lib/generators/refinery/dummy/templates/rails/manifest.js b/core/lib/generators/refinery/dummy/templates/rails/manifest.js new file mode 100644 index 00000000000..5cc2c089409 --- /dev/null +++ b/core/lib/generators/refinery/dummy/templates/rails/manifest.js @@ -0,0 +1,3 @@ +//= link_tree ../images +//= link_directory ../javascripts .js +//= link_directory ../stylesheets .css \ No newline at end of file diff --git a/core/lib/generators/refinery/dummy/templates/rails/storage.yml b/core/lib/generators/refinery/dummy/templates/rails/storage.yml new file mode 100644 index 00000000000..8fdb99ce16b --- /dev/null +++ b/core/lib/generators/refinery/dummy/templates/rails/storage.yml @@ -0,0 +1,7 @@ +test: + service: Disk + root: <%%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%%= Rails.root.join("storage") %> \ No newline at end of file diff --git a/core/refinerycms-core.gemspec b/core/refinerycms-core.gemspec index eae45993546..964acd6a2bf 100644 --- a/core/refinerycms-core.gemspec +++ b/core/refinerycms-core.gemspec @@ -3,7 +3,7 @@ require File.expand_path('../core/lib/refinery/version', __dir__) version = Refinery::Version.to_s -rails_version = ['>= 6.0.0', '< 8'] +rails_version = ['>= 6.1.0', '< 9'] Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY diff --git a/pages/app/helpers/refinery/admin/pages_helper.rb b/pages/app/helpers/refinery/admin/pages_helper.rb index ece7c7a8fff..5e1bb50ca97 100644 --- a/pages/app/helpers/refinery/admin/pages_helper.rb +++ b/pages/app/helpers/refinery/admin/pages_helper.rb @@ -7,7 +7,11 @@ def parent_id_nested_set_options(current_page) # page.title needs the :translations association, doing something like # nested_set_options(::Refinery::Page.includes(:translations), page) doesn't work, yet. # See https://github.com/collectiveidea/awesome_nested_set/pull/123 - ActiveRecord::Associations::Preloader.new(records: pages, associations: :translations) + if Rails::VERSION::MAJOR >= 7 + ActiveRecord::Associations::Preloader.new(records: pages, associations: :translations).call + else + ActiveRecord::Associations::Preloader.new.preload(pages, :translations) + end pages.map { |page| ["#{'-' * page.level} #{page.title}", page.id] } end diff --git a/pages/refinerycms-pages.gemspec b/pages/refinerycms-pages.gemspec index f61c19ffabe..5f7980ff801 100644 --- a/pages/refinerycms-pages.gemspec +++ b/pages/refinerycms-pages.gemspec @@ -23,7 +23,7 @@ Gem::Specification.new do |s| s.add_dependency 'babosa', '~> 1.0' s.add_dependency 'diffy', '~> 3.1', '>= 3.1.0' s.add_dependency 'friendly_id', '>= 5.4.0' - s.add_dependency 'friendly_id-mobility', '~> 1.0.3' + s.add_dependency 'friendly_id-mobility', '~> 1.0.4' s.add_dependency 'refinerycms-core', version s.add_dependency 'seo_meta', '~> 3.0', '>= 3.0.0' s.add_dependency 'speakingurl-rails', '~> 8.0', '>= 8.0.0' diff --git a/refinerycms.gemspec b/refinerycms.gemspec index 3418b4f8f27..5642c52095e 100644 --- a/refinerycms.gemspec +++ b/refinerycms.gemspec @@ -8,8 +8,8 @@ Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY s.name = 'refinerycms' s.version = version - s.description = "A CMS for Ruby on Rails, supporting Rails 6+. It's developer friendly and easy to extend." - s.summary = 'A CMS for Ruby on Rails, supporting Rails 6+' + s.description = "A CMS for Ruby on Rails, supporting Rails 6.1+. It's developer friendly and easy to extend." + s.summary = 'A CMS for Ruby on Rails, supporting Rails 6.1+' s.email = 'gems@p.arndt.io' s.homepage = 'https://www.refinerycms.com' s.authors = ['Philip Arndt', 'David Jones', 'Uģis Ozols', 'Brice Sanchez'] From 598e84c7d2962275149fb30e7ae24f42c4b81901 Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Fri, 2 Jan 2026 19:11:54 +1300 Subject: [PATCH 66/74] Try converting coffee to JS --- .../javascripts/refinery/ajaxy_pagination.js | 16 +++++++ .../refinery/ajaxy_pagination.js.coffee | 10 ----- .../refinery/interface.js.coffee.erb | 29 ------------- .../javascripts/refinery/interface.js.erb | 42 +++++++++++++++++++ .../refinery/submit_continue.js.coffee.erb | 12 ------ .../refinery/submit_continue.js.erb | 23 ++++++++++ .../refinery/engine/templates/Gemfile | 1 - core/refinerycms-core.gemspec | 2 +- 8 files changed, 82 insertions(+), 53 deletions(-) create mode 100644 core/app/assets/javascripts/refinery/ajaxy_pagination.js delete mode 100644 core/app/assets/javascripts/refinery/ajaxy_pagination.js.coffee delete mode 100644 core/app/assets/javascripts/refinery/interface.js.coffee.erb create mode 100644 core/app/assets/javascripts/refinery/interface.js.erb delete mode 100644 core/app/assets/javascripts/refinery/submit_continue.js.coffee.erb create mode 100644 core/app/assets/javascripts/refinery/submit_continue.js.erb diff --git a/core/app/assets/javascripts/refinery/ajaxy_pagination.js b/core/app/assets/javascripts/refinery/ajaxy_pagination.js new file mode 100644 index 00000000000..b1aaec19be7 --- /dev/null +++ b/core/app/assets/javascripts/refinery/ajaxy_pagination.js @@ -0,0 +1,16 @@ +window.init_ajaxy_pagination = function() { + if (typeof window.history.pushState === "function") { + $(".pagination_container .pagination a").on("click", function(e) { + let navigate_to = this.href.replace(/(\&(amp\;)?)?from_page\=\d+/, ""); + navigate_to += "&from_page=" + $(".current").text(); + navigate_to = navigate_to.replace("?&", "?").replace(/\s+/, ""); + + const current_state_location = location.pathname + location.href.split(location.pathname)[1]; + + window.history.pushState({ path: current_state_location }, "", navigate_to); + $(document).paginateTo(navigate_to); + + e.preventDefault(); + }); + } +}; \ No newline at end of file diff --git a/core/app/assets/javascripts/refinery/ajaxy_pagination.js.coffee b/core/app/assets/javascripts/refinery/ajaxy_pagination.js.coffee deleted file mode 100644 index fb7b9d569a1..00000000000 --- a/core/app/assets/javascripts/refinery/ajaxy_pagination.js.coffee +++ /dev/null @@ -1,10 +0,0 @@ -@init_ajaxy_pagination = -> - if typeof (window.history.pushState) == "function" - $(".pagination_container .pagination a").on "click", (e) -> - navigate_to = @href.replace(/(\&(amp\;)?)?from_page\=\d+/, "") - navigate_to += "&from_page=" + $(".current").text() - navigate_to = navigate_to.replace("?&", "?").replace(/\s+/, "") - current_state_location = (location.pathname + location.href.split(location.pathname)[1]) - window.history.pushState path: current_state_location, "", navigate_to - $(document).paginateTo navigate_to - e.preventDefault() diff --git a/core/app/assets/javascripts/refinery/interface.js.coffee.erb b/core/app/assets/javascripts/refinery/interface.js.coffee.erb deleted file mode 100644 index 592f4066b0c..00000000000 --- a/core/app/assets/javascripts/refinery/interface.js.coffee.erb +++ /dev/null @@ -1,29 +0,0 @@ -@init_interface = -> - $("body#dialog_container.dialog").addClass "iframed" if parent and parent.document.location.href != document.location.href - $("input:submit:not(.button)").addClass "button" - - if typeof(visual_editor_init_interface_hook) != 'undefined' - visual_editor_init_interface_hook() - - $("#current_locale li a").click (e) -> - $("#current_locale li a span.action").each (span) -> - $(this).css "display", (if $(this).css("display") == "none" then "" else "none") - - $("#other_locales").animate - opacity: "toggle" - height: "toggle" - , 250 - $("html,body").animate scrollTop: $("#other_locales").parent().offset().top, 250 - e.preventDefault() - - $(".form-actions .form-actions-left input:submit#submit_button").click (e) -> - $(this).nextAll('#spinner').removeClass('hidden_icon').addClass('unhidden_icon') - - $(".form-actions.form-actions-dialog .form-actions-left a.close_dialog").click (e) -> - titlebar_close_button = $('.ui-dialog-titlebar-close') - titlebar_close_button = parent.$('.ui-dialog-titlebar-close') if parent - titlebar_close_button.click - e.preventDefault() - - $("a.suppress").on "click", (e) -> - e.preventDefault() diff --git a/core/app/assets/javascripts/refinery/interface.js.erb b/core/app/assets/javascripts/refinery/interface.js.erb new file mode 100644 index 00000000000..bd352629f1e --- /dev/null +++ b/core/app/assets/javascripts/refinery/interface.js.erb @@ -0,0 +1,42 @@ +window.init_interface = function() { + if (parent && parent.document.location.href !== document.location.href) { + $("body#dialog_container.dialog").addClass("iframed"); + } + + $("input:submit:not(.button)").addClass("button"); + + if (typeof visual_editor_init_interface_hook !== 'undefined') { + visual_editor_init_interface_hook(); + } + + $("#current_locale li a").click(function(e) { + $("#current_locale li a span.action").each(function() { + $(this).css("display", $(this).css("display") === "none" ? "" : "none"); + }); + + $("#other_locales").animate({ + opacity: "toggle", + height: "toggle" + }, 250); + + $("html,body").animate({ scrollTop: $("#other_locales").parent().offset().top }, 250); + e.preventDefault(); + }); + + $(".form-actions .form-actions-left input:submit#submit_button").click(function(e) { + $(this).nextAll('#spinner').removeClass('hidden_icon').addClass('unhidden_icon'); + }); + + $(".form-actions.form-actions-dialog .form-actions-left a.close_dialog").click(function(e) { + let titlebar_close_button = $('.ui-dialog-titlebar-close'); + if (parent) { + titlebar_close_button = parent.$('.ui-dialog-titlebar-close'); + } + titlebar_close_button.click(); + e.preventDefault(); + }); + + $("a.suppress").on("click", function(e) { + e.preventDefault(); + }); +}; diff --git a/core/app/assets/javascripts/refinery/submit_continue.js.coffee.erb b/core/app/assets/javascripts/refinery/submit_continue.js.coffee.erb deleted file mode 100644 index baa2b44aa99..00000000000 --- a/core/app/assets/javascripts/refinery/submit_continue.js.coffee.erb +++ /dev/null @@ -1,12 +0,0 @@ -<%# encoding: utf-8 %> -@init_submit_continue = -> - $("#submit_continue_button").click submit_and_continue - $("form").change (e) -> - $(this).attr "data-changes-made", true - - if (continue_editing_button = $("#continue_editing")).length > 0 and continue_editing_button.attr("rel") != "no-prompt" - $("#editor_switch a").click (e) -> - e.preventDefault() unless confirm("<%= ::I18n.t("refinery.js.admin.confirm_changes") %>") if $("form[data-changes-made]").length > 0 - - $("input[id=page_custom_slug]").change -> - $("#submit_continue_button").remove() diff --git a/core/app/assets/javascripts/refinery/submit_continue.js.erb b/core/app/assets/javascripts/refinery/submit_continue.js.erb new file mode 100644 index 00000000000..b8ec63d89ab --- /dev/null +++ b/core/app/assets/javascripts/refinery/submit_continue.js.erb @@ -0,0 +1,23 @@ +<%# encoding: utf-8 %> +window.init_submit_continue = function() { + $("#submit_continue_button").click(submit_and_continue); + + $("form").change(function(e) { + $(this).attr("data-changes-made", true); + }); + + const continue_editing_button = $("#continue_editing"); + if (continue_editing_button.length > 0 && continue_editing_button.attr("rel") !== "no-prompt") { + $("#editor_switch a").click(function(e) { + if ($("form[data-changes-made]").length > 0) { + if (!confirm("<%= ::I18n.t("refinery.js.admin.confirm_changes") %>")) { + e.preventDefault(); + } + } + }); + } + + $("input[id=page_custom_slug]").change(function() { + $("#submit_continue_button").remove(); + }); +}; diff --git a/core/lib/generators/refinery/engine/templates/Gemfile b/core/lib/generators/refinery/engine/templates/Gemfile index cf6921bd72b..c2fc050947c 100644 --- a/core/lib/generators/refinery/engine/templates/Gemfile +++ b/core/lib/generators/refinery/engine/templates/Gemfile @@ -38,6 +38,5 @@ end # in production environments by default. group :assets do gem 'sass-rails' - gem 'coffee-rails' gem 'uglifier' end diff --git a/core/refinerycms-core.gemspec b/core/refinerycms-core.gemspec index 964acd6a2bf..f9e8c8d4fd8 100644 --- a/core/refinerycms-core.gemspec +++ b/core/refinerycms-core.gemspec @@ -24,7 +24,7 @@ Gem::Specification.new do |s| s.add_dependency 'actionpack', rails_version s.add_dependency 'activerecord', rails_version - s.add_dependency 'coffee-rails', ['~> 5.0', '>= 5.0.0'] + s.add_dependency 'decorators', '~> 2.0', '>= 2.0.0' s.add_dependency 'font-awesome-sass', '>= 4.3.0', '< 5.0' s.add_dependency 'jquery-rails', '~> 4.3', '>= 4.3.1' From 0587a9483a97887b3ce48e89e226d2cd6064c335 Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Fri, 2 Jan 2026 19:45:44 +1300 Subject: [PATCH 67/74] For now, precompile assets for Rails ~> 8.1.0 --- .github/workflows/main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index da5d9a35043..2a997f9c504 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -55,4 +55,6 @@ jobs: sudo -u postgres psql -c "CREATE USER runner WITH SUPERUSER PASSWORD 'runner'" - run: bin/rake refinery:testing:dummy_app + - run: cd spec/dummy && bin/rails assets:precompile + if: ${{ matrix.rails == '~> 8.1.0' }} - run: bin/rspec ${{ matrix.extension }}/spec From 77bac044c1795d6882ad4b0e63c785d8fa49ae90 Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Fri, 2 Jan 2026 20:45:39 +1300 Subject: [PATCH 68/74] Reverse order of ruby tests --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2a997f9c504..45a84ca0e94 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,8 +12,8 @@ jobs: matrix: database: [ sqlite3, mysql, postgresql ] extension: [ core, dragonfly, images, pages, resources ] - ruby: [ 3.2, 3.3, 3.4, 4.0 ] - rails: [ '~> 7.2.0', '~> 8.0.0', '~> 8.1.0' ] + ruby: [ 4.0, 3.4, 3.3, 3.2 ] + rails: [ '~> 8.1.0', '~> 8.0.0', '~> 7.2.0' ] fail-fast: false runs-on: ubuntu-latest From 0a0f43f9829166ca86806c26e3440c071fa1896b Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Fri, 2 Jan 2026 21:03:18 +1300 Subject: [PATCH 69/74] Try to remove the logger override --- Rakefile | 6 +----- .../generators/refinery/dummy/templates/rails/boot.rb.erb | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/Rakefile b/Rakefile index b7f03836981..24949db4bef 100644 --- a/Rakefile +++ b/Rakefile @@ -1,5 +1,4 @@ #!/usr/bin/env rake -require 'logger' begin require 'bundler/setup' @@ -8,10 +7,7 @@ rescue LoadError end APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__) - -if File.exist?(APP_RAKEFILE) - load 'rails/tasks/engine.rake' -end +load 'rails/tasks/engine.rake' if File.exist?(APP_RAKEFILE) Dir[File.expand_path('../tasks/**/*', __FILE__)].each do |task| load task diff --git a/core/lib/generators/refinery/dummy/templates/rails/boot.rb.erb b/core/lib/generators/refinery/dummy/templates/rails/boot.rb.erb index 4fc50e85117..c61f04db8c2 100644 --- a/core/lib/generators/refinery/dummy/templates/rails/boot.rb.erb +++ b/core/lib/generators/refinery/dummy/templates/rails/boot.rb.erb @@ -1,5 +1,4 @@ require 'rubygems' -require 'logger' gemfile = File.expand_path("<%= gemfile_path %>", __FILE__) ENV['BUNDLE_GEMFILE'] = gemfile From 756523f79af9f6df5b44f5495702cd8bd5879c9e Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Fri, 2 Jan 2026 21:18:54 +1300 Subject: [PATCH 70/74] Try to rescue the dreaded logger error --- .../refinery/dummy/templates/rails/application.rb.erb | 9 ++++++++- core/lib/refinery.rb | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/core/lib/generators/refinery/dummy/templates/rails/application.rb.erb b/core/lib/generators/refinery/dummy/templates/rails/application.rb.erb index 647139b94c3..85980852c18 100644 --- a/core/lib/generators/refinery/dummy/templates/rails/application.rb.erb +++ b/core/lib/generators/refinery/dummy/templates/rails/application.rb.erb @@ -1,6 +1,13 @@ require File.expand_path('../boot', __FILE__) -require 'rails/all' +begin + require 'rails/all' +rescue NameError => e + raise unless e.message.include?("ActiveSupport::LoggerThreadSafeLevel::Logger") + + require "logger" + retry +end require 'refinerycms/core' # Require the gems listed in Gemfile, including any gems diff --git a/core/lib/refinery.rb b/core/lib/refinery.rb index 921851faa03..a12e806bb6f 100644 --- a/core/lib/refinery.rb +++ b/core/lib/refinery.rb @@ -1,4 +1,11 @@ -require 'rails' # from railties +begin + require 'rails' # from railties +rescue NameError => e + raise unless e.message.include?("ActiveSupport::LoggerThreadSafeLevel::Logger") + + require "logger" + retry +end require 'active_record' require 'action_controller' require 'rbconfig' From 88e2af5e72b5efef5b880e8708de6edecdbe6ba5 Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Fri, 2 Jan 2026 22:07:47 +1300 Subject: [PATCH 71/74] Try to update the docs as much as possible --- Gemfile | 1 - contributing.md | 2 +- .../app/assets/images/refinery/logo-large.png | Bin 11387 -> 50102 bytes .../assets/images/refinery/logo-medium.png | Bin 7507 -> 20736 bytes .../assets/images/refinery/logo-site-bar.png | Bin 6576 -> 12065 bytes .../images/refinery/logo-small-medium.png | Bin 5031 -> 10809 bytes .../app/assets/images/refinery/logo-small.png | Bin 2228 -> 6206 bytes core/app/assets/images/refinery/logo-tiny.png | Bin 1236 -> 2044 bytes core/app/assets/images/refinery/logo.png | Bin 3202 -> 30043 bytes .../images/refinery/refinery-cms-logo.svg | 559 +----------------- core/app/views/refinery/_site_bar.html.erb | 4 +- core/refinerycms-core.gemspec | 2 +- .../1 - Installation Prerequisites.md | 38 +- .../2 - Getting Started.md | 2 +- .../4 - How to get help.md | 10 +- readme.md | 40 +- spec/spec_helper.rb | 13 - 17 files changed, 31 insertions(+), 640 deletions(-) diff --git a/Gemfile b/Gemfile index 5022415cef5..d430e9ee70c 100644 --- a/Gemfile +++ b/Gemfile @@ -69,7 +69,6 @@ group :development, :test do end group :test do - gem 'coveralls', require: false gem 'generator_spec', '~> 0.9.3' gem 'launchy' gem 'refinerycms-testing', path: './testing' diff --git a/contributing.md b/contributing.md index c8e1584ce12..2c98796a21b 100644 --- a/contributing.md +++ b/contributing.md @@ -33,4 +33,4 @@ inclusion. ## Links ## -See also the [Contributing to Refinery](https://www.refinerycms.com/guides/contributing-to-refinery) guide. +See also the [Contributing to Refinery](https://github.com/refinery/refinerycms/blob/main/doc/guides/8%20-%20Contributing/1%20-%20Contributing%20to%20Refinery.md) guide. diff --git a/core/app/assets/images/refinery/logo-large.png b/core/app/assets/images/refinery/logo-large.png index e933fbac16f21fa360cfba6839c7944c9c8fabdb..830186d71497dbec0973fa0a719e2e5ecc46f2b9 100644 GIT binary patch literal 50102 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 literal 11387 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 H`T73=YkHtj diff --git a/core/app/assets/images/refinery/logo-medium.png b/core/app/assets/images/refinery/logo-medium.png index f1a76bd3adafa482304533882bebb521a8ebb389..0b3bf66daa05de5c46c2a913be4e9771c6d4e316 100644 GIT binary patch literal 20736 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 diff --git a/core/app/assets/images/refinery/logo-small.png b/core/app/assets/images/refinery/logo-small.png index a95b57e988d5df6df476d4a107b41fe7050d3dfa..5b95fb0673c43207b4e8da8a526abeda4513247c 100644 GIT binary patch literal 6206 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 ABme*a literal 2228 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 uRYO*_J#Ztosf?47>(W#wiNV2 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 diff --git a/core/app/assets/images/refinery/refinery-cms-logo.svg b/core/app/assets/images/refinery/refinery-cms-logo.svg index 4b95f02cc90..1cf88aae664 100644 --- a/core/app/assets/images/refinery/refinery-cms-logo.svg +++ b/core/app/assets/images/refinery/refinery-cms-logo.svg @@ -1,558 +1 @@ - -image/svg+xml \ No newline at end of file + \ No newline at end of file diff --git a/core/app/views/refinery/_site_bar.html.erb b/core/app/views/refinery/_site_bar.html.erb index 4335b1f939d..d130aee3dbc 100644 --- a/core/app/views/refinery/_site_bar.html.erb +++ b/core/app/views/refinery/_site_bar.html.erb @@ -6,8 +6,8 @@
            - <%= link_to 'https://www.refinerycms.com', :id => 'site_bar_refinery_cms_logo', :target => '_blank' do %> - <%= image_tag 'refinery/refinery-cms-logo.svg', alt: 'Refinery CMS™' %> + <%= link_to 'https://github.com/refinery/refinerycms', :id => 'site_bar_refinery_cms_logo', :target => '_blank' do %> + <%= image_tag 'refinery/refinery-cms-logo.svg', alt: 'Refinery CMS' %> <% end %>
            diff --git a/core/refinerycms-core.gemspec b/core/refinerycms-core.gemspec index f9e8c8d4fd8..28bba0c4fab 100644 --- a/core/refinerycms-core.gemspec +++ b/core/refinerycms-core.gemspec @@ -12,7 +12,7 @@ Gem::Specification.new do |s| s.summary = 'Core extension for Refinery CMS' s.description = 'The core of Refinery CMS. This handles the common functionality and is required by most extensions' s.email = 'gems@p.arndt.io' - s.homepage = 'https://www.refinerycms.com' + s.homepage = 'https://github.com/refinery/refinerycms' s.authors = ['Philip Arndt', 'David Jones', 'Uģis Ozols', 'Brice Sanchez'] s.license = 'MIT' s.require_paths = %w[lib] diff --git a/doc/guides/1 - Getting Started/1 - Installation Prerequisites.md b/doc/guides/1 - Getting Started/1 - Installation Prerequisites.md index 2459b6f8024..a16e26d2858 100644 --- a/doc/guides/1 - Getting Started/1 - Installation Prerequisites.md +++ b/doc/guides/1 - Getting Started/1 - Installation Prerequisites.md @@ -4,15 +4,15 @@ This guide covers getting your system ready for Refinery. Afterwards you will ha * A working version of Ruby * ImageMagick installed -* Either the SQLite, MySQL, or PostgreSQL database configured +* Either the SQLite, MySQL, or Postgres database configured ## Checklist If you are already a Rails developer, you will most likely not have to install anything else. Here's the requirements for Refinery: -* __Ruby__ - 2.2.2+, Rubinius, and JRuby are all acceptable +* __Ruby__ - 3.2+ and JRuby are all acceptable * __RubyGems__ - Recommended that you have the latest version installed -* __Database__ - SQLite3 (default), MySQL, or PostgreSQL +* __Database__ - SQLite3 (default), MySQL, or Postgres * __ImageMagick__ - Recommended that you have the latest version installed If you have all of these things, great! Proceed on to the [Getting Started with Refinery](/guides/getting-started/) guide. @@ -57,35 +57,15 @@ $ sudo apt-get install mysql-client mysql-server libmysqlclient-dev $ sudo apt-get install imagemagick ``` -## Mac OS X +## macOS ### Ruby -__TIP__: The best way to install Ruby is using [rbenv](https://github.com/rbenv/rbenv) - -### Rubygems - -__TIP__: If you used `rbenv` above then this step will not be necessary. - -Rubygems also comes installed by default, however, it could be an old version which will cause problems. Update using: - -```shell -$ gem update --system -``` - -Also, in the past, we face to a RDoc bug, you should update it as well: - -```shell -$ gem install rdoc -``` - -### Database - -SQLite is most likely already installed. +__TIP__: The best way to install Ruby is using [mise-en-place](https://mise.jdx.dev/) ### ImageMagick -Use this shell script: . Or, if you have [Homebrew](http://mxcl.github.io/homebrew/) installed, you can use: +If you have [Homebrew](https://brew.sh/) installed, you can use: ```shell $ brew install imagemagick @@ -95,17 +75,15 @@ $ brew install imagemagick ### Ruby and Rubygems - provides a great installer to get you up and running in no time. Just download the kit and follow through the installer. - ### Database -If you used Rails Installer, then SQLite will have been installed by default. For MySQL, follow the instructions at the MySQL website: +For MySQL, follow the instructions at the MySQL website: . For Postgres, follow the instructions at the Postgres website: . ### ImageMagick __WARNING__: ImageMagick is tricky to install on Windows. Make sure to read the instructions carefully, and if one version does not work for you try an older version as well. -Follow the instructions at +Follow the instructions at ## Ready to Install! diff --git a/doc/guides/1 - Getting Started/2 - Getting Started.md b/doc/guides/1 - Getting Started/2 - Getting Started.md index 04a2845c940..c751b364416 100644 --- a/doc/guides/1 - Getting Started/2 - Getting Started.md +++ b/doc/guides/1 - Getting Started/2 - Getting Started.md @@ -38,7 +38,7 @@ The Refinery philosophy includes several guiding principles: * __"The Rails Way" where possible__ - Refinery embraces conventions used in Rails, allowing any Rails programmer to leverage existing knowledge to get up and running quickly. * __End user focused interface__ - Refinery's user interface is simple, bright and attractive so end users feel invited and not overwhelmed. * __Awesome developer tools__ - Refinery makes it easy for developers to add functionality and change the front-end look and feel. -* __Encourage and Help Others__ - Refinery has an active community on Github, Gitter and Google Groups. If you ever have a question there is someone able and willing to assist. +* __Encourage and Help Others__ - Refinery has an active community on GitHub, and Google Groups. If you ever have a question there is someone able and willing to assist. ### Refinery's architecture diff --git a/doc/guides/1 - Getting Started/4 - How to get help.md b/doc/guides/1 - Getting Started/4 - How to get help.md index e0ff8a0b20a..73d320e65b7 100644 --- a/doc/guides/1 - Getting Started/4 - How to get help.md +++ b/doc/guides/1 - Getting Started/4 - How to get help.md @@ -6,14 +6,6 @@ We all sometimes hit a brick wall. This guide will show you how to: One of Refinery's key principles is "Encourage and Help Others" so if you have any problems just let us know and we'll do our best to help you. And one day you might help someone else out too! -## Gitter Channel - -Connect to our [Refinery Gitter channel](https://gitter.im/refinery/refinerycms). - -If you ask a question and don't get an immediate response, don't take offence; either wait a half-hour and ask again, or just post your question on the [Google Group](https://groups.google.com/forum/#!forum/refinery-cms) instead. This gives developers who are in other timezones or who are temporarily unavailable a chance to help you out. - -Please remember that Refinery is a fully open-source application. None of us are paid to fix it or improve it; we do so because we like Refinery and we greatly appreciate its value. Unfortunately, we cannot immediately drop everything to solve a problem for you (as much as we'd like to). Be patient, and try to figure out the problem on your own while you wait. The more information you can dig up yourself, the easier it is for us to help you resolve your issues expediently. - ## Google Group The [Refinery CMS Google Group](https://groups.google.com/forum/#!forum/refinery-cms) is a great place to ask for help if you don't get a response on Gitter. Your first message may take a short while to appear, as all first-time posters are required by Google Groups to pass moderation. @@ -22,4 +14,4 @@ The [Refinery CMS Google Group](https://groups.google.com/forum/#!forum/refinery * API (click 'File List' in top right) - http://api.refinerycms.org * [GitHub Wiki](https://github.com/refinery/refinerycms/wiki) -* [Guides](https://www.refinerycms.com/guides) \ No newline at end of file +* [Guides](https://www.refinerycms.com/guides) diff --git a/readme.md b/readme.md index 4526e8173e7..736d3346c52 100644 --- a/readme.md +++ b/readme.md @@ -1,38 +1,32 @@ -# Refinery CMS™ +# Refinery CMS -__An open source content management system for Rails 5.1+__ - -More information at [https://www.refinerycms.com](https://www.refinerycms.com) - -[![Build Status](https://travis-ci.org/refinery/refinerycms.svg?branch=master)](https://travis-ci.org/refinery/refinerycms) [![Code Climate](https://codeclimate.com/github/refinery/refinerycms.svg)](https://codeclimate.com/github/refinery/refinerycms) [![Coverage Status](https://img.shields.io/coveralls/refinery/refinerycms.svg)](https://coveralls.io/r/refinery/refinerycms?branch=master) - -You can chat with us using Gitter: - -[![Gitter chat](https://badges.gitter.im/refinery/refinerycms.svg)](https://gitter.im/refinery/refinerycms) +__An open source content management system for Rails 6.1+ through 8.1+__ You can deploy an example app to Heroku: [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/refinery/refinerycms-example-app) +Note that some of our docs in this README are out of date, and our website is not currently live. Please refer to our [GitHub repository](https://github.com/refinery/refinerycms) for the current status and instructions. Guides can be found in the [doc/guides](https://github.com/refinery/refinerycms/tree/main/doc/guides) folder. + ## Requirements -* [Bundler](http://gembundler.com) -* [ImageMagick](http://www.imagemagick.org/script/install-source.php) +* [Bundler](https://bundler.io/) +* [ImageMagick](https://imagemagick.org/script/install-source.php) * :warning: Warning: ImageMagick currently has a serious security vulnerability, CVE-2016–3714. After installing, you must disable certain features in ImageMagick's policy configuration. Please see the following for details: * https://imagetragick.com/ * Mac OS X users should use [homebrew's](https://github.com/mxcl/homebrew/wiki/installation) `brew install imagemagick` or the [magick-installer](https://github.com/maddox/magick-installer). ## How to -* __[Install Refinery CMS™](https://www.refinerycms.com/download)__ -* [Install Refinery CMS™ on Heroku](https://www.refinerycms.com/guides/heroku) -* [Contribute to Refinery CMS™](readme.md#contributing) +* __[Install Refinery CMS](https://www.refinerycms.com/download)__ +* [Install Refinery CMS on Heroku](https://github.com/refinery/refinerycms/blob/main/doc/guides/7%20-%20Hosting%20and%20Deployment/1%20-%20Heroku.md) +* [Contribute to Refinery CMS](readme.md#contributing) ## Getting Started If you're new to Refinery, start with this guide: -* __[Getting Started](https://www.refinerycms.com/guides/getting-started)__ +* __[Getting Started](https://github.com/refinery/refinerycms/tree/main/doc/guides/1%20-%20Getting%20Started)__ For Rails 5.1+ support, you can use version `4.0.x` using this template: @@ -57,14 +51,12 @@ Unlike other content managers, Refinery is truly __aimed at the end user__ makin * Easily customise the look to suit the business. * __Extend with custom extensions__ to do anything Refinery doesn't do out of the box. * Sticks to __"the Rails way"__ as much as possible; we don't force you to learn new templating languages. -* Uses [jQuery](http://jquery.com/) for fast and concise Javascript. +* Uses [jQuery](http://jquery.com/) for now, for fast and concise Javascript. ## Help and Documentation -* [Getting Started](https://www.refinerycms.com/guides/getting-started) -* [Guides](https://www.refinerycms.com/guides) -* [Google Group Discussion](https://groups.google.com/forum/#!forum/refinery-cms) -* [Gitter chat](https://gitter.im/refinery/refinerycms) +* [Getting Started](https://github.com/refinery/refinerycms/tree/main/doc/guides/1%20-%20Getting%20Started) +* [Google Group Discussion](https://groups.google.com/g/refinery-cms) * [GitHub repository](https://github.com/refinery/refinerycms) * [Developer/API documentation](http://rubydoc.info/github/refinery/refinerycms) * [Twitter Account](https://twitter.com/refinerycms) @@ -112,13 +104,13 @@ For help run the command without any options: ## Contributing See [contributing.md](contributing.md) -and [Contributing to Refinery](https://www.refinerycms.com/guides/contributing-to-refinery) +and [Contributing to Refinery](https://github.com/refinery/refinerycms/blob/main/doc/guides/8%20-%20Contributing/1%20-%20Contributing%20to%20Refinery.md) guide for details about contributing and running test. ## License -Refinery CMS™ is released under the MIT license. See the [license.md file](license.md#readme) for details. +Refinery CMS is released under the MIT license. See the [license.md file](license.md#readme) for details. ### Credits -Many of the icons used in Refinery CMS™ are from the wonderful [Silk library by Mark James](http://www.famfamfam.com/lab/icons/silk/). +Many of the icons used in Refinery CMS are from the wonderful [Silk library by Mark James](http://www.famfamfam.com/lab/icons/silk/). diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b5c1c4d13a9..07c4561f04b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,24 +7,11 @@ # Configure Rails Environment ENV["RAILS_ENV"] ||= 'test' -if ENV['TRAVIS'] - require 'coveralls' - Coveralls.wear! -end - require File.expand_path("../dummy/config/environment", __FILE__) require 'rack/test' require 'rspec/rails' - -# if testing on localhost -Capybara.configure do |config| - parsed = Rack::Builder.parse_file(File.expand_path('../config.ru', __dir__)) - # Rack 3+ returns the app directly; Rack 2 returned [app, options] - config.app = parsed.is_a?(Array) ? parsed.first : parsed -end - if ENV['RETRY_COUNT'] require 'rspec/retry' RSpec.configure do |config| From e1ee3447d3eec615052b3ac848137d50f6dfdca8 Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Fri, 2 Jan 2026 22:20:15 +1300 Subject: [PATCH 72/74] Some name/label pedantry --- .github/workflows/legacy.yml | 2 +- .github/workflows/main.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/legacy.yml b/.github/workflows/legacy.yml index 997b9f6a167..a595315b887 100644 --- a/.github/workflows/legacy.yml +++ b/.github/workflows/legacy.yml @@ -33,7 +33,7 @@ jobs: RAILS_VERSION: ${{ matrix.rails }} RETRY_COUNT: 3 - name: rails-${{ matrix.rails }} ruby-${{ matrix.ruby }} refinerycms-${{ matrix.extension }} ${{ matrix.database }} + name: rails ${{ matrix.rails }} ruby ${{ matrix.ruby }} refinerycms-${{ matrix.extension }} ${{ matrix.database }} steps: - run: sudo apt-get update && sudo apt-get install libsqlite3-dev -y - uses: actions/checkout@v6 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 45a84ca0e94..d160936f591 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,7 +28,7 @@ jobs: RAILS_VERSION: ${{ matrix.rails }} RETRY_COUNT: 3 - name: rails-${{ matrix.rails }} ruby-${{ matrix.ruby }} refinerycms-${{ matrix.extension }} ${{ matrix.database }} + name: rails ${{ matrix.rails }} ruby ${{ matrix.ruby }} refinerycms-${{ matrix.extension }} ${{ matrix.database }} steps: - run: sudo apt-get update && sudo apt-get install libsqlite3-dev -y - uses: actions/checkout@v6 From 9e4345c3797a777df898e04ab5efdaeb5cb8cd94 Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Fri, 2 Jan 2026 22:24:32 +1300 Subject: [PATCH 73/74] Update Gemfile --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index d430e9ee70c..191982acc41 100644 --- a/Gemfile +++ b/Gemfile @@ -22,7 +22,7 @@ path './' do gem 'refinerycms-resources' end -gem 'refinerycms-i18n', github: 'anitagraham/refinerycms-i18n', branch: 'ruby3' +gem 'refinerycms-i18n', github: 'refinery/refinerycms-i18n', branch: 'main' # Add support for refinerycms-acts-as-indexed gem 'refinerycms-acts-as-indexed', '~> 4.0', '>= 4.0.0', From c25072a765d557f007bb241d054961377db203d6 Mon Sep 17 00:00:00 2001 From: Philip Arndt Date: Fri, 16 Jan 2026 08:36:34 +1300 Subject: [PATCH 74/74] Update Gemfile --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 191982acc41..2fe1c5caffc 100644 --- a/Gemfile +++ b/Gemfile @@ -27,7 +27,7 @@ gem 'refinerycms-i18n', github: 'refinery/refinerycms-i18n', branch: 'main' # Add support for refinerycms-acts-as-indexed gem 'refinerycms-acts-as-indexed', '~> 4.0', '>= 4.0.0', github: 'refinery/refinerycms-acts-as-indexed', - branch: 'master' + branch: 'main' # Add the default visual editor, for now. gem 'refinerycms-wymeditor', ['~> 3.0', '>= 3.0.0']