diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..95633f4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,91 @@ +name: CI +on: + pull_request: + push: + branches: [master] + +permissions: + contents: read + pages: write + id-token: write + +jobs: + test: + name: Ruby ${{ matrix.ruby }} / Rails ${{ matrix.rails }} / AA ${{ matrix.activeadmin }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ruby: ['3.2', '3.3', '3.4'] + rails: ['7.1.0', '7.2.0', '8.0.0'] + activeadmin: ['3.2.0', '3.3.0', '3.4.0', '3.5.0'] + exclude: + - rails: '8.0.0' + activeadmin: '3.2.0' + env: + RAILS: ${{ matrix.rails }} + AA: ${{ matrix.activeadmin }} + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - name: Run tests + run: bundle exec rspec spec + + - name: Generate badge.json + if: matrix.ruby == '3.4' && matrix.rails == '8.0.0' && matrix.activeadmin == '3.5.0' + run: | + LAST_RUN="coverage/.last_run.json" + if [ ! -f "$LAST_RUN" ]; then + mkdir -p badge + echo '{"schemaVersion":1,"label":"coverage","message":"unknown","color":"lightgrey"}' > badge/badge.json + exit 0 + fi + PERCENT=$(ruby -rjson -e "puts JSON.parse(File.read('$LAST_RUN')).dig('result','line').round(1)") + PERCENT_NUM=$(ruby -rjson -e "puts JSON.parse(File.read('$LAST_RUN')).dig('result','line')") + if ruby -e "exit(($PERCENT_NUM >= 90) ? 0 : 1)"; then COLOR="brightgreen" + elif ruby -e "exit(($PERCENT_NUM >= 75) ? 0 : 1)"; then COLOR="green" + elif ruby -e "exit(($PERCENT_NUM >= 60) ? 0 : 1)"; then COLOR="yellow" + else COLOR="red"; fi + mkdir -p badge + echo "{\"schemaVersion\":1,\"label\":\"coverage\",\"message\":\"${PERCENT}%\",\"color\":\"${COLOR}\"}" > badge/badge.json + + - name: Upload badge artifact + if: matrix.ruby == '3.4' && matrix.rails == '8.0.0' && matrix.activeadmin == '3.5.0' + uses: actions/upload-artifact@v4 + with: + name: coverage-badge + path: badge + + deploy-coverage: + needs: test + if: github.ref == 'refs/heads/master' && github.event_name == 'push' + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - uses: actions/checkout@v4 + with: + ref: gh-pages + + - name: Download coverage badge + uses: actions/download-artifact@v4 + with: + name: coverage-badge + path: . + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: . + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f8a9240 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/.bundle/ +/Gemfile.lock +/pkg/ +/tmp/ +spec/rails/rails-* +/coverage diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 210ac46..0000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -sudo: required - -language: ruby - -addons: - chrome: stable - -before_install: - - gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true - - gem install bundler -v '< 2' - -script: bundle exec rspec - -env: - matrix: - - RAILS=5.1.6 AA=1.1.0 - - RAILS=5.2.1 AA=1.4.0 - - RAILS=5.2.2 AA=2.8.0 - - RAILS=6.0.0 AA=2.8.0 - -rvm: - - 2.5 - - 2.6 diff --git a/Gemfile b/Gemfile index d98c914..165e561 100644 --- a/Gemfile +++ b/Gemfile @@ -1,18 +1,20 @@ source 'https://rubygems.org' - -# Specify your gem's dependencies in activeadmin_scoped_collection_actions.gemspec gemspec -group :test do - gem 'rails', "~> #{ENV['RAILS'] || '5.2.1'}" - gem 'activeadmin', "~> #{ENV['AA'] || '1.3.1'}" +default_rails_version = '7.1.0' +default_activeadmin_version = '3.2.0' +gem 'rails', "~> #{ENV['RAILS'] || default_rails_version}" +gem 'activeadmin', "~> #{ENV['AA'] || default_activeadmin_version}" +gem 'sprockets-rails' +gem 'sass-rails' + +group :test do + gem 'simplecov', require: false gem 'rspec-rails' - gem 'sqlite3' + gem 'sqlite3', '~> 2.0' gem 'database_cleaner' gem 'capybara' - gem 'selenium-webdriver' - gem 'chromedriver-helper' - gem 'byebug' - gem 'sassc-rails' + gem 'cuprite' + gem 'webrick', require: false end diff --git a/README.md b/README.md index 700754c..9ccfe6d 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,35 @@ [![Gem Version](https://badge.fury.io/rb/active_admin_sidebar.svg)](https://badge.fury.io/rb/active_admin_sidebar) [![NPM Version](https://badge.fury.io/js/@activeadmin-plugins%2Factive_admin_sidebar.svg)](https://badge.fury.io/js/@activeadmin-plugins%2Factive_admin_sidebar) ![npm](https://img.shields.io/npm/dm/@activeadmin-plugins/active_admin_sidebar) +![Coverage](https://img.shields.io/endpoint?url=https://activeadmin-plugins.github.io/active_admin_sidebar/badge.json) # ActiveAdmin Sidebar -Provides ability to manipulate sidebar position for ActiveAdmin (tested with ActiveAdmin ~> 1.0.0) +Manipulate sidebar position and add collapsible sidebar support for ActiveAdmin 3.x. ## Install -Add following line to the `Gemfile` +Add to your `Gemfile`: ```ruby gem 'active_admin_sidebar' ``` -##### Using assets via Sprockets -Add following line to the `app/assets/stylesheets/active_admin.css.scss` +### Using assets via Sprockets + +Add to `app/assets/stylesheets/active_admin.scss`: ```scss - @import "active_admin_sidebar"; +@import "active_admin_sidebar"; ``` -If you want to use collapsing feature, add following line +Add to `app/assets/javascripts/active_admin.js`: -```javascripts - //= require active_admin_sidebar +```javascript +//= require active_admin_sidebar ``` -to the `app/assets/javascripts/active_admin.js` - -##### Using assets via Webpacker (or any other assets bundler) as a NPM module (Yarn package) - -Execute: +### Using assets via NPM $ npm i @activeadmin-plugins/active_admin_sidebar @@ -39,81 +37,62 @@ Or $ yarn add @activeadmin-plugins/active_admin_sidebar -Or add manually to `package.json`: - -```json -"dependencies": { - "@activeadmin-plugins/active_admin_sidebar": "2.0.0" -} -``` -and execute: - - $ yarn - -Add the following line into `app/assets/javascripts/active_admin.js`: +Add to `app/assets/javascripts/active_admin.js`: ```javascript import '@activeadmin-plugins/active_admin_sidebar'; ``` -Add the following line into `app/assets/stylesheets/active_admin.scss`: +Add to `app/assets/stylesheets/active_admin.scss`: -```css +```scss @import '@activeadmin-plugins/active_admin_sidebar'; ``` -# Configuration per resource +## Configuration per resource -Changing sidebar position dynamically with before_action +Change sidebar position with `before_action`: ```ruby - # app/admin/posts.rb - ActiveAdmin.register Post do - before_action :left_sidebar!, only: [:show] +# app/admin/posts.rb +ActiveAdmin.register Post do + before_action only: [:index] do + left_sidebar! end +end - # app/admin/comments.rb - ActiveAdmin.register Comment do - before_action :right_sidebar! +# app/admin/comments.rb +ActiveAdmin.register Comment do + before_action do + right_sidebar! end +end ``` ## Global configuration -Moving sidebar to the left within all resource. Set configuration in `config/initializers/active_admin.rb` +Move sidebar to the left for all resources in `config/initializers/active_admin.rb`: ```ruby - # == Controller before-actions - # - # You can add before, after and around actions to all of your resources - ActiveAdmin.setup do |config| - config.before_action do - left_sidebar! if respond_to?(:left_sidebar!) - end +ActiveAdmin.setup do |config| + config.before_action do + left_sidebar! if respond_to?(:left_sidebar!) end +end ``` -## Collapsing sidebar +## Collapsible sidebar -You can use sidebar collapsing. -It will add "hide/show" button. Shown/Hidden state is persisted across all pages. +Add a toggle button to collapse/expand the sidebar. State is persisted per-resource across page navigations. ```ruby - left_sidebar!(collapsed: true) -``` +# Collapsible sidebar (starts expanded) +left_sidebar!(collapsible: true) +right_sidebar!(collapsible: true) -You can override button color according to your color theme. For example: - -```scss - body.active_admin { - #active_admin_content.left_sidebar, #active_admin_content.collapsed_sidebar { - .collapse_btn, .uncollapse_btn { - background-color: #767270; - } - } - } +# Collapsible sidebar (starts collapsed) +left_sidebar!(collapsible: true, start_collapsed: true) +right_sidebar!(collapsible: true, start_collapsed: true) ``` -Example - -![Alt text](https://raw.githubusercontent.com/activeadmin-plugins/active_admin_sidebar/master/screen/sidebar.jpg "Example") +![Demo](https://activeadmin-plugins.github.io/active_admin_sidebar/demo.gif "Collapsible sidebar demo") diff --git a/Rakefile b/Rakefile index a5cc536..dedca81 100644 --- a/Rakefile +++ b/Rakefile @@ -2,6 +2,4 @@ require "bundler" require 'rake' Bundler.setup Bundler::GemHelper.install_tasks - -# Import all our rake tasks FileList['tasks/**/*.rake'].each { |task| import task } diff --git a/active_admin_sidebar.gemspec b/active_admin_sidebar.gemspec index 7db8dd3..353c8f5 100644 --- a/active_admin_sidebar.gemspec +++ b/active_admin_sidebar.gemspec @@ -1,4 +1,3 @@ -# -*- encoding: utf-8 -*- $:.push File.expand_path("../lib", __FILE__) require "active_admin_sidebar/version" @@ -7,15 +6,17 @@ Gem::Specification.new do |s| s.version = ActiveAdminSidebar::VERSION s.authors = ["Igor"] s.email = ["fedoronchuk@gmail.com"] - s.homepage = "https://github.com/Fivell/active_admin_sidebar" + s.homepage = "https://github.com/activeadmin-plugins/active_admin_sidebar" s.summary = %q{active_admin_sidebar gem} s.description = %q{extension for activeadmin gem to manage sidebar} + s.license = "MIT" - s.add_dependency "activeadmin" + s.required_ruby_version = '>= 3.1.0' s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } s.require_paths = ["lib"] + s.add_dependency "activeadmin", ">= 3.0", "< 4.0" end diff --git a/app/assets/javascripts/active_admin_sidebar.js b/app/assets/javascripts/active_admin_sidebar.js index 070d9f3..fc66fcd 100644 --- a/app/assets/javascripts/active_admin_sidebar.js +++ b/app/assets/javascripts/active_admin_sidebar.js @@ -1,29 +1,29 @@ (function() { $(document).ready(function() { - var $aa_content, set_collapsed_sidebar; if ($('body').hasClass('index') && ($('#active_admin_content').hasClass('collapsible_sidebar'))) { - $aa_content = $('#active_admin_content'); - $aa_content.find('.sidebar_section:first>h3').append(''); - $aa_content.prepend(''); - set_collapsed_sidebar = function(value) { + var $aa_content = $('#active_admin_content'); + var $sidebar = $aa_content.find('#sidebar'); + + var $toggleBtn = $(''); + $sidebar.prepend($toggleBtn); + + var set_collapsed_sidebar = function(value) { return $.getJSON(this.href, { collapsed_sidebar: value }); }; - return $aa_content.on('click', '.collapse_btn, .uncollapse_btn', function(e) { + + $toggleBtn.on('click', function(e) { if (!$aa_content.hasClass('collapsed_sidebar')) { set_collapsed_sidebar(true); - $aa_content.removeClass('left_sidebar'); $aa_content.addClass('collapsed_sidebar'); - return $aa_content.trigger('collapsible_sidebar:collapsed'); + $aa_content.trigger('collapsible_sidebar:collapsed'); } else { set_collapsed_sidebar(false); $aa_content.removeClass('collapsed_sidebar'); - $aa_content.addClass('left_sidebar'); - return $aa_content.trigger('collapsible_sidebar:uncollapsed'); + $aa_content.trigger('collapsible_sidebar:uncollapsed'); } }); } }); - }).call(this); diff --git a/app/assets/stylesheets/active_admin_sidebar.scss b/app/assets/stylesheets/active_admin_sidebar.scss index 112a1e8..878bb60 100644 --- a/app/assets/stylesheets/active_admin_sidebar.scss +++ b/app/assets/stylesheets/active_admin_sidebar.scss @@ -1,7 +1,11 @@ -@import "active_admin_sidebar_pure_icons"; +$toggle-btn-width: 12px; +$toggle-btn-height: 48px; +// === Left sidebar layout === +// Moves sidebar before main content in the DOM, so we need to +// reverse the default AA float layout. body.active_admin { - #active_admin_content.left_sidebar, #active_admin_content.collapsed_sidebar { + #active_admin_content.left_sidebar { #sidebar { display: block; margin-left: 0; @@ -25,67 +29,119 @@ body.active_admin { float: inherit; margin-left: 298px; width: auto; + #main_content { float: inherit; margin: 0; - .tabs .comments { - .active_admin_comment { - clear: none; - } + + .tabs .comments .active_admin_comment { + clear: none; } } - } .table_tools:after { clear: none; padding-bottom: 16px; } + } +} - .collapse_btn, .uncollapse_btn { - background-color: #767270; - border-radius: 5px; - color: #ffffff; - cursor: pointer; - } +// === Collapsible sidebar: toggle button === +body.active_admin #active_admin_content.collapsible_sidebar { + #sidebar { + position: relative; + } - .collapse_btn { - clear: both; - display: block; - float: right; + .sidebar_toggle_btn { + width: $toggle-btn-width; + height: $toggle-btn-height; + background-color: #767270; + border-radius: 3px; + cursor: pointer; + z-index: 10; + position: absolute; + top: 12px; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + background-color: #5a5857; } - .uncollapse_btn { - display: none; - margin-top: 5px; - position: absolute; + .chevron { + width: 0; + height: 0; + border-top: 4px solid transparent; + border-bottom: 4px solid transparent; } + } + + // Left sidebar expanded: button on right edge, chevron points left (collapse) + &.left_sidebar .sidebar_toggle_btn { + right: -$toggle-btn-width; + .chevron { border-right: 5px solid #fff; border-left: none; } + } + + // Left sidebar collapsed: chevron points right (expand) + &.left_sidebar.collapsed_sidebar .sidebar_toggle_btn { + .chevron { border-left: 5px solid #fff; border-right: none; } + } + + // Right sidebar expanded: button on left edge, chevron points right (collapse) + &:not(.left_sidebar) .sidebar_toggle_btn { + left: -$toggle-btn-width; + .chevron { border-left: 5px solid #fff; border-right: none; } + } + // Right sidebar collapsed: chevron points left (expand) + &:not(.left_sidebar).collapsed_sidebar .sidebar_toggle_btn { + .chevron { border-right: 5px solid #fff; border-left: none; } } } -body.active_admin #active_admin_content.collapsed_sidebar { +// === Collapsed state: left sidebar === +body.active_admin #active_admin_content.collapsible_sidebar.collapsed_sidebar.left_sidebar { + #sidebar { + width: 0; + overflow: visible; + + .sidebar_section { + display: none; + } + } #main_content_wrapper { - margin-left: 30px; + margin-left: $toggle-btn-width + 8px; } +} +// === Collapsed state: right sidebar (default AA position) === +body.active_admin #active_admin_content.collapsible_sidebar.collapsed_sidebar:not(.left_sidebar) { #sidebar { - display: none; - } + width: 0; + margin-left: 0; + overflow: visible; - .uncollapse_btn { - display: block; + .sidebar_section { + display: none; + } } + #main_content_wrapper #main_content { + margin-right: $toggle-btn-width + 8px; + } } -body.active_admin.index #active_admin_content.with_sidebar.collapsible_sidebar { +// === Left sidebar expanded + collapsible: fix margin === +body.active_admin.index #active_admin_content.with_sidebar.left_sidebar.collapsible_sidebar { #main_content_wrapper #main_content { margin-right: 0; } } +// === Comments overflow fix === .with_sidebar .comments .active_admin_comment { overflow: auto; } diff --git a/app/assets/stylesheets/active_admin_sidebar_pure_icons.scss b/app/assets/stylesheets/active_admin_sidebar_pure_icons.scss deleted file mode 100644 index ed4e085..0000000 --- a/app/assets/stylesheets/active_admin_sidebar_pure_icons.scss +++ /dev/null @@ -1,66 +0,0 @@ -/* .icono-caret-left */ -.icono-caret-right, .icono-caret-left { - height: 19px; - width: 19px; -} - -.icono-caret-right:before, -.icono-caret-right:after, -.icono-caret-left:before, -.icono-caret-left:after { - bottom: 1px; - box-shadow: inset 0 0 0 32px; - height: 2px; - margin: auto 0; - position: absolute; - right: 6px; - transform-origin: right; - width: 8px; -} - -.icono-caret-right:before, .icono-caret-left:before { - top: 2px; - -moz-transform: rotate(45deg); - -ms-transform: rotate(45deg); - -o-transform: rotate(45deg); - -webkit-transform: rotate(45deg); - transform: rotate(45deg); -} - -.icono-caret-right:after, .icono-caret-left:after { - top: 0; - -moz-transform: rotate(-45deg); - -ms-transform: rotate(-45deg); - -o-transform: rotate(-45deg); - -webkit-transform: rotate(-45deg); - transform: rotate(-45deg); -} - -.icono-caret-left { - -moz-transform: rotate(180deg); - -ms-transform: rotate(180deg); - -o-transform: rotate(180deg); - -webkit-transform: rotate(180deg); - transform: rotate(180deg); -} - -[class*="icono-"] { - direction: ltr; - display: inline-block; - font-style: normal; - position: relative; - text-align: left; - text-indent: -9999px; - vertical-align: middle; -} - -[class*="icono-"]:before, -[class*="icono-"]:after { - content: ""; - pointer-events: none; -} - -[class*="icono-"], -[class*="icono-"] * { - box-sizing: border-box; -} diff --git a/lib/active_admin_sidebar/activeadmin_views_pages_base.rb b/lib/active_admin_sidebar/activeadmin_views_pages_base.rb index 6ccf2d9..7d0f337 100644 --- a/lib/active_admin_sidebar/activeadmin_views_pages_base.rb +++ b/lib/active_admin_sidebar/activeadmin_views_pages_base.rb @@ -1,6 +1,9 @@ module ActiveAdminSidebar::ActiveAdminViewsPagesBase def build_page_content + # When no sidebar options are set, fall back to vanilla ActiveAdmin behavior + return super unless sidebar_options_present? + build_flash_messages div id: "active_admin_content", class: main_content_classes do build_sidebar unless skip_sidebar? || right_sidebar? @@ -17,12 +20,16 @@ def build_sidebar end end + def sidebar_options_present? + assigns[:sidebar_options].present? + end + def left_sidebar? assigns[:sidebar_options].try!(:[], :position) == :left end def collapsible_sidebar? - left_sidebar? && !!assigns[:sidebar_options].try!(:[], :collapsed) + !!assigns[:sidebar_options].try!(:[], :collapsible) end def sidebar_is_collapsed? @@ -40,6 +47,7 @@ def main_content_classes else classes << "with_sidebar" classes << "left_sidebar" if left_sidebar? + classes << "right_sidebar" if right_sidebar? if collapsible_sidebar? classes << "collapsible_sidebar" classes << "collapsed_sidebar" if sidebar_is_collapsed? diff --git a/lib/active_admin_sidebar/positions.rb b/lib/active_admin_sidebar/positions.rb index 3bddb25..fa29fb4 100644 --- a/lib/active_admin_sidebar/positions.rb +++ b/lib/active_admin_sidebar/positions.rb @@ -2,26 +2,56 @@ module ActiveAdminSidebar module Positions def left_sidebar!(options = {}) @sidebar_options = { position: :left } - if options.fetch(:collapsed, false) - collapsed_sidebar - @sidebar_options.merge!( - is_collapsed: session[:collapsed_sidebar], - collapsed: true - ) - end + + validate_sidebar_options!(options) + + collapsible = options.fetch(:collapsible, false) + start_collapsed = options.fetch(:start_collapsed, false) + + apply_collapsible_options(start_collapsed) if collapsible end - def right_sidebar! + def right_sidebar!(options = {}) @sidebar_options = { position: :right } + + validate_sidebar_options!(options) + + collapsible = options.fetch(:collapsible, false) + start_collapsed = options.fetch(:start_collapsed, false) + + apply_collapsible_options(start_collapsed) if collapsible + end + + private + + def validate_sidebar_options!(options) + if options.key?(:collapsed) + raise ArgumentError, + "The :collapsed option has been removed in v3. " \ + "Use `collapsible: true, start_collapsed: true` instead." + end + end + + def apply_collapsible_options(start_collapsed) + handle_sidebar_toggle_request + toggled = (session[:aas_toggled] || []).include?(controller_name) + is_collapsed = toggled ? !start_collapsed : start_collapsed + @sidebar_options.merge!( + collapsible: true, + is_collapsed: is_collapsed + ) end - def collapsed_sidebar - if request.xhr? - if params[:collapsed_sidebar].present? - collapsed = params[:collapsed_sidebar].to_s == 'true' - session[:collapsed_sidebar] = collapsed - render json: { collapsed_sidebar: collapsed } and return + def handle_sidebar_toggle_request + if request.xhr? && params[:collapsed_sidebar].present? + toggled = session[:aas_toggled] || [] + if toggled.include?(controller_name) + toggled -= [controller_name] + else + toggled += [controller_name] end + session[:aas_toggled] = toggled + render json: { collapsed_sidebar: params[:collapsed_sidebar] } and return end end diff --git a/lib/active_admin_sidebar/version.rb b/lib/active_admin_sidebar/version.rb index 03a392b..47e21ef 100644 --- a/lib/active_admin_sidebar/version.rb +++ b/lib/active_admin_sidebar/version.rb @@ -1,3 +1,3 @@ module ActiveAdminSidebar - VERSION = '2.0.0'.freeze + VERSION = '3.0.0'.freeze end diff --git a/package.json b/package.json index 4f6115c..566b5e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@activeadmin-plugins/active_admin_sidebar", - "version": "2.0.0", + "version": "3.0.0", "description": "Extension for ActiveAdmin gem to manage sidebar", "main": "src/active_admin_sidebar.js", "style": "src/active_admin_sidebar.scss", diff --git a/screen/sidebar.jpg b/screen/sidebar.jpg deleted file mode 100644 index cd96484..0000000 Binary files a/screen/sidebar.jpg and /dev/null differ diff --git a/spec/sidebars_spec.rb b/spec/sidebars_spec.rb index e16f297..203884e 100644 --- a/spec/sidebars_spec.rb +++ b/spec/sidebars_spec.rb @@ -5,49 +5,184 @@ before do Author.create!(name: 'John', last_name: 'Doe') Author.create!(name: 'Jane', last_name: 'Roe') - add_author_resource - add_post_resource end - context 'left-sidebar with default settings' do + context 'left-sidebar with collapsible: true (starts expanded)' do before do + add_author_resource + add_post_resource visit '/admin/authors' end - it 'has left-sidebar with colapse-button' do + it 'has left-sidebar with toggle button' do expect(page).to have_css('#filters_sidebar_section') - expect(page).to have_css('#filters_sidebar_section .collapse_btn.icono-caret-left') + expect(page).to have_css('.sidebar_toggle_btn') expect(page).to have_css('#active_admin_content.with_sidebar.left_sidebar.collapsible_sidebar') end - context 'when click on Collapse' do + it 'sidebar starts expanded' do + expect(page).to have_css('#sidebar .sidebar_section', visible: :visible) + expect(page).not_to have_css('#active_admin_content.collapsed_sidebar') + end + + context 'when click on toggle to collapse' do before do - page.find('#filters_sidebar_section .collapse_btn').click + page.find('.sidebar_toggle_btn').click end it "sidebar is hidden, and save it's state after going to another page" do - expect(page).to have_css('#sidebar', visible: :hidden) + expect(page).to have_css('#active_admin_content.collapsed_sidebar') # Posts page is configured as: "before_action :skip_sidebar!" visit '/admin/posts' - # sidebar does not exists at all + # sidebar does not exist at all expect(page).to have_css('#page_title', text: 'Posts') expect(page).not_to have_css('#sidebar', visible: :all) visit '/admin/authors' - # sidebar is hidden + # sidebar is collapsed expect(page).to have_css('#page_title', text: 'Authors') - expect(page).to have_css('#sidebar', visible: :hidden) + expect(page).to have_css('#active_admin_content.collapsed_sidebar') - page.find('.uncollapse_btn').click + page.find('.sidebar_toggle_btn').click # sidebar is visible - expect(page).to have_css('#sidebar', visible: :visible) - expect(page).to have_css('.collapse_btn') - expect(page).not_to have_css('.uncollapse_btn') + expect(page).to have_css('#sidebar .sidebar_section', visible: :visible) + expect(page).not_to have_css('#active_admin_content.collapsed_sidebar') end end end + context 'left-sidebar with collapsible: true, start_collapsed: true' do + before do + add_author_resource_with_start_collapsed + add_post_resource + visit '/admin/authors' + end + + it 'has collapsible sidebar that starts collapsed' do + expect(page).to have_css('#active_admin_content.collapsible_sidebar.collapsed_sidebar') + expect(page).to have_css('.sidebar_toggle_btn') + end + + it 'can be expanded by clicking toggle button' do + page.find('.sidebar_toggle_btn').click + + expect(page).to have_css('#sidebar .sidebar_section', visible: :visible) + expect(page).not_to have_css('#active_admin_content.collapsed_sidebar') + + # session remembers expanded state + visit '/admin/authors' + expect(page).to have_css('#sidebar .sidebar_section', visible: :visible) + expect(page).not_to have_css('#active_admin_content.collapsed_sidebar') + end + end + + context 'deprecated collapsed option raises error' do + it 'raises ArgumentError for left_sidebar!' do + obj = Object.new + obj.extend(ActiveAdminSidebar::Positions) + expect { obj.left_sidebar!(collapsed: true) }.to raise_error(ArgumentError, /removed in v3/) + end + + it 'raises ArgumentError for right_sidebar!' do + obj = Object.new + obj.extend(ActiveAdminSidebar::Positions) + expect { obj.right_sidebar!(collapsed: true) }.to raise_error(ArgumentError, /removed in v3/) + end + end + + context 'right-sidebar with collapsible: true (starts expanded)' do + before do + add_author_resource_right_sidebar + add_post_resource + visit '/admin/authors' + end + + it 'has right-sidebar with toggle button' do + expect(page).to have_css('#filters_sidebar_section') + expect(page).to have_css('.sidebar_toggle_btn') + expect(page).to have_css('#active_admin_content.with_sidebar.collapsible_sidebar') + expect(page).not_to have_css('#active_admin_content.left_sidebar') + end + + it 'sidebar starts expanded' do + expect(page).to have_css('#sidebar .sidebar_section', visible: :visible) + expect(page).not_to have_css('#active_admin_content.collapsed_sidebar') + end + + context 'when click on toggle to collapse' do + before do + page.find('.sidebar_toggle_btn').click + end + + it 'sidebar is hidden and toggle button is on the right side' do + expect(page).to have_css('#active_admin_content.collapsed_sidebar') + + btn_left = page.evaluate_script("document.querySelector('.sidebar_toggle_btn').getBoundingClientRect().left") + page_width = page.evaluate_script('document.documentElement.clientWidth') + expect(btn_left).to be > (page_width / 2), "toggle button should be on the right side of the page" + + page.find('.sidebar_toggle_btn').click + + expect(page).not_to have_css('#active_admin_content.collapsed_sidebar') + expect(page).to have_css('#sidebar .sidebar_section', visible: :visible) + end + end + end + + context 'right-sidebar with collapsible: true, start_collapsed: true' do + before do + add_author_resource_right_sidebar_start_collapsed + add_post_resource + visit '/admin/authors' + end + + it 'has collapsible right-sidebar that starts collapsed' do + expect(page).to have_css('#active_admin_content.collapsible_sidebar.collapsed_sidebar') + expect(page).to have_css('.sidebar_toggle_btn') + end + + it 'can be expanded by clicking toggle button' do + page.find('.sidebar_toggle_btn').click + + expect(page).to have_css('#sidebar .sidebar_section', visible: :visible) + expect(page).not_to have_css('#active_admin_content.collapsed_sidebar') + + # session remembers expanded state + visit '/admin/authors' + expect(page).to have_css('#sidebar .sidebar_section', visible: :visible) + expect(page).not_to have_css('#active_admin_content.collapsed_sidebar') + end + end + + context 'per-resource sidebar state isolation' do + before do + Post.create!(title: 'Test', body: 'Body', author: Author.first) + add_author_resource_with_start_collapsed + add_post_resource_with_sidebar + end + + it 'changing sidebar state on one resource does not affect another' do + # Authors starts collapsed (collapsible: true, start_collapsed: true) + visit '/admin/authors' + expect(page).to have_css('#active_admin_content.collapsed_sidebar') + + # Expand authors + page.find('.sidebar_toggle_btn').click + expect(page).to have_css('#sidebar .sidebar_section', visible: :visible) + + # Posts should still be in its default state (expanded, not collapsed) + visit '/admin/posts' + expect(page).to have_css('#active_admin_content.collapsible_sidebar') + expect(page).not_to have_css('#active_admin_content.collapsed_sidebar') + + # Authors should still be expanded (from earlier toggle) + visit '/admin/authors' + expect(page).to have_css('#sidebar .sidebar_section', visible: :visible) + expect(page).not_to have_css('#active_admin_content.collapsed_sidebar') + end + end + end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 37ef3f7..4caf0b4 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,8 @@ +require 'simplecov' +SimpleCov.start do + add_filter '/spec/' +end + $LOAD_PATH.unshift(File.dirname(__FILE__)) $LOAD_PATH << File.expand_path('../support', __FILE__) @@ -6,12 +11,10 @@ Bundler.setup ENV['RAILS_ENV'] = 'test' -# Ensure the Active Admin load path is happy require 'rails' ENV['RAILS'] = Rails.version ENV['RAILS_ROOT'] = File.expand_path("../rails/rails-#{ENV['RAILS']}", __FILE__) -# Create the test app if it doesn't exists -unless File.exists?(ENV['RAILS_ROOT']) +unless File.exist?(ENV['RAILS_ROOT']) system 'rake setup' end @@ -19,15 +22,12 @@ require 'active_admin' ActiveAdmin.application.load_paths = [ENV['RAILS_ROOT'] + "/app/admin"] require ENV['RAILS_ROOT'] + '/config/environment.rb' -# Disabling authentication in specs so that we don't have to worry about -# it allover the place ActiveAdmin.application.authentication_method = false ActiveAdmin.application.current_user_method = false require 'rspec/rails' require 'capybara/rails' require 'capybara/rspec' -require 'selenium-webdriver' require 'support/admin' require 'support/capybara' @@ -36,6 +36,7 @@ config.use_transactional_fixtures = false config.before(:suite) do + ActiveRecord::Migration.maintain_test_schema! DatabaseCleaner.strategy = :truncation DatabaseCleaner.clean_with(:truncation) end @@ -46,5 +47,4 @@ config.after(:each) do DatabaseCleaner.clean end - end diff --git a/spec/support/admin.rb b/spec/support/admin.rb index 987b712..55928c7 100644 --- a/spec/support/admin.rb +++ b/spec/support/admin.rb @@ -8,6 +8,44 @@ def add_author_resource(options = {}, &block) end +def add_author_resource_with_start_collapsed(options = {}, &block) + + ActiveAdmin.register Author do + config.filters = true + before_action do + left_sidebar!(collapsible: true, start_collapsed: true) + end + end + + Rails.application.reload_routes! + +end + +def add_author_resource_right_sidebar(options = {}, &block) + + ActiveAdmin.register Author do + config.filters = true + before_action do + right_sidebar!(collapsible: true) + end + end + + Rails.application.reload_routes! + +end + +def add_author_resource_right_sidebar_start_collapsed(options = {}, &block) + + ActiveAdmin.register Author do + config.filters = true + before_action do + right_sidebar!(collapsible: true, start_collapsed: true) + end + end + + Rails.application.reload_routes! + +end def add_post_resource(options = {}, &block) @@ -19,3 +57,17 @@ def add_post_resource(options = {}, &block) Rails.application.reload_routes! end + +def add_post_resource_with_sidebar(options = {}, &block) + + ActiveAdmin.register Post do + config.filters = true + skip_before_action :skip_sidebar!, raise: false + before_action do + left_sidebar!(collapsible: true) + end + end + + Rails.application.reload_routes! + +end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 87f4846..e09d7e9 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -1,13 +1,8 @@ -Capybara.configure do |config| - config.match = :prefer_exact -end - -Capybara.register_driver :selenium_chrome do |app| - options = Selenium::WebDriver::Chrome::Options.new( - args: %w[headless disable-gpu no-sandbox] - ) - Capybara::Selenium::Driver.new(app, browser: :chrome, options: options) -end +require 'capybara/cuprite' Capybara.server = :webrick -Capybara.javascript_driver = :selenium_chrome +Capybara.register_driver :cuprite do |app| + Capybara::Cuprite::Driver.new(app, headless: true, window_size: [1280, 800]) +end +Capybara.javascript_driver = :cuprite +Capybara.default_max_wait_time = 5 diff --git a/spec/support/rails_template.rb b/spec/support/rails_template.rb index 57608f2..cc2ffcb 100644 --- a/spec/support/rails_template.rb +++ b/spec/support/rails_template.rb @@ -1,21 +1,34 @@ -# Rails template to build the sample app for specs +# Ensure Sprockets manifest exists (required by Rails 8+) +FileUtils.mkdir_p("app/assets/config") +File.write("app/assets/config/manifest.js", + "//= link_directory ../javascripts .js\n//= link_directory ../stylesheets .css\n") -generate :model, 'author name:string{10}:uniq last_name:string birthday:date' -generate :model, 'post title:string:uniq body:text author:references' +generate :model, 'author name:string{10}:uniq last_name:string birthday:date --force' +generate :model, 'post title:string:uniq body:text author:references --force' -#Add validation -inject_into_file "app/models/author.rb", " validates_presence_of :name\n validates_uniqueness_of :last_name\n", after: "Base\n" +inject_into_file "app/models/author.rb", " validates_presence_of :name\n validates_uniqueness_of :last_name\n", after: "ApplicationRecord\n" inject_into_file "app/models/post.rb", " validates_presence_of :author\n", after: ":author\n" -# Configure default_url_options in test environment -inject_into_file "config/environments/test.rb", " config.action_mailer.default_url_options = { :host => 'example.com' }\n", after: "config.cache_classes = true\n" - -# Add our local Active Admin to the load path -inject_into_file "config/environment.rb", - "\n$LOAD_PATH.unshift('#{File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'lib'))}')\nrequire \"active_admin\"\n", - after: "require File.expand_path('../application', __FILE__)" - -run "rm Gemfile" +# Add ransackable_attributes for Ransack 4+ +inject_into_file "app/models/author.rb", + " def self.ransackable_attributes(auth_object = nil)\n" \ + " [\"name\", \"last_name\", \"birthday\", \"created_at\", \"updated_at\"]\n" \ + " end\n", + after: "ApplicationRecord\n" + +inject_into_file "app/models/post.rb", + " def self.ransackable_attributes(auth_object = nil)\n" \ + " [\"title\", \"body\", \"author_id\"]\n" \ + " end\n" \ + " def self.ransackable_associations(auth_object = nil)\n" \ + " [\"author\"]\n" \ + " end\n", + after: "ApplicationRecord\n" + +# Add our local Active Admin to the load path (Rails 7.1+) +gsub_file "config/environment.rb", + 'require_relative "application"', + "require_relative \"application\"\n$LOAD_PATH.unshift('#{File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'lib'))}')\nrequire \"active_admin\"\n" $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) @@ -24,26 +37,19 @@ # Initialize plugin inject_into_file "config/initializers/active_admin.rb", - " config.before_action do\n left_sidebar!(collapsed: true) if respond_to?(:left_sidebar!)\n end\n\n", + " config.before_action do\n left_sidebar!(collapsible: true) if respond_to?(:left_sidebar!)\n end\n\n", after: "ActiveAdmin.setup do |config|\n" inject_into_file "app/assets/stylesheets/active_admin.scss", "@import \"active_admin_sidebar\";\n", after: "@import \"active_admin/base\";\n" -if File.file?("app/assets/javascripts/active_admin.js") - inject_into_file "app/assets/javascripts/active_admin.js", - "//= require active_admin_sidebar\n", - after: "//= require active_admin/base\n" -else - inject_into_file "app/assets/javascripts/active_admin.js.coffee", - "#= require active_admin_sidebar\n", - after: "#= require active_admin/base\n" -end - -run "rm -r test" -run "rm -r spec" +inject_into_file "app/assets/javascripts/active_admin.js", + "//= require active_admin_sidebar\n", + after: "//= require active_admin/base\n" +run "rm -rf test" route "root :to => 'admin/dashboard#index'" - rake "db:migrate" + +run "rm -f Gemfile Gemfile.lock" diff --git a/tasks/test.rake b/tasks/test.rake index a32c625..f46bf45 100644 --- a/tasks/test.rake +++ b/tasks/test.rake @@ -1,13 +1,11 @@ desc "Creates a test rails app for the specs to run against" task :setup do require 'rails/version' - system("mkdir spec/rails") unless File.exists?("spec/rails") rails_new_opts = %w( --skip-turbolinks --skip-spring --skip-bootsnap - --skip-webpack-install -m spec/support/rails_template.rb )