diff --git a/.github/workflows/legacy.yml b/.github/workflows/legacy.yml new file mode 100644 index 00000000000..a595315b887 --- /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 b778325b94e..d160936f591 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,11 +10,11 @@ jobs: test: strategy: matrix: - database: [ mysql, postgresql ] + database: [ sqlite3, mysql, postgresql ] extension: [ core, dragonfly, images, pages, resources ] - ruby: [ 2.7, 2.6 ] + ruby: [ 4.0, 3.4, 3.3, 3.2 ] + rails: [ '~> 8.1.0', '~> 8.0.0', '~> 7.2.0' ] fail-fast: false - max-parallel: 20 runs-on: ubuntu-latest env: @@ -25,17 +25,22 @@ 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@v2 + - 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: | @@ -50,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 diff --git a/Gemfile b/Gemfile index 17e0baf8dd4..2fe1c5caffc 100644 --- a/Gemfile +++ b/Gemfile @@ -1,34 +1,55 @@ +# frozen_string_literal: true + source 'https://rubygems.org' gemspec -path "./" do - gem "refinerycms-core" - gem "refinerycms-images" - gem "refinerycms-pages" - gem "refinerycms-resources" +# 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 + +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', github: 'refinery/refinerycms-i18n', branch: 'main' # 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: 'main' # 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 -unless ENV['CI'] +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' 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 @@ -43,19 +64,15 @@ group :development, :test do gem 'activejob' gem 'bootsnap', require: false gem 'listen', '~> 3.0' + gem 'puma', require: false + gem 'rspec-rails' end group :test do - gem 'refinerycms-testing', path: './testing' 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' - - # 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/Rakefile b/Rakefile index e294ee1fc12..24949db4bef 100644 --- a/Rakefile +++ b/Rakefile @@ -1,4 +1,5 @@ #!/usr/bin/env rake + begin require 'bundler/setup' rescue LoadError @@ -6,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/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 e933fbac16f..830186d7149 100644 Binary files a/core/app/assets/images/refinery/logo-large.png and b/core/app/assets/images/refinery/logo-large.png differ diff --git a/core/app/assets/images/refinery/logo-medium.png b/core/app/assets/images/refinery/logo-medium.png index f1a76bd3ada..0b3bf66daa0 100644 Binary files a/core/app/assets/images/refinery/logo-medium.png and b/core/app/assets/images/refinery/logo-medium.png differ diff --git a/core/app/assets/images/refinery/logo-site-bar.png b/core/app/assets/images/refinery/logo-site-bar.png index 03a48192e7a..bb33192ae2a 100644 Binary files a/core/app/assets/images/refinery/logo-site-bar.png and b/core/app/assets/images/refinery/logo-site-bar.png differ diff --git a/core/app/assets/images/refinery/logo-small-medium.png b/core/app/assets/images/refinery/logo-small-medium.png index 3282f5606cf..32b02c86d8a 100644 Binary files a/core/app/assets/images/refinery/logo-small-medium.png and b/core/app/assets/images/refinery/logo-small-medium.png differ diff --git a/core/app/assets/images/refinery/logo-small.png b/core/app/assets/images/refinery/logo-small.png index a95b57e988d..5b95fb0673c 100644 Binary files a/core/app/assets/images/refinery/logo-small.png and b/core/app/assets/images/refinery/logo-small.png differ diff --git a/core/app/assets/images/refinery/logo-tiny.png b/core/app/assets/images/refinery/logo-tiny.png index 7fe27dabbaf..0da1cec0a12 100644 Binary files a/core/app/assets/images/refinery/logo-tiny.png and b/core/app/assets/images/refinery/logo-tiny.png differ diff --git a/core/app/assets/images/refinery/logo.png b/core/app/assets/images/refinery/logo.png index c8720d8c971..eab4ba3f52c 100644 Binary files a/core/app/assets/images/refinery/logo.png and b/core/app/assets/images/refinery/logo.png differ 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/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/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/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..24a0c1efed4 100644 --- a/core/app/assets/stylesheets/refinery/components/_icons.scss +++ b/core/app/assets/stylesheets/refinery/components/_icons.scss @@ -2,62 +2,104 @@ /* 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; + // icon_base (the icon) occupies the ::before position + @include icon_base($locale_icon_colour, $size: 1.5rem); + + &::after { + position: absolute; + left: 0.3rem; + font-size: 0.8rem; + color: $locale_text_colour; + font-weight: bold; + content: attr(id); + 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 e7eb7dc36b0..650a8489e32 100644 --- a/core/app/assets/stylesheets/refinery/global/_colours.scss +++ b/core/app/assets/stylesheets/refinery/global/_colours.scss @@ -7,15 +7,22 @@ $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: $icon_locale_colour; +$action_border_colour: #b7e3fc; -// $icon_preview_colour: #7f00ff; // violet -// $icon_edit_colour: #007fff; // azure -// $icon_add_colour: #7fff00; // chartreuse \ No newline at end of file +$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 new file mode 100644 index 00000000000..1cbfc94262a --- /dev/null +++ b/core/app/assets/stylesheets/refinery/mixins/_images.scss @@ -0,0 +1,99 @@ +@mixin indexImage { + 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} + .filename {font-style: italic} + .title {text-transform: capitalize} +} + +@mixin gridImage { + @include indexImage; + + a:has(img) ~ * { + 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; + padding: 0; + overflow: hidden; + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: baseline; + gap: 1rem; + .actions { + @include imageActions { + margin-left: auto; + justify-content: flex-end; + } + } + + &:hover { + background-color: $index_entry_hover_background; + } +} + +@mixin imageIndexLayout { + list-style-type: none; + width: 100%; + padding: 0; + line-height: 1.2; + @content; +} + +// apply to #image_grid +@mixin imageGridLayout { + @include imageIndexLayout { + padding-inline-start: 0; + display: grid; + grid-template-columns: repeat(4, 150px); + grid-template-rows: auto; + gap: 15px; + } +} + +// 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/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/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 { diff --git a/core/app/assets/stylesheets/refinery/refinery.scss b/core/app/assets/stylesheets/refinery/refinery.scss index 15ed8a80b75..ce57fdf8d4e 100644 --- a/core/app/assets/stylesheets/refinery/refinery.scss +++ b/core/app/assets/stylesheets/refinery/refinery.scss @@ -8,10 +8,13 @@ @import 'mixins/icon_base'; @import 'mixins/icon'; @import 'mixins/rounded'; +@import 'mixins/locales'; +@import 'mixins/images'; @import 'plugins/ui'; @import 'components/icons'; +@import 'components/file_type_icons'; @import 'components/dialog'; @import 'components/submenu'; @import 'components/tooltips'; @@ -19,4 +22,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..18ec49bbaf5 100644 --- a/core/app/assets/stylesheets/refinery/sections/_layout.scss +++ b/core/app/assets/stylesheets/refinery/sections/_layout.scss @@ -1,14 +1,16 @@ $admin_width: 1080px; $menu_width: 120px; -$standard_rounding: 0px; +$standard_rounding: 0; p { font-size: 14px; line-height: 18px; } + html { height: 100%; } + body { height: 100%; margin: -52px 0; @@ -19,43 +21,52 @@ body { color: #41403C; background: rgb(217, 217, 217); } + acronym { cursor: help; } + label[for] { cursor: pointer; } + #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; @include rounded($standard_rounding); + a { color: #41403C; text-decoration: none; } + #menu { a:hover { - border-bottom: 0px; + border-bottom: 0; } } + .hidden { display: none; } } + #login_container { background: white; width: 620px; } + #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); } + #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: 0px; + padding-top: 0; + ul { - margin: 0px 0px 18px 0px; + 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 0px; + padding: 5px 5px 5px 0; } + hr { border: 0; border-top: 1px solid #484743; @@ -114,82 +136,97 @@ hr { .errorExplanation { background-color: #FFB1B1; - padding: 0px 5px 5px 30px; + padding: 0 5px 5px 30px; font-weight: bold; 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: 0px none; + border-bottom: 0 none; } + .index #content, .splash #content { background-color: white; background-repeat: repeat-y; } + #content { - padding: 0px; + 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,56 +326,67 @@ noscript .flash.flash_alert a:hover { } } } + .actions { a { display: block; float: right; margin: 3px 3px; line-height: inherit; - border-bottom: 0px none; + border-bottom: 0 none; } } } + .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,72 +398,90 @@ nav { padding-top: 15px; } } + label { - margin-top: 0px; + margin-top: 0; } + label, a { font-size: 14px; } + .field { margin-bottom: 20px; + &.remember_me, &.forgot_password { - margin-bottom: 0px; - margin-top: 0px; + 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: 0px -41px; + background-position: 0 -41px; } + .fieldWithErrors input.larger { - background-position: 0px -82px; + 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 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; 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,174 +555,188 @@ 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, > #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: 0px 5px; + padding: 0 5px; vertical-align: top; margin-bottom: 2px; line-height: 35px; position: relative; } - > #recent_activity > ul li, > #recent_inquiries > ul li { - 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; } } + #records { 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; } } } } + .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; } - > #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; } + #records.tree ul li ul, .tree ul li ul { 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'); } + #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: 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 { .disabled { color: #A8B9C1; } - a, #content a, .current, .disabled, em { + + 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; } } + textarea { line-height: 20px; padding: 5px; } + .field-couple { margin-bottom: 20px; } + .submit { border: inherit; width: auto; @@ -650,6 +748,7 @@ label, .label_with_help { margin-top: 20px; display: block; } + label.input_label { font-size: inherit; margin-bottom: inherit; @@ -657,53 +756,63 @@ 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; font-weight: normal; font-size: 1em; - margin: 0px; - padding: 0px; + 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, @@ -720,27 +829,33 @@ 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: 0px; - top: 0px; + right: 0; + top: 0; width: 120px; text-align: right; display: block; line-height: 28px; } + #records.tree.icons .title { display: block; margin: 0 120px 0 20px; @@ -749,29 +864,36 @@ 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 0px 3px; + 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 { + +#records.tree li span.icon { display: block; float: left; width: 16px; @@ -781,15 +903,18 @@ 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'); } + #image_grid .actions a img { padding: 4px 4px 1px 4px; vertical-align: top; @@ -799,54 +924,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 0px 5px 0px; + 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: 0px; + 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; @@ -854,35 +995,42 @@ body.dialog form { min-height: 405px; width: 696px; } + span.radio { display: block; line-height: 18px; - padding: 6px 0px; + 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: 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; @@ -892,31 +1040,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: 0px; - padding: 10px 0px 0px 0px; + margin: 0; + padding: 10px 0 0 0; + li { float: left; list-style: none; - margin: 0px 18px 0px 0px; - text-align:center; + margin: 0 18px 0 0; + text-align: center; + a { display: block; border: 1px solid #999999; font-size: 10px; } + &.selected { a { border-color: #22A7F2; @@ -928,41 +1083,53 @@ 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_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: 0px; + padding: 0; } } + #upload_image_area, #upload_resource_area { padding: 12px; } + .visual_editor_dialog #page { width: 940px; padding: 6px; } + #dialog_main { .pagination { - margin: 0px; + margin: 0; position: fixed; bottom: 5px; right: 12px; z-index: 1000; } + .pages_list { 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; list-style: none; + a { display: block; padding: 3px 3px 3px 27px; @@ -970,57 +1137,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 0px 0px 0px !important; + 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; @@ -1028,36 +1210,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: 0px; + 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 @@ -1066,8 +1258,9 @@ Firefox Dotted Line Fix a:focus { outline: none; } + a img { - border: 0px none; + border: 0 none; } @@ -1096,7 +1289,7 @@ ul#page_parts_controls { margin: 0 3px; a { - border: 0px none; + border: 0 none; } } } @@ -1105,95 +1298,116 @@ 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; - padding: 0px; - border: 0px solid #F2F1ED; + 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: 0px; + 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: 0px none; + 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: 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 { position: fixed; } + 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; + 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 { width: 100%; padding: 0px; margin: 10px 0px 15px 0px; } + ul#image_grid li, .pagination_container > ul#image_grid li { position: relative; float: left; @@ -1206,170 +1420,214 @@ ul#image_grid li, .pagination_container > ul#image_grid li { 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; } + ul#image_grid li span.actions { width: 100%; } + ul.checkboxes { - margin: 0px; - padding: 0px; + 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: 0px none; + 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: 0px 10px; + 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: 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; + 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; } + /* 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: 0px; + 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: 0px 0px; + margin: 0 0; margin-left: 10px; float: left; width: 45%; + &.right_side { - float: right; + 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 { vertical-align: middle; } @@ -1394,124 +1652,46 @@ 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; margin-right: 20px; } -/* Locale picker */ -#switch_locale_picker { - margin: 0px; - padding: 0px; - li { - float: left; - padding: 0px; - 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 #records .title .preview a.locale { - border-bottom: 0px none; -} /* AJAX pagination */ .pagination_container { position: relative; } + .pagination_frame { padding: 0; width: 100%; @@ -1521,12 +1701,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; @@ -1534,21 +1717,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; @@ -1558,16 +1746,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; } } diff --git a/core/app/helpers/refinery/action_helper.rb b/core/app/helpers/refinery/action_helper.rb new file mode 100644 index 00000000000..2c24ce67480 --- /dev/null +++ b/core/app/helpers/refinery/action_helper.rb @@ -0,0 +1,75 @@ +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 locale_language(locale) + Refinery::I18n.locales[locale] + end + + def edit_in_locale(locale, url:, title: nil, **options) + if options.delete(:label) + action_label( + :locale, "#{url}?switch_locale=#{locale}", + title || locale.to_s.upcase, + **options, + id: locale, + class: :edit, **options + ) + else + 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 = [], i18n_scope: %i[refinery admin locale_picker]) + return if locales.empty? + + edit_links = locales.map do |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 +end 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/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/site_bar_helper.rb b/core/app/helpers/refinery/site_bar_helper.rb index 6d6519957e8..46d4eb3e154 100644 --- a/core/app/helpers/refinery/site_bar_helper.rb +++ b/core/app/helpers/refinery/site_bar_helper.rb @@ -1,26 +1,29 @@ +# frozen_string_literal: true + module Refinery 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), + 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 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.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 - { :locale => Refinery::I18n.current_locale } + { locale: Refinery::I18n.current_locale } end def display_site_bar? diff --git a/core/app/helpers/refinery/tag_helper.rb b/core/app/helpers/refinery/tag_helper.rb index 20acff05f4a..3a5e82676db 100644 --- a/core/app/helpers/refinery/tag_helper.rb +++ b/core/app/helpers/refinery/tag_helper.rb @@ -1,5 +1,6 @@ module Refinery module TagHelper + include ActionHelper # Returns @@ -12,44 +13,11 @@ 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) - options[:title] = title - options[:class].presence ? options[:class] << " #{action}_icon " : options[:class] = "#{action}_icon" - options[:class] << ' icon_label' if label - - 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 + 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 f3d36f3cbbe..f44e7c68f82 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 @@ -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: Refinery::I18n.default_frontend_locale.to_s) unless include_current + + 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/_site_bar.html.erb b/core/app/views/refinery/_site_bar.html.erb index e5fcb7993a8..d130aee3dbc 100644 --- a/core/app/views/refinery/_site_bar.html.erb +++ b/core/app/views/refinery/_site_bar.html.erb @@ -3,26 +3,26 @@ <% 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™' %> + <%= 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 %> -
+
<%= site_bar_switch_link -%> <%= site_bar_edit_link -%>
-
- +
+ <%= 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') %>:

    <% if defined?(include_object_name) and include_object_name %> - <% object.errors.full_messages.each do |value| %> -
  • <%= value %>
  • + <% object.errors.each do |error| %> +
  • <%= error.message %>
  • <% end %> <% else %> - <% object.errors.each do |key, value| %> -
  • <%= value %>
  • + <% object.errors.each do |error| %> +
  • <%= error.message %>
  • <% 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 9bc5d4d2360..7738a921af6 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..9fc630ec862 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_code}' + edit_in_language: Edit in %{language} resource_picker: download_current: Download current file opens_in_new_window: Opens in a new window diff --git a/core/lib/generators/refinery/cms/cms_generator.rb b/core/lib/generators/refinery/cms/cms_generator.rb index b4a86e645d1..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' @@ -5,18 +7,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 +57,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 +217,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 +255,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,7 +331,7 @@ 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] + self.old_pretend = self.options[:pretend] new_options = self.options.dup new_options[:pretend] = true self.options = new_options @@ -340,10 +342,12 @@ def stop_pretending? # Stop pretending if destination_path == Refinery.root say_status :'-- finished pretending --', nil, :yellow - new_options = self.options.dup + 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/core/lib/generators/refinery/dummy/dummy_generator.rb b/core/lib/generators/refinery/dummy/dummy_generator.rb index 80a9556f69b..805ba3ba11b 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,16 @@ 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/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 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..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 @@ -10,7 +17,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 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/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/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/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' diff --git a/core/lib/refinery/core/engine.rb b/core/lib/refinery/core/engine.rb index ae0b04c84bb..efe00dce909 100644 --- a/core/lib/refinery/core/engine.rb +++ b/core/lib/refinery/core/engine.rb @@ -45,11 +45,22 @@ 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 end end diff --git a/core/lib/refinery/crud.rb b/core/lib/refinery/crud.rb index a94b86ecda5..38bf00a2563 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,14 @@ def crudify(model_name, options = {}) class_name = options[:class_name] singular_name = options[:singular_name] plural_name = options[:plural_name] + actions = [*options[:find_actions]] - [*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] + prepend_before_action :find_#{singular_name}, only: #{actions} prepend_before_action :merge_position_into_params!, :only => :create def new 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/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/refinerycms-core.gemspec b/core/refinerycms-core.gemspec index fa50e66c16b..28bba0c4fab 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 = ['>= 5.2.0', '< 7'] +rails_version = ['>= 6.1.0', '< 9'] Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY @@ -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] @@ -24,11 +24,11 @@ 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' - 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' diff --git a/core/spec/helpers/refinery/translation_helper_spec.rb b/core/spec/helpers/refinery/translation_helper_spec.rb index 10bbde9634b..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,7 @@ module Refinery before do Mobility.with_locale(:en) do - page.title = "draft" - page.save! + page.update!({title: 'draft'}) end Mobility.with_locale(:lv) do @@ -25,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/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/presenters/refinery/translated_field_presenter_spec.rb b/core/spec/presenters/refinery/translated_field_presenter_spec.rb index bad639bee28..da9136b8c20 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..9a8aa9da8fb 100644 --- a/core/spec/system/refinery/admin/xhr_paging_spec.rb +++ b/core/spec/system/refinery/admin/xhr_paging_spec.rb @@ -2,17 +2,21 @@ module Refinery describe "Crudify", type: :system do + before do + driven_by(:selenium_chrome_headless) + end refinery_login describe "xhr_paging", :js do # 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) + allow(Refinery::Images).to receive(:preferred_image_view).and_return(:grid) end it 'performs ajax paging of index' do @@ -37,5 +41,4 @@ module Refinery end end end - end diff --git a/core/spec/system/refinery/site_bar_spec.rb b/core/spec/system/refinery/site_bar_spec.rb index 7c5add0ecc9..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 @@ -10,10 +12,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 @@ -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/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/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/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..06856f15d2c --- /dev/null +++ b/images/app/presenters/refinery/admin/image_presenter.rb @@ -0,0 +1,145 @@ +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 + delegate :t, to: :context + + 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 + 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 = {}) + 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/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/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/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..d1ccdb6cfc7 --- /dev/null +++ b/images/app/views/refinery/admin/images/_image.html.erb @@ -0,0 +1,5 @@ +
  • + <%= image.edit_link %> + <%= image.text_elements %> + <%= image.actions %> +
  • 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_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_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_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', 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/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/controllers/refinery/admin/pages_controller.rb b/pages/app/controllers/refinery/admin/pages_controller.rb index f94c708d490..02238c9fbb3 100644 --- a/pages/app/controllers/refinery/admin/pages_controller.rb +++ b/pages/app/controllers/refinery/admin/pages_controller.rb @@ -1,17 +1,18 @@ 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, + exclude_from_find: [:show] 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 +80,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/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/helpers/refinery/admin/pages_helper.rb b/pages/app/helpers/refinery/admin/pages_helper.rb index ae862744fb4..5e1bb50ca97 100644 --- a/pages/app/helpers/refinery/admin/pages_helper.rb +++ b/pages/app/helpers/refinery/admin/pages_helper.rb @@ -3,22 +3,26 @@ 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]} + 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 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 @@ -52,7 +56,25 @@ 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 = ['icon'] + if number_of_children.zero? + classes.push icon_class('page') + 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') + end + + tag.span(class: classes.join(' '), title: title) end end end 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/app/views/refinery/admin/pages/_form.html.erb b/pages/app/views/refinery/admin/pages/_form.html.erb index ca2562046c4..83d8e5b8093 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 %> @@ -64,7 +64,7 @@ action: prev_url, target: prev_target }); - }, 100); + }, 500); }); }); diff --git a/pages/app/views/refinery/admin/pages/_page.html.erb b/pages/app/views/refinery/admin/pages/_page.html.erb index efc2011519c..d664066c1f4 100644 --- a/pages/app/views/refinery/admin/pages/_page.html.erb +++ b/pages/app/views/refinery/admin/pages/_page.html.erb @@ -1,49 +1,21 @@ -<% # 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))} - } - %> +<% edit_url = refinery.admin_edit_page_path(page.nested_url) %> -
  • -
    - <% if page.children.present? %> - - - <% else %> - - <% end %> +
  • +
    + <%= 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, locales_with_translated_field(page, 'title', include_current: false)) %> - '> - <%= translated_field(page, :title) %> - <%= 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 %> - -
    - <%= locale_text_icon(translation.locale.upcase) %> -
    - <% end %> - <% end %> - <% end %> -
    - <% 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, 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..47b01f635e6 100644 --- a/pages/config/locales/en.yml +++ b/pages/config/locales/en.yml @@ -32,15 +32,15 @@ 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 + new: Add a new child page view_live_html: View this page live
    (opens in a new window) form: preview: Preview diff --git a/pages/lib/refinery/pages/finder.rb b/pages/lib/refinery/pages/finder.rb index f32af40a977..e58dbbea164 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..5f7980ff801 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.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/pages/spec/controllers/refinery/pages_controller_spec.rb b/pages/spec/controllers/refinery/pages_controller_spec.rb index 2e9d86a4df8..b2b5083c954 100644 --- a/pages/spec/controllers/refinery/pages_controller_spec.rb +++ b/pages/spec/controllers/refinery/pages_controller_spec.rb @@ -3,13 +3,13 @@ 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 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") 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..46186152275 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 @@ -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/support/selector_helpers.rb b/pages/spec/support/selector_helpers.rb new file mode 100644 index 00000000000..fd3a0bf9084 --- /dev/null +++ b/pages/spec/support/selector_helpers.rb @@ -0,0 +1,43 @@ +# 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 diff --git a/pages/spec/system/refinery/admin/pages_spec.rb b/pages/spec/system/refinery/admin/pages_spec.rb index fb13b476563..144b44cec2d 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}") + expect(page).to have_selector("#switch_locale_picker a.selected##{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,15 +87,14 @@ 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 allow(Refinery::Pages).to receive(:auto_expand_admin_tree).and_return(false) - visit refinery.admin_pages_path end @@ -111,15 +109,15 @@ module Admin end it "expands children", js: true do - find("#page_#{company.id} .title.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} .title.toggle").click - find("#page_#{locations.id} .title.toggle").click + find("#page_#{company.id} .icon.toggle").click + find("#page_#{locations.id} .icon.toggle").click expect(page).to have_content("New York") end @@ -127,9 +125,8 @@ module Admin context "with auto expand option turned on" do before do - allow(Refinery::Pages).to receive(:auto_expand_admin_tree).and_return(true) + Refinery::Pages.auto_expand_admin_tree = true Rails.cache.clear - visit refinery.admin_pages_path end @@ -138,26 +135,26 @@ 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 + describe " new / create " do + it " Creates a page ", 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" expect { click_button "Save" }.to change(Refinery::Page, :count).from(0).to(1) expect(page).to have_content("'My first page' was successfully added.") @@ -174,11 +171,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 @@ -192,13 +189,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.") @@ -218,9 +215,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.") @@ -229,10 +226,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 @@ -262,12 +259,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" @@ -281,7 +278,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" @@ -290,11 +287,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 @@ -302,7 +299,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" @@ -311,7 +308,7 @@ module Admin expect_window_with_content( preview_content, window: window - ) + ) window.close @@ -322,8 +319,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") @@ -333,9 +330,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 @@ -345,8 +342,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" @@ -354,23 +351,26 @@ 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 + # 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") 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? @@ -380,11 +380,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 @@ -397,29 +397,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 @@ -435,7 +435,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 @@ -447,12 +447,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-.+?}) @@ -462,12 +462,14 @@ module Admin context "with translations" do before do allow(Refinery::I18n).to receive(:frontend_locales).and_return([:en, :ru]) + 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', - :link_url => '/', - :menu_match => "^/$" + Page.create title: 'Home', + link_url: '/', + menu_match: "^/$" end Mobility.with_locale(:ru) do @@ -476,11 +478,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 @@ -489,24 +491,21 @@ module Admin expect(Refinery::Page.count).to eq(2) end - it "shows locale flag 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') + 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.find('a[tooltip="Edit this page"]')[:href]).to include('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 @@ -520,7 +519,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 @@ -531,17 +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 - allow(Refinery::I18n).to receive(:frontend_locales).and_return([:en, :ru]) - - _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 @@ -551,7 +545,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") @@ -560,20 +554,23 @@ 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.") 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_css('.locale_marker', count: 2) - expect(page).to have_content('EN') - expect(page).to have_content('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 @@ -589,7 +586,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 @@ -610,10 +607,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 } @@ -630,7 +627,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.") @@ -639,13 +636,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 @@ -655,9 +652,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 @@ -675,23 +672,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 @@ -709,7 +706,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 @@ -720,12 +717,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 @@ -772,10 +769,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 @@ -798,7 +795,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], @@ -809,17 +806,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 @@ -827,13 +824,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.") @@ -847,7 +844,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 @@ -860,7 +857,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 @@ -870,14 +867,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']") @@ -888,17 +885,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 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 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/refinerycms.gemspec b/refinerycms.gemspec index 6a88a5b7a55..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'] @@ -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/app/controllers/refinery/admin/resources_controller.rb b/resources/app/controllers/refinery/admin/resources_controller.rb index 4c8c2a45682..1700aaf0407 100644 --- a/resources/app/controllers/refinery/admin/resources_controller.rb +++ b/resources/app/controllers/refinery/admin/resources_controller.rb @@ -1,9 +1,11 @@ module Refinery module Admin class ResourcesController < ::Refinery::AdminController + helper Refinery::Admin::ResourceHelper crudify :'refinery/resource', include: [:translations], + exclude_from_find: :show, order: "updated_at DESC", sortable: false @@ -62,9 +64,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 86c16b44d54..5225e6e623c 100644 --- a/resources/app/views/refinery/admin/resources/_resource.html.erb +++ b/resources/app/views/refinery/admin/resources/_resource.html.erb @@ -1,33 +1,15 @@ <% + # 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: translated_field(resource, :title))}} + translated_locales = locales_with_translated_field(resource, 'resource_title') %> -
  • - - - <%= 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) %> - +
  • + <%= 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)) %> 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..ff647bf9080 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') @@ -97,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 be passed form parameters' do - params = { file: [file, file, file], fake_param: 'blah' } + specify 'each returned array item should should be processed with other form parameters' do + 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/resources/spec/system/refinery/admin/resources_spec.rb b/resources/spec/system/refinery/admin/resources_spec.rb index 65bc50b01ab..b27a3f0d55a 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 @@ -21,17 +32,12 @@ 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 - - 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 + def uploading_a_file + visit refinery.admin_resources_path + find('a', text: 'Upload new file').click + 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 +45,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,8 +53,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')) end @@ -65,11 +70,11 @@ module Admin 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 + within '#file' do expect(page).to have_selector('a[tooltip="The maximum file size is 1.2 KB."]') end end @@ -80,12 +85,12 @@ module Admin 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_selector('a[tooltip="Filen må maksimalt fylde 1,2 kB."]') end end end @@ -125,7 +130,7 @@ module Admin click_link 'Edit this file' within '#switch_locale_picker' do - click_link 'FR' + click_link 'fr' end fill_in 'Title', with: 'Premier fichier' @@ -138,16 +143,34 @@ module Admin 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/spec/spec_helper.rb b/spec/spec_helper.rb index ffb8cbb115c..07c4561f04b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,18 +7,10 @@ # 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' -require 'capybara/rspec' -require 'webdrivers/chromedriver' -require 'falcon/capybara' -Capybara.server = :falcon if ENV['RETRY_COUNT'] require 'rspec/retry' @@ -41,6 +33,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 @@ -58,11 +51,11 @@ end config.before(:each, type: :system) do - driven_by :rack_test + driven_by :selenium_chrome_headless end config.before(:each, type: :system, js: true) do - driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1080] + driven_by :selenium_chrome_headless, using: :headless_chrome, screen_size: [1400, 1080] end unless ENV['FULL_BACKTRACE'] diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb new file mode 100644 index 00000000000..37c2704b621 --- /dev/null +++ b/spec/support/capybara.rb @@ -0,0 +1,62 @@ +require 'rack/test' +require 'rspec/rails' +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 + options.add_argument("--window-size=1400,1080") + + Capybara::Selenium::Driver.new(app, browser: :firefox, options: options) +end + +Capybara.register_driver :local_selenium_headless do |app| + 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: :firefox, 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.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| + 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/spec/support/refinery/generator_spec_describer.rb b/spec/support/refinery/generator_spec_describer.rb new file mode 100644 index 00000000000..6d4564620f9 --- /dev/null +++ b/spec/support/refinery/generator_spec_describer.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module GeneratorSpec + module Matcher + class Directory + def description + 'match a directory structure' + end + end + + class File + def description + 'match a file' + end + end + end +end diff --git a/testing/refinerycms-testing.gemspec b/testing/refinerycms-testing.gemspec index 908cafcb7f1..24ccdf94149 100644 --- a/testing/refinerycms-testing.gemspec +++ b/testing/refinerycms-testing.gemspec @@ -19,11 +19,11 @@ 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', '~> 4.0.0.beta2' - s.add_dependency 'webdrivers', '~> 4.0' + s.add_dependency 'rspec-rails', '~> 6.1' + s.add_dependency 'selenium-webdriver', '>= 2.46.0' s.required_ruby_version = Refinery::Version.required_ruby_version