diff --git a/.document b/.document deleted file mode 100644 index ecf3673194b..00000000000 --- a/.document +++ /dev/null @@ -1,5 +0,0 @@ -README.rdoc -lib/**/*.rb -bin/* -features/**/*.feature -LICENSE diff --git a/.gherkin-lintrc b/.gherkin-lintrc new file mode 100644 index 00000000000..8afd8ef7f5b --- /dev/null +++ b/.gherkin-lintrc @@ -0,0 +1,39 @@ +{ + "no-files-without-scenarios" : "off", + "no-unnamed-features": "on", + "no-unnamed-scenarios": "on", + "no-dupe-feature-names": "on", + "no-partially-commented-tag-lines": "on", + "indentation": [ + "on", { + "Feature": 0, + "Background": 2, + "Scenario": 2, + "Step": 2, + "Examples": 0, + "example": 2, + "given": 4, + "when": 4, + "then": 4, + "and": 4, + "but": 4, + "feature tag": 0, + "scenario tag": 2 + } + ], + "no-trailing-spaces": "on", + "new-line-at-eof": ["on", "yes"], + "no-multiple-empty-lines": "on", + "no-empty-file": "on", + "no-scenario-outlines-without-examples": "on", + "name-length": ["on", {"Feature": 50, "Scenario": 85, "Step": 115}], + "no-restricted-tags": ["on", {"tags": ["@watch", "@wip"]}], + "use-and": "on", + "no-duplicate-tags": "on", + "no-superfluous-tags": "on", + "no-homogenous-tags": "on", + "one-space-between-tags": "on", + "no-unused-variables": "on", + "no-background-only-scenario": "on", + "no-empty-background": "on" +} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000000..f25b1ebbd6e --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,5 @@ +--- + +liberapay: Active-Admin +open_collective: activeadmin +tidelift: rubygems/activeadmin diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 00000000000..a65fee43345 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: If you've already asked for help with a problem and confirmed something is broken with ActiveAdmin itself, create a bug report. +title: '' +labels: '' +assignees: '' +--- + + + +Describe your issue with a **clear title and description**. Make sure to include +as much relevant information as possible, including a code sample or failing +test that demonstrates the expected behavior, as well as your system +configuration. Your goal should be to make it easy for yourself - and others - +to reproduce the bug and figure out a fix. + +### Expected behavior + +What do you think should happen? + +### Actual behavior + +What actually happens? + +### How to reproduce + +Having a way to reproduce your issue will help people confirm, investigate, +and ultimately fix your issue. You can do this by providing an executable test +case. To make this process easier, please use [our bug report template script]. + +Copy the content of the appropriate template into an `.rb` file and make the +necessary changes to demonstrate the issue. You can execute it by running +`ruby the_file.rb` in your terminal. If all goes well, you should see your test +case failing. + +[our bug report template script]: https://github.com/activeadmin/activeadmin/blob/master/tasks/bug_report_template.rb diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..3202933f390 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +blank_issues_enabled: false +contact_links: + - name: Get Help + url: https://github.com/activeadmin/activeadmin/discussions/new?category=help + about: If you can't get something to work the way you expect, open a question in our discussion forums. + - name: Feature Request + url: https://github.com/activeadmin/activeadmin/discussions/new?category=ideas + about: Suggest any ideas you have using our discussion forums. + - name: Documentation Issue + url: https://github.com/activeadmin/activeadmin/pulls + about: For documentation improvements, feel free to create a pull request. + - name: Localization Issue + url: https://github.com/activeadmin/activeadmin/pulls + about: For any localization updates, create a pull request as we rely entirely on the community for these. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..f1294ef22ba --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,20 @@ + diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000000..80219a6c70f --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,156 @@ +# Active Admin - GitHub Copilot Instructions + +## Project Overview + +Active Admin is a Ruby on Rails framework for creating elegant backends for website administration. It provides a DSL for developers to quickly create good-looking administration interfaces. + +## Technology Stack + +- **Backend**: Ruby on Rails (currently Rails ~> 8.1.0) +- **Frontend**: JavaScript (ES6+), Tailwind CSS, Flowbite +- **Testing**: RSpec (unit tests), Cucumber (feature tests), Capybara (integration tests) +- **Build Tools**: Rollup (JavaScript bundling), cssbundling-rails +- **Key Dependencies**: Devise (authentication), Ransack (search), Formtastic (forms), Kaminari (pagination) + +## Ruby Conventions + +- Minimum Ruby version: 3.3+ (required) +- Minimum Rails version: 7.2+ (required by gemspec) +- Current development uses Rails ~> 8.1.0 +- Follow RuboCop style guide (configuration in `.rubocop.yml`) +- RuboCop plugins enabled: capybara, packaging, performance, rails, rspec +- Use frozen string literals: `# frozen_string_literal: true` at the top of Ruby files + +## JavaScript Conventions + +- Use ES6+ modern JavaScript syntax +- Follow ESLint configuration (see `eslint.config.js`) +- JavaScript source files are in `app/javascript/` +- Build JavaScript with: `npm run build` +- Lint JavaScript with: `npm run lint` + +## Testing Guidelines + +### Ruby Tests (RSpec) +- Unit tests are in `spec/unit/` +- Request specs are in `spec/requests/` +- Helper specs are in `spec/helpers/` +- Run RSpec tests: `bundle exec rspec` + +### Feature Tests (Cucumber) +- Cucumber features are in `features/` +- Uses Capybara with Cuprite (headless Chrome) +- Cucumber scenarios require Chrome to be installed +- Run Cucumber tests: `bundle exec cucumber` +- Lint Gherkin files: `npm run gherkin-lint` + +### Running All Tests +- Run the complete test suite: `bin/rake` +- Tests run against a sample Rails app generated in `tmp/test_apps/` + +## Building and Development + +### Setup +```bash +bundle install +yarn install +``` + +Note: The `bin/rake local server` command requires foreman, which it will invoke automatically. Install with `gem install foreman` if needed. + +### Testing Against Different Rails Versions +```bash +# Available versions: rails_72, rails_80 +export BUNDLE_GEMFILE=gemfiles/rails_72/Gemfile +``` + +### Local Development Server +```bash +bin/rake local server +# Visit http://localhost:3000/admin +# Login: admin@example.com / password +``` + +### Other Local Commands +```bash +bin/rake local console # Rails console +bin/rake local db:migrate # Run migrations +``` + +## Code Organization + +- `lib/active_admin/` - Core framework code +- `app/` - Rails application components (controllers, helpers, views, assets) +- `spec/` - RSpec tests +- `features/` - Cucumber feature tests +- `docs/` - VitePress documentation (run with `npm run docs:dev`) + +## Important Guidelines + +1. **Minimal Changes**: Make surgical, precise changes. Don't refactor unrelated code. +2. **Backward Compatibility**: Active Admin is a widely-used gem. Maintain backward compatibility unless explicitly breaking changes are intended. +3. **Test Coverage**: Include tests for new features. Prefer RSpec specs (especially request specs) over Cucumber features when possible, as specs are easier to work with and maintain. +4. **Documentation**: Update documentation in `docs/` if adding user-facing features. +5. **Internationalization**: Support i18n - translation files are in `config/locales/` +6. **Security**: This is an administration framework so be extra cautious with security implications. +7. **Code Quality**: Always run ALL relevant linters before submitting any PR to ensure code follows project style guidelines: + - Ruby code: `bundle exec rubocop` + - JavaScript code: `npm run lint` + - Gherkin files: `npm run gherkin-lint` + - Run these linters BEFORE requesting code review or submitting the PR. + +## Contributing Workflow + +1. Create feature request discussion before starting significant new features +2. Fork and create a descriptive branch +3. Ensure tests pass: `bin/rake` +4. **Run ALL linters BEFORE submitting PR**: + - Ruby code: `bundle exec rubocop` + - JavaScript code: `npm run lint` + - Gherkin files: `npm run gherkin-lint` +5. View changes in browser: `bin/rake local server` +6. Submit pull request with passing CI + +## Commit Message Guidelines + +Follow the project's commit message standards as outlined in [CONTRIBUTING.md](../CONTRIBUTING.md): + +### Format Requirements +- **Reference**: Follow [How to Write a Git Commit Message](https://cbea.ms/git-commit/#seven-rules) +- **Format**: Use imperative mood ("Add feature" not "Added feature") +- **Length**: Limit subject line to 50 characters, body lines to 72 characters +- **Structure**: + ``` + Short summary (50 chars max) + + Detailed explanation if needed (72 chars per line) + + - Use bullet points for multiple changes + - Reference issues with "Fixes #123" or "Closes #456" + ``` + +### Commit Message Examples +``` +Fix temporal query performance regression + +Add support for Rails 8.0 compatibility + +Update dependencies for security patches + +Fixes #123 +``` + +### Best Practices +- Keep commits atomic (one logical change per commit) +- Write clear, descriptive commit messages +- Reference related issues and pull requests +- Avoid generic messages like "Fix bug" or "Update code" + +## Additional Context + +- This is both a Ruby gem and an npm package +- Published to RubyGems as `activeadmin` +- Published to npm as `@activeadmin/activeadmin` +- The project uses both Ruby and JavaScript tooling +- CI runs tests against multiple Rails versions +- Code coverage tracked with SimpleCov and CodeCov diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..6fd6d21a4cf --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,65 @@ +version: 2 +multi-ecosystem-groups: + bundler: + schedule: + interval: weekly +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily + cooldown: + default-days: 5 + groups: + github_actions: + patterns: + - "*" + - package-ecosystem: bundler + directory: / + versioning-strategy: lockfile-only + cooldown: + default-days: 5 + patterns: + - "*" + multi-ecosystem-group: bundler + - package-ecosystem: npm + directory: / + schedule: + interval: monthly + versioning-strategy: lockfile-only + cooldown: + default-days: 5 + groups: + npm: + patterns: + - "*" + - package-ecosystem: bundler + directory: /gemfiles/rails_72 + versioning-strategy: lockfile-only + cooldown: + default-days: 5 + patterns: + - "*" + multi-ecosystem-group: bundler + ignore: + - dependency-name: connection_pool + versions: ">= 3" # TODO: remove when dropping Rails 7.2 (see rails/rails#56620) + - dependency-name: rails + versions: ">= 8.0.0" + - dependency-name: rails-i18n + versions: ">= 8.0.0" + - dependency-name: railties + versions: ">= 8.0.0" + - package-ecosystem: bundler + directory: /gemfiles/rails_80 + versioning-strategy: lockfile-only + cooldown: + default-days: 5 + patterns: + - "*" + multi-ecosystem-group: bundler + ignore: + - dependency-name: rails + versions: ">= 8.1.0" + - dependency-name: rails-i18n + versions: ">= 8.1.0" diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 00000000000..fd55d1984b5 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,23 @@ +changelog: + categories: + - title: Breaking Changes 🚨 + labels: + - type breaking change + - title: Template Updates 📝 + labels: + - type template update + - title: Enhancements ✨ + labels: + - type enhancement + - title: Bug Fixes 🐛 + labels: + - type bug fix + - title: Security Fixes 🔒 + labels: + - type security fix + - title: Other Changes 🛠 + labels: + - "*" + exclude: + authors: + - dependabot diff --git a/.github/workflows/bug-report-template.yml b/.github/workflows/bug-report-template.yml new file mode 100644 index 00000000000..68bee34e65b --- /dev/null +++ b/.github/workflows/bug-report-template.yml @@ -0,0 +1,42 @@ +name: Bug Reports + +on: + schedule: + # Run every day at noon UTC + - cron: '0 12 * * *' + pull_request: + +concurrency: + group: ${{ github.repository }}-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + RUBY_VERSION: "4.0" + +jobs: + bug_report_template_test: + name: Run bug report template + # Don't run scheduled workflow on forks + if: ${{ (github.event_name == 'pull_request' || github.repository_owner == 'activeadmin') }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: tj-actions/changed-files@v47.0.6 + id: changed-files + with: + files: | + app/** + config/** + lib/*.rb + lib/active_admin/** + tasks/bug_report_template.rb + Gemfile* + *.gemspec + - uses: ruby/setup-ruby@v1 + if: steps.changed-files.outputs.any_changed == 'true' + with: + ruby-version: ${{ env.RUBY_VERSION }} + bundler-cache: true + - name: Run bug report template + if: steps.changed-files.outputs.any_changed == 'true' + run: ACTIVE_ADMIN_PATH=. ruby tasks/bug_report_template.rb diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000000..c81492f0144 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,121 @@ +name: ci + +on: + pull_request: + push: + branches: + - master + +concurrency: + group: ${{ github.repository }}-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + test: + name: test (${{ matrix.ruby }}, ${{ matrix.rails }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + ruby: + - "4.0" + - "3.4" + - "3.3" + os: + - ubuntu-latest + rails: + - rails_81 + - rails_80 + - rails_72 + exclude: + - ruby: '4.0' + os: ubuntu-latest + rails: rails_72 + steps: + - uses: actions/checkout@v7 + - name: Configure bundler (default) + run: | + echo "BUNDLE_GEMFILE=Gemfile" >> "$GITHUB_ENV" + if: matrix.rails == 'rails_81' + - name: Configure bundler (alternative) + run: | + echo "BUNDLE_GEMFILE=gemfiles/${{ matrix.rails }}/Gemfile" >> "$GITHUB_ENV" + if: matrix.rails != 'rails_81' + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + rubygems: latest + - name: Create test app + run: bin/rake setup + - name: Restore cached RSpec runtimes + uses: actions/cache@v6 + with: + path: tmp/parallel_runtime_rspec.log + key: runtimes-rspec-${{ matrix.ruby }}-${{ matrix.rails }}-${{ hashFiles('tmp/parallel_runtime_rspec.log') }} + - name: Run RSpec tests + env: + COVERAGE: true + run: | + bin/parallel_rspec + RSPEC_FILESYSTEM_CHANGES=true bin/rspec + - name: Restore cached cucumber runtimes + uses: actions/cache@v6 + with: + path: tmp/parallel_runtime_cucumber.log + key: runtimes-cucumber-${{ matrix.ruby }}-${{ matrix.rails }}-${{ hashFiles('tmp/parallel_runtime_cucumber.log') }} + - name: Run Cucumber features + env: + COVERAGE: true + run: | + bin/parallel_cucumber --fail-fast + bin/cucumber --profile filesystem-changes + bin/cucumber --profile class-reloading + - name: Rename coverage file by matrix run + run: mv coverage/coverage.xml coverage/coverage-ruby-${{ matrix.ruby }}-${{ matrix.rails }}.xml + - uses: actions/upload-artifact@v7 + with: + name: coverage-ruby-${{ matrix.ruby }}-${{ matrix.rails }} + path: coverage + if-no-files-found: error + + upload_coverage: + name: Upload Coverage + runs-on: ubuntu-latest + # Do not run on forks + if: ${{ github.repository_owner == 'activeadmin' }} + needs: [test] + steps: + - uses: actions/checkout@v7 + - uses: actions/download-artifact@v8 + with: + path: coverage + pattern: coverage-ruby-* + merge-multiple: true + - uses: codecov/codecov-action@v7 + with: + token: ${{ secrets.CODECOV_TOKEN }} + directory: coverage + fail_ci_if_error: true + + test_docs_build: + name: Build docs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: tj-actions/changed-files@v47.0.6 + id: changed-files + with: + files: | + docs/** + package*.json + - uses: actions/setup-node@v7 + if: steps.changed-files.outputs.any_changed == 'true' + with: + node-version: 24 + cache: npm + - run: npm ci + if: steps.changed-files.outputs.any_changed == 'true' + - run: npm run docs:test + if: steps.changed-files.outputs.any_changed == 'true' diff --git a/.github/workflows/do-not-merge.yml b/.github/workflows/do-not-merge.yml new file mode 100644 index 00000000000..142f208a772 --- /dev/null +++ b/.github/workflows/do-not-merge.yml @@ -0,0 +1,22 @@ +name: Do Not Merge + +on: + pull_request: + types: + - opened + - reopened + - synchronize + - labeled + - unlabeled + +jobs: + do-not-merge: + if: ${{ contains(github.event.*.labels.*.name, 'do not merge') }} + name: Prevent merging + runs-on: ubuntu-latest + steps: + - name: Check for PR label + run: | + echo "Pull request has a 'do not merge' label applied." + echo "This workflow fails to prevent merging until label is removed." + exit 1 diff --git a/.github/workflows/docs-deployment.yml b/.github/workflows/docs-deployment.yml new file mode 100644 index 00000000000..3a3c672ca01 --- /dev/null +++ b/.github/workflows/docs-deployment.yml @@ -0,0 +1,50 @@ +name: Docs Deployment + +on: + push: + branches: + - master + release: + types: + - published + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: ${{ github.repository }}-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + build_docs: + name: Build docs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 24 + cache: npm + - uses: actions/configure-pages@v6 + - run: npm ci + - run: npm run docs:test + - uses: actions/upload-pages-artifact@v5 + with: + path: docs/.vitepress/dist + + deploy_docs: + name: Deploy docs site + runs-on: ubuntu-latest + needs: build_docs + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/deploy-pages@v5 + id: deployment diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml new file mode 100644 index 00000000000..087ffc68a23 --- /dev/null +++ b/.github/workflows/eslint.yml @@ -0,0 +1,32 @@ +name: ESLint + +on: + pull_request: + +env: + NODE_VERSION: ${{ vars.ESLINT_NODE_VERSION || '24.x' }} + +jobs: + eslint: + name: Run eslint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + - uses: tj-actions/changed-files@v47.0.6 + id: changed-files + with: + files: | + **.js + package*.json + .github/workflows/eslint.yml + - uses: reviewdog/action-eslint@v1 + if: steps.changed-files.outputs.any_changed == 'true' + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + filter_mode: nofilter # added (default), diff_context, file, nofilter + fail_level: any + reporter: github-pr-check diff --git a/.github/workflows/gherkin-lint.yml b/.github/workflows/gherkin-lint.yml new file mode 100644 index 00000000000..61e4159b4a7 --- /dev/null +++ b/.github/workflows/gherkin-lint.yml @@ -0,0 +1,31 @@ +name: Gherkin Lint + +on: + pull_request: + +env: + NODE_VERSION: 24.x + +jobs: + gherkin_lint: + name: Run gherkin-lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: tj-actions/changed-files@v47.0.6 + id: changed-files + with: + files: | + **.feature + .gherkin-lintrc + package*.json + .github/workflows/gherkin-lint.yml + - uses: actions/setup-node@v7 + if: steps.changed-files.outputs.any_changed == 'true' + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + - run: npm ci + if: steps.changed-files.outputs.any_changed == 'true' + - run: npm run gherkin-lint + if: steps.changed-files.outputs.any_changed == 'true' diff --git a/.github/workflows/github-actions-lint.yml b/.github/workflows/github-actions-lint.yml new file mode 100644 index 00000000000..0e1afc57d9e --- /dev/null +++ b/.github/workflows/github-actions-lint.yml @@ -0,0 +1,24 @@ +name: GitHub Actions Lint + +on: + pull_request: + +jobs: + github_actions_lint: + name: Run actionlint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: tj-actions/changed-files@v47.0.6 + id: changed-files + with: + files: | + .github/workflows/*.yaml + .github/workflows/*.yml + - uses: reviewdog/action-actionlint@v1 + if: steps.changed-files.outputs.any_changed == 'true' + with: + fail_level: any + filter_mode: nofilter # added (default), diff_context, file, nofilter + github_token: ${{ secrets.GITHUB_TOKEN }} + reporter: github-pr-check diff --git a/.github/workflows/markdown-lint.yml b/.github/workflows/markdown-lint.yml new file mode 100644 index 00000000000..0f062626a6e --- /dev/null +++ b/.github/workflows/markdown-lint.yml @@ -0,0 +1,30 @@ +name: Markdown Lint + +on: + pull_request: + +env: + MARKDOWNLINT_FLAGS: ${{ vars.REVIEWDOG_MARKDOWNLINT_FLAGS || '--git-recurse .' }} + +jobs: + markdownlint: + name: Run markdownlint + runs-on: ubuntu-latest + if: ${{ vars.SKIP_REVIEWDOG_MARKDOWNLINT != 'true' }} + steps: + - uses: actions/checkout@v7 + - uses: tj-actions/changed-files@v47.0.6 + id: changed-files + with: + files: | + **.md + .markdownlint.yml + .github/workflows/markdown-lint.yml + - uses: reviewdog/action-markdownlint@v0 + if: steps.changed-files.outputs.any_changed == 'true' + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + filter_mode: nofilter # added (default), diff_context, file, nofilter + fail_level: any + markdownlint_flags: ${{ env.MARKDOWNLINT_FLAGS }} + reporter: github-pr-check diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml new file mode 100644 index 00000000000..75a78946cfe --- /dev/null +++ b/.github/workflows/rubocop.yml @@ -0,0 +1,44 @@ +name: Rubocop + +on: + pull_request: + +env: + RUBY_VERSION: ${{ vars.RUBOCOP_RUBY_VERSION || '4.0' }} + +jobs: + rubocop: + name: Run rubocop + runs-on: ubuntu-latest + env: + BUNDLE_ONLY: ${{ vars.RUBOCOP_BUNDLE_ONLY || 'rubocop' }} + steps: + - uses: actions/checkout@v7 + - uses: tj-actions/changed-files@v47.0.6 + id: changed-files + with: + files: | + .github/workflows/rubocop.yml + .rubocop.yml + **.rb + **.rake + **.arb + bin/* + gemfiles/**/Gemfile + Gemfile* + Rakefile + *.gemspec + .simplecov + - uses: ruby/setup-ruby@v1 + if: steps.changed-files.outputs.any_changed == 'true' + with: + ruby-version: ${{ env.RUBY_VERSION }} + bundler-cache: true + - uses: reviewdog/action-rubocop@v2 + if: steps.changed-files.outputs.any_changed == 'true' + with: + fail_level: any + filter_mode: nofilter # added (default), diff_context, file, nofilter + github_token: ${{ secrets.GITHUB_TOKEN }} + skip_install: true + use_bundler: true diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml new file mode 100644 index 00000000000..4bb35194abc --- /dev/null +++ b/.github/workflows/typos.yml @@ -0,0 +1,17 @@ +name: Typos + +on: + pull_request: + +jobs: + typos: + name: Run typos + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: reviewdog/action-typos@v1 + with: + fail_level: any + filter_mode: nofilter # added (default), diff_context, file, nofilter + github_token: ${{ secrets.GITHUB_TOKEN }} + reporter: github-pr-check diff --git a/.github/workflows/yaml-lint.yml b/.github/workflows/yaml-lint.yml new file mode 100644 index 00000000000..5d5954e8f14 --- /dev/null +++ b/.github/workflows/yaml-lint.yml @@ -0,0 +1,24 @@ +name: YAML Lint + +on: + pull_request: + +jobs: + yaml_lint: + name: Run yamllint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: tj-actions/changed-files@v47.0.6 + id: changed-files + with: + files: | + **.yaml + **.yml + - uses: reviewdog/action-yamllint@v1 + if: steps.changed-files.outputs.any_changed == 'true' + with: + fail_level: any + filter_mode: nofilter # added (default), diff_context, file, nofilter + github_token: ${{ secrets.GITHUB_TOKEN }} + reporter: github-pr-check diff --git a/.gitignore b/.gitignore index f3b524d6bd3..b6717c75e30 100644 --- a/.gitignore +++ b/.gitignore @@ -1,39 +1,16 @@ -## MAC OS -.DS_Store - -## TEXTMATE -*.tmproj -tmtags - -## EMACS -*~ -\#* -.\#* - -## VIM -*.swp -# IDEA / RUBYMINE -.idea - -## PROJECT::GENERAL -tags -coverage -rdoc -doc -.yardoc -pkg - -## PROJECT::SPECIFIC +/tmp +/tasks/tmp +/coverage +/.yardoc +/.ruby-version +/pkg .bundle -spec/rails -*.sqlite3-journal -Gemfile.lock -Gemfile-*.lock -capybara* -viewcumber -test-rails* -public -.rvmrc -.rspec -.rails-version -.rbenv-version +/.rspec_failures +/node_modules +/src +/vendor/bundle +/rails_70 +/dist +docs/.vitepress/cache +docs/.vitepress/dist +docs/.vitepress/.temp diff --git a/.markdownlint.yml b/.markdownlint.yml new file mode 100644 index 00000000000..9fe39abc0d1 --- /dev/null +++ b/.markdownlint.yml @@ -0,0 +1,5 @@ +default: true +MD002: false +MD013: false +MD024: false +MD041: false diff --git a/.rspec b/.rspec new file mode 100644 index 00000000000..03d161c7cc7 --- /dev/null +++ b/.rspec @@ -0,0 +1,3 @@ +--format <%= ENV['CI'] ? 'documentation' : 'progress' %> +--require spec_helper +<%= "--require #{__dir__}/spec/support/simplecov_changes_env.rb --tag changes_filesystem" if ENV['RSPEC_FILESYSTEM_CHANGES'] %> diff --git a/.rspec_parallel b/.rspec_parallel new file mode 100644 index 00000000000..e86512bdfb6 --- /dev/null +++ b/.rspec_parallel @@ -0,0 +1,3 @@ +--require <%= "#{__dir__}/spec/support/simplecov_regular_env.rb" %> +--format progress +--format ParallelTests::RSpec::RuntimeLogger --out tmp/parallel_runtime_rspec.log diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000000..6f0b4159bd2 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,420 @@ +--- + +inherit_mode: + merge: + - Include + +plugins: + - rubocop-capybara + - rubocop-packaging + - rubocop-performance + - rubocop-rails + - rubocop-rspec + +AllCops: + DisabledByDefault: true + TargetRubyVersion: 3.3 + TargetRailsVersion: 7.2 + + Exclude: + - .git/**/* + - .github/**/* + - bin/**/* + - gemfiles/**/vendor/**/* + - node_modules/**/* + - tmp/**/* + - vendor/**/* + + Include: + - gemfiles/*/Gemfile + - .simplecov + + DisplayCopNames: true + + StyleGuideCopsOnly: false + +Capybara/AssertStyle: + Enabled: true + +Capybara/FindAllFirst: + Enabled: true + +Capybara/RedundantWithinFind: + Enabled: true + +Capybara/RSpec/CurrentPathExpectation: + Enabled: true + +Capybara/RSpec/HaveContent: + Enabled: true + +Capybara/RSpec/HaveSelector: + Enabled: true + +Capybara/RSpec/MatchStyle: + Enabled: true + +Capybara/RSpec/NegationMatcher: + Enabled: true + +Capybara/RSpec/NegationMatcherAfterVisit: + Enabled: true + +Capybara/RSpec/PredicateMatcher: + Enabled: true + +Capybara/RSpec/SpecificMatcher: + Enabled: true + +Capybara/RSpec/VisibilityMatcher: + Enabled: true + +Capybara/SpecificActions: + Enabled: true + +Capybara/SpecificFinders: + Enabled: true + +Layout/AccessModifierIndentation: + Enabled: true + +Layout/ArgumentAlignment: + Enabled: true + +Layout/CaseIndentation: + Enabled: true + +Layout/ClosingParenthesisIndentation: + Enabled: true + +Layout/CommentIndentation: + Enabled: true + +Layout/ElseAlignment: + Enabled: true + +Layout/EmptyLines: + Enabled: true + +Layout/EmptyLinesAroundBlockBody: + Enabled: true + +Layout/EndAlignment: + Enabled: true + +Layout/EndOfLine: + Enabled: true + +Layout/ExtraSpacing: + AllowForAlignment: false + Enabled: true + +Layout/FirstArgumentIndentation: + Enabled: true + +Layout/FirstHashElementIndentation: + Enabled: true + EnforcedStyle: consistent + +Layout/FirstMethodArgumentLineBreak: + Enabled: true + +Layout/FirstParameterIndentation: + Enabled: true + +Layout/HashAlignment: + Enabled: true + +Layout/IndentationConsistency: + Enabled: true + +Layout/IndentationStyle: + Enabled: true + EnforcedStyle: spaces + +Layout/IndentationWidth: + Enabled: true + +Layout/ParameterAlignment: + Enabled: true + EnforcedStyle: with_fixed_indentation + +Layout/SpaceAfterComma: + Enabled: true + +Layout/SpaceAroundEqualsInParameterDefault: + Enabled: true + +Layout/SpaceAroundOperators: + Enabled: true + +Layout/SpaceBeforeBlockBraces: + Enabled: true + +Layout/SpaceBeforeComma: + Enabled: true + +Layout/SpaceBeforeFirstArg: + Enabled: true + +Layout/SpaceInsideBlockBraces: + Enabled: true + +Layout/SpaceInsideHashLiteralBraces: + Enabled: true + +Layout/SpaceInsideParens: + Enabled: true + +Layout/TrailingEmptyLines: + Enabled: true + +Layout/TrailingWhitespace: + Enabled: true + +Lint/AmbiguousOperator: + Enabled: true + +Lint/AmbiguousRegexpLiteral: + Enabled: true + +Lint/ParenthesesAsGroupedExpression: + Enabled: true + +Lint/RedundantStringCoercion: + Enabled: true + +Lint/UselessAccessModifier: + Enabled: true + +Lint/UselessAssignment: + Enabled: true + +Naming/PredicatePrefix: + Enabled: true + + ForbiddenPrefixes: + - is_ + - have_ + + AllowedMethods: + - has_many + - has_many_actions + +Packaging/BundlerSetupInTests: + Enabled: true + +Packaging/GemspecGit: + Enabled: true + +Packaging/RequireHardcodingLib: + Enabled: true + +Packaging/RequireRelativeHardcodingLib: + Enabled: true + +Performance/AncestorsInclude: + Enabled: false + +Performance/ArraySemiInfiniteRangeSlice: + Enabled: false + +Performance/BigDecimalWithNumericArgument: + Enabled: true + +Performance/BindCall: + Enabled: true + +Performance/BlockGivenWithExplicitBlock: + Enabled: true + +Performance/Caller: + Enabled: true + +Performance/Casecmp: + Enabled: false + +Performance/CaseWhenSplat: + Enabled: true + +Performance/ChainArrayAllocation: + Enabled: false + +Performance/CollectionLiteralInLoop: + Enabled: true + Exclude: + - spec/**/* + +Performance/CompareWithBlock: + Enabled: true + +Performance/ConcurrentMonotonicTime: + Enabled: true + +Performance/ConstantRegexp: + Enabled: true + +Performance/Count: + Enabled: true + +Performance/DeletePrefix: + Enabled: true + +Performance/DeleteSuffix: + Enabled: true + +Performance/Detect: + Enabled: true + +Performance/DoubleStartEndWith: + Enabled: true + IncludeActiveSupportAliases: true + +Performance/EndWith: + Enabled: true + +Performance/FixedSize: + Enabled: true + +Performance/FlatMap: + Enabled: true + EnabledForFlattenWithoutParams: false + +Performance/InefficientHashSearch: + Enabled: true + +Performance/IoReadlines: + Enabled: true + +Performance/MapCompact: + Enabled: false + +Performance/MapMethodChain: + Enabled: false + +Performance/MethodObjectAsBlock: + Enabled: true + +Performance/OpenStruct: + Enabled: true + +Performance/RangeInclude: + Enabled: true + +Performance/RedundantBlockCall: + Enabled: false + +Performance/RedundantEqualityComparisonBlock: + Enabled: false + +Performance/RedundantMatch: + Enabled: true + +Performance/RedundantMerge: + Enabled: true + MaxKeyValuePairs: 2 + +Performance/RedundantSortBlock: + Enabled: true + +Performance/RedundantSplitRegexpArgument: + Enabled: true + +Performance/RedundantStringChars: + Enabled: true + +Performance/RegexpMatch: + Enabled: true + +Performance/ReverseEach: + Enabled: true + +Performance/ReverseFirst: + Enabled: true + +Performance/SelectMap: + Enabled: false + +Performance/Size: + Enabled: true + +Performance/SortReverse: + Enabled: true + +Performance/Squeeze: + Enabled: true + +Performance/StartWith: + Enabled: true + +Performance/StringBytesize: + Enabled: true + +Performance/StringIdentifierArgument: + Enabled: true + +Performance/StringInclude: + Enabled: true + +Performance/StringReplacement: + Enabled: true + +Performance/Sum: + Enabled: false + +Performance/TimesMap: + Enabled: true + +Performance/UnfreezeString: + Enabled: true + +Performance/UriDefaultParser: + Enabled: true + +Performance/ZipWithoutBlock: + Enabled: true + +Rails/FilePath: + Enabled: true + EnforcedStyle: slashes + +Rails/RootPathnameMethods: + Enabled: true + +RSpec/EmptyLineAfterExample: + Enabled: true + +RSpec/EmptyLineAfterExampleGroup: + Enabled: true + +RSpec/HookArgument: + Enabled: true + +Style/BlockDelimiters: + Enabled: true + +Style/Dir: + Enabled: true + +Style/Encoding: + Enabled: true + +Style/ExpandPathArguments: + Enabled: true + +Style/FrozenStringLiteralComment: + Enabled: true + EnforcedStyle: always + +Style/HashSyntax: + Enabled: true + EnforcedShorthandSyntax: never + +Style/ParallelAssignment: + Enabled: true + +Style/StringLiterals: + Enabled: false + +Style/TrailingCommaInArguments: + Enabled: true diff --git a/.simplecov b/.simplecov new file mode 100644 index 00000000000..b02b8a08d9d --- /dev/null +++ b/.simplecov @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +SimpleCov.profiles.define "activeadmin" do + # Opt out of SimpleCov 1.0's default test_frameworks profile to reintroduce + # `/features` coverage and surface dead code there. + remove_filter %r{\A(test|features|spec|autotest)/} + skip %r{\Aspec/} + skip "tmp/development_apps/" + skip "tmp/test_apps/" + skip "tasks/test_application.rb" + + if ENV["COVERAGE"] == "true" + require "simplecov-cobertura" + formatter SimpleCov::Formatter::CoberturaFormatter + end +end diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0a4d34e38e2..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -script: bundle exec rake test -rvm: - - 1.9.3 - - 2.0.0 -before_install: - - gem update --system - - gem --version - - gem install bundler -env: - - RAILS=3.0.20 - - RAILS=3.1.12 - - RAILS=3.2.13 -matrix: - allow_failures: - - rvm: 2.0.0 diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 00000000000..b48d900243b --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,18 @@ +# https://yamllint.readthedocs.io/en/stable/configuration.html +extends: default +ignore: | + node_modules/ + tmp/ + vendor/ + cucumber.yml +rules: # https://yamllint.readthedocs.io/en/stable/rules.html + comments: + min-spaces-from-content: 1 + document-start: disable + line-length: disable + truthy: + allowed-values: + - "true" + - "false" + - "on" + - "off" diff --git a/.yardopts b/.yardopts deleted file mode 100644 index e05df33c368..00000000000 --- a/.yardopts +++ /dev/null @@ -1,8 +0,0 @@ -lib/**/*.rb ---protected ---no-private ---asset docs/images:images -- -README.rdoc -CHANGELOG.md -docs/**/*.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ca9e1ba1f2..dfb583197cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,894 +1,1033 @@ -## Master (unreleased) - [compare](https://github.com/gregbell/active_admin/compare/v0.6.0...master) +# Changelog -### Features +[Future changelogs have moved to GitHub Releases](https://github.com/activeadmin/activeadmin/releases) -* OmniAuth provider links now automatically appear on the login page [#2088][] by [@henrrrik][] -* Menu items can now properly overflow [#2046][] by [@maax][]; later updated in [#2125][] by [@ball-hayden][] +## 3.2.0 [☰](https://github.com/activeadmin/activeadmin/compare/v3.1.0..v3.2.0) + +### Security Fixes + +* Backport protect against CSV Injection. [#8167] by [@mgrunberg] + +### Enhancements + +* Backport support citext column type in string filter. [#8165] by [@mgrunberg] +* Backport provide detail in DB statement timeout error for filters. [#8163] by [@mgrunberg] ### Bug Fixes -* Fixes problem where extra `/` route was being generated [#2062][] by [@jbhannah][] -* `IndexAsBlog` now renders title/body procs in the view context [#2087][] by [@macfanatic][] -* Fixes `route_instance_path` for `belongs_to` resources [#2099][] by [@pcreux][] -* Fixes breadcrumb links for `belongs_to` resources [#2090][] by [@Daxter][] -* Forces a `I18n.reload!` to ensure translations are loaded in production [#2072][] [@ericpromislow][] +* Backport make sure menu creation does not modify menu options. [#8166] by [@mgrunberg] +* Backport ransack error with filters when ActiveStorage is used. [#8164] by [@mgrunberg] + +## 3.1.0 [☰](https://github.com/activeadmin/activeadmin/compare/v3.0.0..v3.1.0) ### Enhancements -* Adds option to "undecorate" resource when building forms [#2085][] by [@amiel][] -* Adds [better_errors](https://github.com/charliesome/better_errors) gem for a better AA development experience [#2095][] by [@Daxter][] -* Scopes now support blocks for the `:default` option [#2084][] by [@macfanatic][] -* `:if` and `:unless` options added to `scope_to` [#2089][] by [@macfanatic][] -* German (Switzerland), English (UK) locales added [#1916][] by [@psy-q][] -* Renames Comment to AdminComment [#2060][] by [@jbhannah][]; later replaced by [#2113][] -* Improves Comments UI and adds config settings [#2113][] by [@Daxter][] -```ruby - config.show_comments_in_menu = false # Defaults to true - config.comments_registration_name = 'AdminComment' # Defaults to 'Comment' -``` +* Support Rails 7.1. [#8102] by [@mgrunberg] +* Remove deprecated usage of ActiveSupport::Deprecation singleton. [#8106] by [@mgrunberg] +* Replace to_formatted_s with to_s to convert date to string. [#8105] by [@mgrunberg] +* Remove upper bound dependency limits from gemspec. [#8098] by [@javierjulio] -* `has_many` forms - * Adds 'has_many_delete' CSS class to `li` elements [#2054][] by [@shekibobo][] - * Adds `:heading` option to customize the heading displayed [#2068][] by [@coreyward][] - * Adds `:allow_destroy` option to add in a checkbox to the form to delete records [#2071][] by [@shekibobo][] +## 3.0.0 [☰](https://github.com/activeadmin/activeadmin/compare/v2.14.0..v3.0.0) -### Cleanup +### Breaking Changes -* Cucumber step definitions refactor [#2015][] by [@Daxter][] -* Misc cleanup in [#2075][] and [#2107][] by [@Daxter][] -* Removes messy spacing from `AdminUser` generator file [#2058][] by [@lupinglade][] -* Fixes documentation formatting [#2083][] by [@amiel][] +* Remove custom Ransack predicates that were MetaSearch backports. [#8010] by [@javierjulio] +* Require Ransack v4. [#8009] by [@javierjulio] -## 0.6.0 - [compare](https://github.com/gregbell/active_admin/compare/v0.5.1...v0.6.0) +### Enhancements -### Bug Fixes +* Use display name fallback if blank display name result. [#6342] by [@javierjulio] + +### Translation Improvements + +* Improve Swedish translations. [#7993] by [@carlottostromstedt] -* Fix conflict with Redcloth [#1805][] by [@adrienkohlbecker][] -* Add missing batch actions translations. [#1788][] by [@EtienneDepaulis][] -* JS fix for batch actions checkbox toggling [#1947][] by [@ai][] -* Fixed routing bug for root namespace [#2043][] by [@Daxter][] and [@gregbell][] +## 2.14.0 [☰](https://github.com/activeadmin/activeadmin/compare/v2.13.1..v2.14.0) ### Enhancements -* Rubinis compatability change over block variables [#1871][] by [@dbussin][] -* Compatability with Draper 1.0 release [#1896][] by [@hakanensari][] -* Fixed references to "dashboards.rb" in locales, since file doesn't exist [#1873][] by [@ryansch][] -* Removing deprecated bourbon box-shadow mixin [#1913][] by [@stereoscott][] -* More Japanese localizations [#1929][] by [@johnnyshields][] -* Devise lockable module now supported by default [#1933][] by [@Bishop][] -* Index table now uses a unique DOM id (`#index_table_posts` instead of `#posts`) [#1966][] by [@TiagoCardoso1983][] -* Coffeescript 1.5 compatability as constructors no longer return a value [#1940][] by [@ronen][] -* Allow options to be passed to the Abre element for rows in `attributes_table` [#1439][] by [@Daxter][] -* Gender neutral Spanish translations [#1973][] by [@laffinkippah][] -* Adds the ability to use `starts_with` and `ends_with` in string filters [#1962][] by [@rmw][] -* Adds support for translating resources when registered with `:as` [#2044][] by [@Daxter][] -* Scopes are no longer hidden when empty filter results [#1804][] by [@Daxter][] -* Dynamic scope names with procs [#2018][] by [@Daxter][] -* Filters now support the `:if` optional argument [#1801][] by [@Daxter][] -* Member & collection actions support multiple HTTP methods for the same action [#2000][] by [@rdsoze][] - -### Features - -* Authorization DSL including a default CanCan adapter [#1817][] by [@pcreux][] and [@gregbell][] -* New "actions" DSL for customizing actions on index listing [#1834][] by [@ejholmes][] -* Index title can now be set via a proc [#1861][] by [@jamesalmond][] -* Can now disable `download_links` per resource, index collection or globally throughout AA [#1908][] by [@TBAA][] -* Filters: add ability to search for blank/null fields with boolean search [#1893][] by [@whatcould][] -* New `navigation_menu` DSL for menu system [#1967][] by [@macfanatic][] and [@gregbell][] -* Support segmented control switch between different index styles [#1745][] by [@joshuacollins85][] - -### Other - -* Updated documentation for formtastic deprecated f.buttons [#1867][] by [@ericcumbee][] -* Copyright updated for 2013 [#1937][] by [@snapapps][] - -### Contributors - -327 Commits by 42 authors - -* Adrien Kohlbecker -* Alexandr Prudnikov -* Andrew Pietsch -* Andrey A.I. Sitnik -* Andrey Rozhkovsky -* Anthony Zacharakis -* Bartlomiej Niemtur -* David DIDIER -* David Reese -* Daxter -* Dirkjan Bussink -* Dominik Masur -* Eric Cumbee -* Eric J. Holmes -* Etienne Depaulis -* Gosha Arinich -* Greg Bell -* Ian Carroll -* James Almond -* Johnny Shields -* Joshua Collins -* Kieran Klaassen -* Luís Ramalho -* Matt Brewer -* Nathaniel Bibler -* Olek Janiszewski -* Philippe Creux -* Raison Dsouza -* Rebecca Miller-Webster -* Roman Sklenar -* Roman Sklenář -* Ryan Schlesinger -* Scott Meves -* Sean Ian Linsley -* Sergey Pchelincev -* Simon Menke -* Tiago Cardoso -* Travis Pew -* WU Jun -* laffinkippah -* ronen barzel -* тιηуηυмвєяѕ - -## 0.5.1 - [compare](https://github.com/gregbell/active_admin/compare/v0.5.0...v0.5.1) +* Add csp_meta_tag to layout. [#7986] by [@javierjulio] +* Update config.register_javascript with options support. [#7002] by [@lanzhiheng] +* Use `csrf_meta_tags` in place of singular version. [#7985] by [@javierjulio] +* Allow different new and edit rules in authorization adapters. [#6535] by [@timwis] + +### Bug Fixes + +* Fix form layout for hints and checkboxes. [#7772] by [@JewelSam] +* Update filters disabled error to include specific action. [#6195] by [@javawizard] +* Fix Comments controller destroy declaration. [#6482] by [@bliof] +* Stop pagination elements from overflowing outside of panel container. [#7599] by [@ray-curran] + +### Translation Improvements + +* Update vi locale with more translations. [#7984] by [@rs-phunt] +* Update zh-CN locale with multiple corrections. [#7944] by [@hfl] +* Fix typo in Vietnamese locale for filter text. [#7920] by [@tvziet] +* Improve French translation. [#7653] by [@cprodhomme] + +### Documentation + +* Add more documentation about PORO decorator requirements. [#7556] by [@sanfrecce-osaka] +* Add Load Paths docs to the active_admin.rb template. [#7541] by [@gabo-cs] + +### Performance + +* Removes docs from exported gem. [#7013] by [@brunoarueira] + +## 2.13.1 [☰](https://github.com/activeadmin/activeadmin/compare/v2.13.0..v2.13.1) + +### Bug Fixes + +* Honor load paths order when loading admin files. [#7488] by [@tf] +* Fix passing expected hash payload argument. [#7487] by [@ispyropoulos] + +## 2.13.0 [☰](https://github.com/activeadmin/activeadmin/compare/v2.12.0..v2.13.0) + +### Documentation + +* Update validation errors documentation to account for deprecated `ActiveModel::Errors#keys`. [#7475] by [@amit] + +### Dependency Changes + +* Drop rails 6.0 support. [#7476] by [@deivid-rodriguez] + +### Performance + +* Fix pundit performance. [#7479] by [@deivid-rodriguez] + +## 2.12.0 [☰](https://github.com/activeadmin/activeadmin/compare/v2.11.2..v2.12.0) ### Enhancements -* Developer can pass options for CSV generation. [#1626][] by [@rheaton][] -```ruby - ActiveAdmin.register Post do - csv options: {force_quotes: true} do - column :title - end - end -``` - -* Breadcrumb links can be customized by [@simonoff][] -```ruby - ActiveAdmin.register Post do - breadcrumb do - [ - link_to("My account", account_path(current_user)) - ] - end - end -``` +* Add Ransack 3 compatibility. [#7453] by [@tagliala] -* Support proc for parent options on menus [#1664][] by [@shell][] -```ruby - ActiveAdmin.register Post do - menu parent: proc { I18n.t("admin") } - end -``` +### Bug Fixes -* Support automatic use of Decorators. [#1117][] by [@amiel][] and [#1647][] by [@dapi][] -```ruby - ActiveAdmin.register Post do - decorate_with PostDecorator - end -``` +* Fix pundit namespace detection. [#7144] by [@vlad-psh] -* Allow blacklisting of filters with 'remove_filter' [#1609][] by [@tracedwax][] -```ruby - ActiveAdmin.register Post do - remove_filter :author - end -``` +### Documentation -* ActiveAdmin i18n translations can be overwritten in your rails -application locales. [#1775][] by [@caifara][] -* Add "Powered by" to translations. [#1783][] by [@sunny][] -* Forms accept two level deeps has_many. [#1699][] by [@kerberoS][] and tests in [#1782][] by [@ptn][] -* Extract download_format_links into helper [#1752][] by [@randym][] -* Add support for semantic errors [#905][] by [@robdiciuccio][] -* Add support for boolean inputs [#1668][] by [@orendon][] -* Support subURI on logout [#1681][] by [@yawn][] +* Don't mention webpacker as the default asset generator in Rails. [#7377] by [@jaynetics] -### Bug fix -* Apply before_filter to BaseController [#1683][] by [@yorch][] -* ... and much more. +### Performance -### Contributions +* Avoid duplicate work when downloading CSV. [#7336] by [@deivid-rodriguez] -156 commits (49 Pull Requests) by 51 contributors. +## 2.11.2 [☰](https://github.com/activeadmin/activeadmin/compare/v2.11.1..v2.11.2) -## 0.5.0 - [compare](https://github.com/gregbell/active_admin/compare/v0.4.4...v0.5.0) +### Bug Fixes + +* Fix disappearing BOM option for `CSVBuilder`. [#7170] by [@Karoid] + +## 2.11.1 [☰](https://github.com/activeadmin/activeadmin/compare/v2.11.0..v2.11.1) ### Enhancements -* Created new view components (Footer, TitleBar, Header, UtilityNav) to more - easily customize the views in Active Admin and per namespace. ([@gregbell][]) -* All CSS is now encapsulated under the `body.active_admin` class. This may - change the precedence of styles that you created to override or use in - other areas of your application. -* Dashboards are now implemented as pages. For more details of how to configure - a page, checkout http://activeadmin.info/docs/9-custom-pages.html -* Root route can be set to any controller#action using `#root_to`. -* Batch Actions allows you to select entries on index page and perform - an action against them. -* CSV separators are configurable. -* Lot of bug fixes. +* Add turbolinks support to has many js. [#7384] by [@amiel] -### Deprecations +### Documentation -* Removed all references to ActiveAdmin::Renderer. If you were using these - please update code to use an Arbre component. Removed - `ActiveAdmin:Views::HeaderRender` and replaced with - `ActiveAdmin::Views::Header` component. -* ActiveAdmin::Menu and ActiveAdmin::MenuItem API has changed. If you were - creating custom menu items, the builder syntax has changed to. Menu#add now - accepts a MenuItem, instead of building the menu item for you. -* `ActiveAdmin::Dashboards.build` is deprecated in favour of generating a page - and using the new `config.root_to` option. -* Arbre is now a gem on its own. +* Remove `insert_tag` from Form-Partial docs. [#7394] by [@TonyArra] -### Contributions +## 2.11.0 [☰](https://github.com/activeadmin/activeadmin/compare/v2.10.1..v2.11.0) -561 commits (142 Pull Requests) by 88 contributors. +### Enhancements -## 0.4.4 - [compare](https://github.com/gregbell/active_admin/compare/v0.4.3...v0.4.4) +* Add Rails 7 Support. [#7235] by [@tagliala] -### Dependencies +### Bug Fixes + +* Fix form SCSS variables no longer being defined in the outermost scope, so no longer being accessible. [#7341] by [@gigorok] -* Use `formtastic` ~> 2.1.1 until AA 0.5.0 is released -* Use `inherited_resources` >= 1.3.1 (ensure flash messages work) +## 2.10.1 [☰](https://github.com/activeadmin/activeadmin/compare/v2.10.0..v2.10.1) + +### Enhancements -## 0.4.3 - [compare](https://github.com/gregbell/active_admin/compare/v0.4.2...v0.4.3) +* Apply `box-sizing: border-box` globally. [#7349] by [@deivid-rodriguez] +* Vendor normalize 8.0.1. [#7350] by [@deivid-rodriguez] +* Remove deprecation warning using controller filters inside initializer. [#7340] by [@mgrunberg] ### Bug Fixes -* [#1063][]: Fix comment issues when using postgres ([@jancel][]) +* Fix frozen string error when downloading CSV and streaming disabled. [#7332] by [@deivid-rodriguez] -## 0.4.2 - [compare](https://github.com/gregbell/active_admin/compare/v0.4.1...v0.4.2) +## 2.10.0 [☰](https://github.com/activeadmin/activeadmin/compare/v2.9.0..v2.10.0) ### Enhancements -* [#822][]: Automatically include js and css to precompile list ([@jschwindt][]) -* [#1033][]: Site title accepts a proc that is rendered in the context - of the view ([@pcreux][]) -* [#70][]: Form blocks are now rendered within the context of the view ([@gregbell][]) -* [#70][]: Filter's collections are now eval'd in the context of the view ([@gregbell][]) -* [#1032][]: The html body now includes a class for the namespace name ([@mattvague][]) -* [#1013][]: Hide the count from one specific scope using `:show_count => false` - ([@latortuga][]) -* [#1023][]: Add localization support for comments ([@MoritzMoritz][]) +* Load favicon from Webpacker assets when use_webpacker is set to true. [#6954] by [@Fs00] +* Don't apply sorting to collection until after scoping. [#7205] by [@agrobbin] +* Resolve dart sass deprecation warning for division. [#7095] by [@tordans] +* Use `instrument` from the Notifications API instead of low level `publish`. [#7262] by [@sprql] +* Avoid mutating string literals. [#6936] by [@tomgilligan] +* Include print styles in main stylesheet. [#6922] by [@deivid-rodriguez] +* Use `POST` for OmniAuth links. [#6916] by [@deivid-rodriguez] +* Scope new record instantiation by authorization scope. [#6884] by [@ngouy] +* Make `permit_params` and `belongs_to` order independent. [#6906] by [@deivid-rodriguez] +* Use collection length instead of running COUNTs for limited collections. [#5660] by [@MmKolodziej] ### Bug Fixes -* [#34][]: Comments now work with models using string ids ([@jancel][]) -* [#1041][]: When `table_for` collection is empty it no longer outputs - a blank array in Ruby 1.9 ([@jancel][], [#1016][]) -* [#983][]: Fixed compatibility with pry-rails ([@pcreux][]) -* [#409][]: Install generator handles custom class names for user ([@gregbell][]) +* Show ransackable_scopes filters in search results. [#7127] by [@vlad-psh] -### Contributors +### Translation Improvements -42 Commits by 10 authors +* Fix Dutch translation for password reset button. [#7181] by [@mvz] +* Add few key to RO pagination.entry. [#6915] by [@lubosch] +* Change misleading Korean translation. [#6873] by [@1000ship] -* Allen Huang -* Daniel Lepage -* Thibaut Barrère -* Drew Ulmer -* Juan Schwindt -* Moritz Behr -* Jeff Ancel -* Matt Vague -* Greg Bell -* Philippe Creux +### Documentation +* Replace deprecated update_attributes! with update!. [#6959] by [@sergey-alekseev] +* Clarify docs on user setup. [#6872] by [@javawizard] -## 0.4.1 - [compare](https://github.com/gregbell/active_admin/compare/v0.4.0...v0.4.1) +### Dependency Changes + +* Drop rails 5.2 support. [#7293] by [@deivid-rodriguez] +* Drop support for Ruby 2.5. [#7236] by [@alejandroperea] + +## 2.9.0 [☰](https://github.com/activeadmin/activeadmin/compare/v2.8.1..v2.9.0) ### Enhancements -* [#865][]: Pages support the `#page_action` to add custom controller actions - to a page ([@BoboFraggins][]) -* Columns component now supports column spans, max and min widths ([@gregbell][]) -* [#497][]: Custom pagination settings per resource ([@pcreux][]) -* [#993][]: Login form now focuses on email ([@mattvague][]) -* [#865][]: Add `:if` support to sidebar sections ([@BoboFraggins][]) -* [#865][]: Added `:scope_count => false` to the index to hide scope counts - in generated scopes ([@BoboFraggins][]) +* Support for Rails 6.1. [#6548] by [@deivid-rodriguez] +* Add ability to override "Remove" button text on has_many forms. [#6523] by [@littleforest] +* Drop git in gemspec. [#6462] by [@utkarsh2102] ### Bug Fixes -* [#101][]: Global nav now works with RackBaseURI ([@gregbell][]) -* [#960][]: Global nav works when scoped in rails routes ([@gregbell][]) -* [#994][]: Fix index page check collection.limit(1).exists? causes exception when - ordering by virtual colum ([@latortuga][], [@gregbell][]) -* [#971][]: Fix SQL when sorting tables with a column named "group" ([@ggilder][]) +* Pick up upstream fixes in devise templates. [#6536] by [@munen] -### Dependencies +### Documentation -* [#978][]: Support for Inherited Resources 1.3.0 ([@fabiormoura][]) +* Fix `has_many` syntax in forms documentation. [#6583] by [@krzcho] +* Add example of using `default_main_content` in show pages. [#6487] by [@sjieg] -### Contributors +### Dependency Changes -75 Commits by 12 authors +* Remove sassc and sprockets runtime dependencies. [#6584] by [@deivid-rodriguez] -* Bruno Bonamin -* David Radcliffe -* Dinesh Majrekar -* Erik Michaels-Ober -* Fábio Maia -* Gabriel Gilder -* Greg Bell -* Kyle Macey -* Matt Vague -* Oldani Pablo -* Peter Fry -* Philippe Creux -* Søren Houen +## 2.8.1 [☰](https://github.com/activeadmin/activeadmin/compare/v2.8.0..v2.8.1) +### Bug Fixes -## 0.4.0 - [compare](https://github.com/gregbell/active_admin/compare/v0.3.4...v0.4.0) +* Fix `permitted_param` generation for `belongs_to` when `:param` is used. [#6460] by [@deivid-rodriguez] +* Fix streaming CSV export. [#6451] by [@deivid-rodriguez] +* Fix input string filter no rendering dropdown input when its column name ends with a ransack predicate. [#6422] by [@Fivell] -### Upgrade Notes +## 2.8.0 [☰](https://github.com/activeadmin/activeadmin/compare/v2.7.0..v2.8.0) -If you're running on Rails 3.0.x, make sure to run `rails generate active_admin:assets` -since we've changed both the CSS and JS files. +### Enhancements -### Deprecations +* Allow using PORO decorators. [#6249] by [@brunvez] +* Make sure `ActiveAdmin.routes` provides routes in a consistent order. [#6124] by [@jiikko] +* Use proper closing tags for HTML in ModalDialog component. [#6221] by [@javierjulio] + +### Bug Fixes + +* Fix comment layout so regardless of size, each is aligned and spaced evenly. [#6393] by [@Ivanov-Anton] + +### Translation Improvements + +* Fix several Arabic translations. [#6368] by [@mshalaby] +* Add missing `scope/all` italian translation. [#6341] by [@fuzziness] +* Improve Japanese translation. [#6315] by [@rn0rno] +* Fix es and es-MX sign_in and sign_up translation. [#6210] by [@roramirez] + +### Documentation + +* Fix filter_columns_for_large_association and filter_method_for_large_association examples. [#6232] by [@ndbroadbent] + +### Dependency Changes + +* Allow formtastic 4. [#6318] by [@deivid-rodriguez] +* Drop Ruby 2.4 support. [#6198] by [@deivid-rodriguez] + +## 2.7.0 [☰](https://github.com/activeadmin/activeadmin/compare/v2.6.1..v2.7.0) + +### Enhancements + +* Extend menu to allow for nested submenus. [#5994] by [@taralbass] +* Add Webpacker compatibility with opt-in config switch and installation generator. [#5855] by [@sgara] + +### Bug Fixes + +* Fix scopes renderer when resource has only optional scopes and their conditions are false. [#6149] by [@Looooong] +* Fix some missing wrapper markup in "logged out" layout. [#6086] by [@irmela] +* Fix some typos in Vietnamese translation. [#6099] by [@giapnhdev] + +## 2.6.1 [☰](https://github.com/activeadmin/activeadmin/compare/v2.6.0..v2.6.1) + +### Bug Fixes + +* Fix some ruby 2.7 warnings about keyword args. [#6000] by [@vcsjones] +* Missing `create_another` translation in Vietnamese. [#6002] by [@imcvampire] +* Using "destroy" for user facing message is too robotic, prefer "delete". [#6047] by [@vfonic] +* Typo in confirmation message for comment deletion. [#6047] by [@vfonic] + +## 2.6.0 [☰](https://github.com/activeadmin/activeadmin/compare/v2.5.0..v2.6.0) + +### Enhancements + +* Display multiple flash messages in separate elements. [#5929] by [@mirelon] +* Make delete confirmation messages in French & Spanish gender-neutral. [#5946] by [@cprodhomme] -* In the initializer `config.allow_comments_in = []` is now - `config.allow_comments = true`. Use the new namespace specific configurations - to allow or disallow configuration within a specific namespace. -* Removed `Object#to_html` in favour of `to_s`. Any Arbre components - implementing a `to_html` method need to be updated to use `to_s` instead. +### Bug Fixes -### New Features +* Export ModalDialog component to re-enable client side usage. [#5956] by [@sgara] +* Use default ActionView options instead of default Formtastic options for DateRangeInput [#5957] by [@mirelon] +* Fix i18n key in docs example to translate scopes. [#5943] by [@adler99] -* Namespace specific configurations in the initializer ([@gregbell][]) -* [#624][]: Set an image as the site title using `config.site_title_image` in the - Active Admin initializer. ([@mattvague][]) -* [#758][]: Create a standalone page in Active Admin using - `ActiveAdmin.register_page` ([@pcreux][]) -* [#723][]: Register stylesheets with a media type ([@macfanatic][]) +## 2.5.0 [☰](https://github.com/activeadmin/activeadmin/compare/v2.4.0..v2.5.0) ### Enhancements -* [#428][]: Paginated Collection now supports `:param_name` and `:download_links`. - These two additions allow you to use the `paginated_collection` component multiple - times on show screens. ([@samvincent][]) -* [#527][]: Refactored all form helpers to use Formtastic 2([@ebeigarts][]) -* [#551][]: Dashboards can now be conditionally displayed using `:if` ([@samvincent][]) -* [#555][]: scopes now accept `:if`. They only get displayed if the proc returns true ([@macfanatic][]) -* [#601][]: Breadcrumbs are internationalized ([@vairix-ssierra][]) -* [#605][]: Validations on ActiveAdmin::Comment should work with - `accepts_nested_attributes_for` ([@DMajrekar ][]) -* [#623][]: Index table can sort on any table using `:sort => 'table.column'` ([@ZequeZ][]) -* [#638][]: Add `:label` option to `status_tag` component ([@fbuenemann][]) -* [#644][]: Added proper I18n support to pagination ([@fbuenemann][]) -* [#689][]: Scopes preserve title when provided as a string ([@macfanatic][]) -* [#711][]: Styles update. Now sexier and more refined design. Redesigned Scopes. Split - css into smaller files. ([@mattvague][]) -* [#741][]: Default media type of css is now "all" instead of "screen" ([@sftsang][]) -* [#751][]: Pagination numbers work with a custom `[@per_page][]` ([@DMajrekar][]) -* `default_actions` in an index table only display implemented actions ([@watson][]) +* Azerbaijani translation. [#5078] by [@orkhan] ### Bug Fixes -* [#590][]: Comments now work in the default namespace ([@jbarket][]) -* [#780][]: Fix stack level too deep exception when logout path is setup to use - `:logout_path` named route. ([@george][]) -* [#637][]: Fix scope all I18n ([@fbuenemann][]) -* [#496][]: Remove global `Object#to_html` [@ebeigarts][] -* [#470][], [#154][]: Arbre properly supports blocks that return Numeric values - ([@bobbytables][], [@utkarshkukreti][], [@gregbell][]) -* [#897][]: Fix count display for collections with GROUP BY [@comboy][] - -### Dependencies - -* [#468][]: Removed vendored jQuery. Now depends on the jquery-rails gem. If you're - running Rails 3.0.x (no asset pipeline), make sure to run - `rails generate active_admin:assets` to generate the correct files. ([@gregbell][]) -* [#527][]: Active Admin now requires Formtastic 2.0 or greater ([@ebeigarts][]) -* [#711][]: Active admin now depends on Bourbon > 1.0.0. If you're using Rails - 3.0.x, make sure to run `rails generate active_admin:assets` to ensure you - have the correct css files ([@mattvague][]) -* [#869][]: Upgraded Kaminari to >= 0.13.0 and added support for - `Kaminari.config.page_method_name`. Active Admin should now be happy if - `will_paginate` is installed with it. ([@j][]-manu) -* [#931][]: Support for Rails 3.2 added ([@mperham][]) - -### Contributors - -274 commits by 42 authors - - * Greg Bell - * Philippe Creux - * Matt Vague - * Felix Bünemann - * Matthew Brewer - * Edgars Beigarts - * Mike Perham - * Sam Vincent - * Kieran Klaassen - * Jonathan Barket - * Ankur Sethi - * Dinesh Majrekar - * comboy - * Juan E. Pemberthy - * Leandro Moreira - * Manu - * Marc Riera - * Radan Skorić - * Rhys Powell - * Sebastian Sierra - * Sherman Tsang - * Szymon Przybył - * Thomas Watson Steen - * Tim Habermaas - * Yara Mayer - * Zequez - * asancio - * emmek - * Alexey Noskov - * igmizo - * Alli - * Bendik Lynghaug - * Douwe Homans - * Eric Koslow - * Eunsub Kim - * Garami Gábor - * George Anderson - * Henrik Hodne - * Ivan Storck - * Jeff Dickey - * John Ferlito - * Josef Šimánek - - -## 0.3.4 - [compare](https://github.com/gregbell/active_admin/compare/v0.3.3...v0.3.4) - -2 commits by 2 authors +* Convert namespace to sym to prevent duplicate namespaces such as :foo and 'foo'. [#5931] by [@westonganger] +* Use filter label when condition has a predicate. [#5886] by [@ko-lem] +* Fix error when routing with array containing symbol. [#5870] by [@jwesorick] +* Fix error when there is a model named `Tag` and `meta_tags` have been configured. [#5895] by [@micred], [@FabioRos] and [@deivid-rodriguez] +* Allow specifying custom `input_html` for `DateRangeInput`. [#5867] by [@mirelon] +* Adjust `#main_content` right margin to take into account possible custom values of `$sidebar-width` and `$section-padding`. [#5887] by [@guigs] +* Improved polymorphic routes generation to avoid problems when multiple `belongs_to` are defined. [#5938] by [@leio10] + +### Dependency Changes + +* Support for Rails 5.0 and Rails 5.1 has been dropped. [#5877] by [@deivid-rodriguez] + +## 2.4.0 [☰](https://github.com/activeadmin/activeadmin/compare/v2.3.1..v2.4.0) + +### Enhancements + +* Make optimization to not use expensive COUNT queries also work for decorated actions. [#5811] by [@irmela] +* Render a text filter instead of a select for large associations (opt-in). [#5548] by [@DanielHeath] +* Improve German translations. [#5874] by [@juril33t] + +## 2.3.1 [☰](https://github.com/activeadmin/activeadmin/compare/v2.3.0..v2.3.1) ### Bug Fixes -* Fix reloading issues across operating systems. -* Fix issue where SASS was recompiling on every request. This can seriously - decrease the load time of applications when running in development mode. - Thanks [@dhiemstra][] for tracking this one down! +* Revert ransack version pinning because 2.3 has an outstanding bug that affects quite a lot of users. See [this ransack issue](https://github.com/activerecord-hackery/ransack/issues/1039) for more information. [#5854] by [@deivid-rodriguez] -### Contributors +## 2.3.0 [☰](https://github.com/activeadmin/activeadmin/compare/v2.2.0..v2.3.0) -* Danny Hiemstra -* Greg Bell +### Enhancements + +* Bump minimum ransack requirement to make sure everyone gets a version that works ok with all supported versions of Rails. [#5831] by [@deivid-rodriguez] -## 0.3.3 - [compare](https://github.com/gregbell/active_admin/compare/v0.3.2...v0.3.3) +### Bug Fixes -1 commit by 1 author +* Fix CSVBuilder not respecting `ActiveAdmin.application.csv_options = { humanize_name: false }` setting. [#5800] by [@HappyKadaver] +* Fix crash when displaying current filters after filtering by a nested resource. [#5816] by [@deivid-rodriguez] +* Fix pagination when `pagination_total` is false to not show a "Last" link, since it's incorrect because we don't have the total pages information. [#5822] by [@deivid-rodriguez] +* Fix optional nested resources causing incorrect routes to be generated, when renamed resources (through `:as` option) are involved. [#5826] by [@ndbroadbent], [@Kris-LIBIS] and [@deivid-rodriguez] +* Fix double modal issue in applications using turbolinks 5. [#5842] by [@sgara] + +## 2.2.0 [☰](https://github.com/activeadmin/activeadmin/compare/v2.1.0..v2.2.0) ### Enhancements -* Only reload Active Admin when files in the load paths have changed. This is a - major speed increase in development mode. Also helps with memory consumption - because we aren't reloading Active admin all the time. +* The `status_tag` component now supports different labels for `false` and `nil` boolean cases through the locale. Both default to display "No" for backwards compatibility. [#5794] by [@javierjulio] +* Add Macedonian locale. [#5710] by [@violeta-p] -### Contributors +### Bug Fixes + +* Fix pundit policy retrieving for static pages when the pundit namespace is `:active_admin`. [#5777] by [@kwent] +* Fix show page title not being properly escaped if title's content included HTML. [#5802] by [@deivid-rodriguez] +* Revert [21b6138f] from [#5740] since it actually caused the performance in development to regress. [#5801] by [@deivid-rodriguez] + +## 2.1.0 [☰](https://github.com/activeadmin/activeadmin/compare/v2.0.0..v2.1.0) + +### Bug Fixes -* Greg Bell +* Ensure application gets reloaded only once. [#5740] by [@jscheid] +* Crash when rendering comments from a custom controller block. [#5758] by [@deivid-rodriguez] +* Switch `sass` dependency to `sassc-rails`, since `sass` is no longer supported and since it restores support for directly importing `css` files. [#5504] by [@deivid-rodriguez] -## 0.3.2 - [compare](https://github.com/gregbell/active_admin/compare/v0.3.1...v0.3.2) +### Dependency Changes -45 commits by 15 contributors +* Support for ruby 2.3 has been removed. [#5751] by [@deivid-rodriguez] + +## 2.0.0 [☰](https://github.com/activeadmin/activeadmin/compare/v2.0.0.rc2..v2.0.0) + +_No changes_. + +## 2.0.0.rc2 [☰](https://github.com/activeadmin/activeadmin/compare/v2.0.0.rc1..v2.0.0.rc2) ### Enhancements -* Site title can now be a link. Use config.site_title_link in - config/initializers/active_admin.rb -* i18n support for Japanese -* i18n support for Bulgarian -* i18n support for Swedish +* Require arbre `~> 1.2, >= 1.2.1`. [#5726] by [@ionut998], and [#5738] by [@deivid-rodriguez] + +## 2.0.0.rc1 [☰](https://github.com/activeadmin/activeadmin/compare/v1.4.3..v2.0.0.rc1) + +### Enhancements + +* Add your own content to the site ``, like analytics. [#5590] by [@buren] + + ```ruby + ActiveAdmin.setup do |config| + config.head = ''.html_safe + end + ``` + +* Consider authorization when displaying comments in show page. [#5555] by [@amiuhle] +* Add better support for rendering lists. [#5370] by [@dkniffin] +* Undeprecate `config.register_stylesheet` and `config.register_javascript` for lack of better solution for including external assets. It might be reevaluated in the future. [#5662] by [@deivid-rodriguez] + +### Security Fixes + +* Prevent leaking hashed passwords via user CSV export and adds a config option for sensitive attributes. [#5486] by [@chrp] ### Bug Fixes -* DeviseGenerator respects singular table names -* Active Admin no longer assumes sass-rails is installed -* Arbre::Context passes methods to the underlying html which fixes - issues on different types of servers (and on Heroku) -* [#45][]: Fix for multibyte characters ([@tricknotes][]) -* [#505][]: Fix titlebar height when no breadcrumb ([@mattvague][]) -* Fixed vertical align in dashboard -* Fixed i18n path's for multi-word model names +* Fix for paginated collections with `per_page: Array, pagination_total: false`. [#5627] by [@bartoszkopinski] +* Restrict ransack requirement to >= 2.1.1 to play nice with Rails 5.2.2. [#5632] by [@deivid-rodriguez] +* Bad interpolation variables on pagination keys in Lithuanian translation. [#5631] by [@deivid-rodriguez] +* Tabs are not correctly created when using non-transliteratable characters as title. [#5650] by [@panasyuk] +* Sidebar title internationalization. [#5417] by [@WaKeMaTTa] +* `filter` labels not allowing a `Proc` to be passed. [#5418] by [@WaKeMaTTa] -### Dependencies +### Dependency Changes -* Formtastic 2.0.0 breaks Active Admin. Locking to < 2.0.0 +* Rails 4.2 support has been dropped. [#5104] by [@javierjulio] and [@deivid-rodriguez] +* Dependency on coffee-rails has been removed. [#5081] by [@javierjulio] + If your application uses coffescript but was relying on ActiveAdmin to provide + the dependency, you need to add the `coffee-script` gem to your `Gemfile` to + restore it. If your only usage of coffescript was the + `active_admin.js.coffee` generated by ActiveAdmin's generator, you can also + convert that file to plain JS (`//= require active_admin/base` if you + didn't add any stuff to it). +* Devise 3 support has been dropped. [#5608] by [@deivid-rodriguez] and [@javierjulio] +* `action_item` without a name has been removed. [#5099] by [@javierjulio] -### Contributors +## 1.4.3 [☰](https://github.com/activeadmin/activeadmin/compare/v1.4.2..v1.4.3) -* Amiel Martin -* Christian Hjalmarsson -* Edgars Beigarts -* Greg Bell -* Jan Dupal -* Joe Van -* Mark Roghelia -* Mathieu Martin -* Matt Vague -* Philippe Creux -* Ryunosuke SATO -* Sam Vincent -* Trace Wax -* Tsvetan Roustchev -* l4u +### Bug Fixes -## 0.3.1 - [compare](https://github.com/gregbell/active_admin/compare/v0.3.0...v0.3.1) +* Fix `form` parameter to `batch_action` no longer accepting procs. [#5611] by [@buren] and [@deivid-rodriguez] +* Fix passing a proc to `scope_to`. [#5611] by [@deivid-rodriguez] -* Only support InheritedResources up to 1.2.2 +## 1.4.2 [☰](https://github.com/activeadmin/activeadmin/compare/v1.4.1..v1.4.2) -## 0.3.0 - [compare](https://github.com/gregbell/active_admin/compare/v0.2.2...v0.3.0) +### Bug Fixes -326 commits by 35 contributors +* Fix `input_html` filter option evaluated only once. [#5376] by [@kjeldahl] -### New Features +## 1.4.1 [☰](https://github.com/activeadmin/activeadmin/compare/v1.4.0..v1.4.1) -* I18n! Now supported in 10 languages! -* Customizeable CSV ([@pcreux][], [@gregbell][]) -* Menus now support `if` and `priority` (Moritz Lawitschka) -* Rails 3.1 support -* Asset pipeline support ([@gregbell][]) -* `skip_before_filter` now supported in DSL ([@shayfrendt][]) -* Added a blank slate design ([@mattvague][]) -* Collection and Member actions use the Active Admin layout ([@gregbell][]) +### Bug Fixes + +* Fix menu item link with method delete. [#5583] by [@tiagotex] + +## 1.4.0 [☰](https://github.com/activeadmin/activeadmin/compare/v1.3.1..v1.4.0) ### Enhancements -* Better I18n config file loading ([@fabiokr][]) -* `TableFor` now supports I18n headers ([@fabiokr][]) -* `AttributesTable` now supports I18n attributes ([@fabiokr][]) -* Member actions all use CSS class `member_link` ([@doug316][]) -* Made `status_tag` an Arbre component ([@pcreux][]) -* CSV downloads have sexy names such as "articles-2011-06-21.csv" ([@pcreux][]) -* Created `ActiveAdmin::Setting` to easily create settings ([@gregbell][]) -* New datepicker styles ([@mattvague][]) -* Set `[@page_title][]` in member action to render a custom title ([@gregbell][]) -* [#248][]: Settable logout link options in initializer ([@gregbell][]) -* Added a DependencyChecker that warns if dependencies aren't met ([@pcreux][]) +* Add missing I18n for comments. [#5458], [#5461] by [@mauriciopasquier] +* Fix batch_actions.delete_confirmation translation in zh-CN.yml. [#5453] by [@ShallmentMo] +* Add some missing italian translations. [#5433] by [@stefsava] +* Enhance some chinese translations. [#5413] by [@shouya] +* Add missing filter predicate translations to nb. [#5357] by [@rogerkk] +* Add missing norwegian comment translations. [#5375] by [@rogerkk] +* Add missing dutch translations. [#5368] by [@dennisvdvliet] +* Add missing german translations. [#5341] by [@eikes] +* Add missing spanish translation. [#5336] by [@mconiglio] +* Add from and to predicates for russian language. [#5330] by [@glebtv] +* Fix typo in finnish translation. [#5320] by [@JiiHu] +* Add missing turkish translations. [#5295] by [@kobeumut] +* Add missing chinese translations. [#5266] by [@jasl] +* Allow proc label in datepicker input. [#5408] by [@tiagotex] +* Add `group` attribute to scopes in order to show them in grouped. [#5359] by [@leio10] +* Add missing polish translations and improve existing ones. [#5537] by [@Wowu] +* Add `priority` option to `action_item`. [#5334] by [@andreslemik] ### Bug Fixes -* [#52][]: Fix update action with STI models ([@gregbell][]) -* [#122][]: Fix sortable columns on nested resources ([@knoopx][]) -* Fix so that Dashboard Sections can appear in root namespace ([@knoopx][]) -* [#131][]: Fixed `status_tag` with nil content ([@pcreux][]) -* [#110][]: Fixed dropdown menu floats in Firefox ([@mattvague][]) -* Use quoted table names ([@krug][]) -* Fixed CSS float of `.paginated_collection_contents` bug in Firefox ([@mattvague][]) -* Removed unwanted gradient in IE in attribute table headers ([@emzeq][]) -* [#222][]: Added `Arbre::Context#length` for Rack servers ([@gregbell][]) -* [#255][]: Fixed problem with dropdown menus in IE8 and IE9 ([@mattvague][]) -* [#235][]: Default sort order should use primary_key ([@gregbell][]) -* [#197][]: Fixed issues with #form params disappearing ([@rolfb][]) -* [#186][]: Fixes for when `default_namespace = false` ([@gregbell][]) -* [#135][]: Comments on STI classes redirect correctly ([@gregbell][]) -* [#77][]: Fixed performance issue where ActiveRecord::Base.all was being called ([@pcreux][]) -* [#332][]: Fixed Devise redirection when in false namespace ([@gregbell][]) -* [#171][]: Fixed issue where class names would clash with HTML object names ([@gregbell][]) -* [#381][]: Fixed issues with Devise < 1.2 ([@pcreux][]) -* [#369][]: Added support for pluralized model names such as News ([@gregbell][]) -* [#42][]: Default forms work with polymorphic associations ([@mattvague][]) - -### Dependencies - -* Switched from will_paginate to Kaminari for pagination ([@mwindwer][]) -* Removed dependency on InheritedViews ([@gregbell][]) -* Removed Jeweler. Using Bundler and a gemspec ([@gregbell][]) - -### Contributors - -* Armand du Plessis -* Aurelio Agundez -* Bruno Bonamin -* Chris Ostrowski -* Corey Woodcox -* DeLynn Berry -* Doug Puchalski -* Fabio Kreusch -* Greg Bell -* Ismael G Marin C -* Jackson Pires -* Jesper Hvirring Henriksen -* Josef Šimánek -* Jørgen Orehøj Erichsen -* Liborio Cannici -* Matt Vague -* Matthew Windwer -* Moritz Lawitschka -* Nathan Le Ray -* Nicolas Mosconi -* Philippe Creux -* Rolf Bjaanes -* Ryan D Johnson -* Ryan Krug -* Shay Frendt -* Steve Klabnik -* Tiago Rafael Godinho -* Toby Hede -* Vijay Dev -* Víctor Martínez -* doabit -* hoverlover -* nhurst -* whatthewhat -* Łukasz Anwajler - - -## 0.2.2 (2011-05-26) - [compare](https://github.com/gregbell/active_admin/compare/v0.2.1...v0.2.2) - -68 Commits by 13 Contributors - -### Features & Enhancements - -* Arbre includes self closing tags ([#100][]) -* Controller class & action added to body as CSS classes ([#99][]) -* HAML is not required by default ([#92][]) -* Devise login now respects Devise.authentication_keys ([#69][]) -* Active Admin no longer uses ActiveRecord::Base#search ([#28][]) -* Resource's can now override the label in the menu ([#48][]) -* Subdirectories are now loaded in the Active Admin load path +* Fixed the string representation of the resolved `sort_key` when no explicit `sortable` attribute is passed. [#5464] by [@chumakoff] +* Fixed docs on the column `sortable` attribute (which actually doesn't have to be explicitly specified when a block is passed to column). [#5464] by [@chumakoff] +* Fixed `if:` scope option when a lambda is passed. [#5501] by [@deivid-rodriguez] +* Comment validation adding redundant errors when resource is missing. [#5517] by [@deivid-rodriguez] +* Fixed resource filtering by association when the resource has custom primary key. [#5446] by [@wasifhossain] +* Fixed "create another" checkbox styling. [#5324] by [@faucct] + +## 1.3.1 [☰](https://github.com/activeadmin/activeadmin/compare/v1.3.0..v1.3.1) ### Bug Fixes -* Sort order now includes table name ([#38][]) -* Fixed table_for 'odd', 'even' row classes ([#96][]) -* Fixed Devise installation if AdminUser already exists ([#95][]) -* Fixed issues when ActiveAdmin.default_namespaces is false ([#32][]) -* Added styles for missing HTML 5 inputs ([#31][]) -* Fixed issue if adding empty Active Admin Comment ([#21][]) -* Fixed layout issues in FF 4 ([#22][]) -* Use Sass::Plugin.options[:css_location] instead of Rails.root ([#55][]) - -### Test Suite - -* Update RSpec to latest & fix specs (Thanks Ben Marini & Jeremt Ruppel!) ([#100][]) -* Added tests for STI models ([#52][]) - -### Contributors - -* Ben Marini -* Bookis Smuin -* Caley Woods -* Doug Puchalski -* Federico Romero -* Greg Bell -* Ian MacLeod -* Jeremy Ruppel -* Jordan Sitkin -* Juha Suuraho -* Mathieu Martin -* Paul Annesley -* Philippe Creux - -## 0.2.1 (2011-05-12) - [compare](https://github.com/gregbell/active_admin/compare/v0.2.0...v0.2.1) +* gemspec should have more permissive ransack dependency. [#5448] by [@varyonic] + +## 1.3.0 [☰](https://github.com/activeadmin/activeadmin/compare/v1.2.1..v1.3.0) + +### Enhancements + +* Rails 5.2 support [#5343] by [@varyonic], [#5399], [#5401] by [@zorab47] + +## 1.2.1 [☰](https://github.com/activeadmin/activeadmin/compare/v1.2.0..v1.2.1) ### Bug Fixes -* Fixed issue with dashboard rendering a sidebar -## 0.2.0 (2011-05-12) - [compare](https://github.com/gregbell/active_admin/compare/v0.1.1...v0.2.0) +* Resolve issue with [#5275] preventing XSS in filters sidebar. [#5299] by [@faucct] + +## 1.2.0 [☰](https://github.com/activeadmin/activeadmin/compare/v1.1.0..v1.2.0) -0.2.0 is essentially an entire re-write of Active Admin. Here are some -of the highlights. 250 commits. Enough said. +### Enhancements + +* Do not display pagination info when there are no comments. [#5119] by [@alex-bogomolov] +* Revert generated config files to pluralized. [#5120] by [@varyonic], [#5137] by [@deivid-rodriguez] +* Warn when action definition overwrites controller method. [#5167] by [@aarek] +* Better performance of comments show view. [#5208] by [@dhyegofernando] +* Mitigate memory bloat [#4118] with CSV exports. [#5251] by [@f1sherman] +* Fix issue applying custom decorations. [#5253] by [@faucct] +* Brazilian locale updated. [#5125] by [@renotocn] +* Japanese locale updated. [#5143] by [@5t111111], [#5157] by [@innparusu95] +* Italian locale updated. [#5180] by [@blocknotes] +* Swedish locale updated. [#5187] by [@jawa] +* Vietnamese locale updated. [#5194] by [@Nguyenanh] +* Esperanto locale added. [#5210] by [@RobinvanderVliet] + +### Bug Fixes -### Features & Enhancements +* Fix a couple of issues rendering filter labels. [#5223] by [@wspurgin] +* Prevent NameError when filtering on a namespaced association. [#5240] by [@DanielHeath] +* Fix undefined method error in Ransack when building filters. [#5238] by [@wspurgin] +* Fixed [#5198] Prevent XSS on sidebar's current filter rendering. [#5275] by [@deivid-rodriguez] +* Sanitize display_name. [#5284] by [@markstory] -* Full visual redesign -* Integrated Devise for authentication -* Brand new view and component layer called Arbre (Project coming soon) -* Added ActiveAdmin::Comments +## 1.1.0 [☰](https://github.com/activeadmin/activeadmin/compare/v1.0.0..v1.1.0) ### Bug Fixes -* Too many to list! Been in production for close to a year +* Fixed [#5093] Handle table prefix & table suffix for `ActiveAdminComment` model +* Fixed [#4173] by including the default Kaminari templates. [#5069] by [@javierjulio] +* Fixed [#5043]. Do not crash in sidebar rendering when a default scope is not specified. [#5044] by [@Fivell] +* Fixed [#3894]. Make tab's component work with non-ascii titles. [#5046] by [@Fivell] + +### Dependency Changes + +* Ruby 2.1 support has been dropped. [#5003] by [@deivid-rodriguez] +* Replaced `sass-rails` with `sass` dependency. [#5037] by [@javierjulio] +* Removed `jquery-ui-rails` as a dependency. [#5052] by [@javierjulio] + The specific jQuery UI assets used are now within the vendor directory. This + will be replaced by alternatives and dropped entirely in a major release. + Please remove any direct inclusions of `//= require jquery-ui`. This allows us + to upgrade to jquery v3. + +### Deprecations + +* Deprecated `config.register_stylesheet` and `config.register_javascript`. Import your CSS and JS files in `active_admin.scss` or `active_admin.js`. [#5060] by [@javierjulio] +* Deprecated `type` param from `status_tag` and related CSS classes [#4989] by [@javierjulio] + The method signature has changed from: + + ```ruby + status_tag(status, :ok, class: 'completed', label: 'on') + ``` + + to: + + ```ruby + status_tag(status, class: 'completed ok', label: 'on') + ``` + + The following CSS classes have been deprecated and will be removed in the future: -## 0.1.1 (2010-09-15) - [compare](https://github.com/gregbell/active_admin/compare/v0.1.0...v0.1.1) + ```css + .status_tag { + &.ok, &.published, &.complete, &.completed, &.green { background: #8daa92; } + &.warn, &.warning, &.orange { background: #e29b20; } + &.error, &.errored, &.red { background: #d45f53; } + } + ``` + +### Enhancements + +* Support proc as an input_html option value when declaring filters. [#5029] by [@Fivell] +* Base localization support, better associations handling for active filters sidebar. [#4951] by [@Fivell] +* Allow AA scopes to return paginated collections. [#4996] by [@Fivell] +* Added `scopes_show_count` configuration to setup show_count attribute for scopes globally. [#4950] by [@Fivell] +* Allow custom panel title given with `attributes_table`. [#4940] by [@ajw725] +* Allow passing a class to `action_item` block. [#4997] by [@Fivell] +* Add pagination to the comments section. [#5088] by [@alex-bogomolov] + +## 1.0.0 [☰](https://github.com/activeadmin/activeadmin/compare/v0.6.3..v1.0.0) + +### Breaking Changes + +* Rename `allow_comments` to `comments` for more consistent naming. [#3695] by [@pranas] +* JavaScript `window.AA` has been removed, use `window.ActiveAdmin`. [#3606] by [@timoschilling] +* `f.form_buffers` has been removed. [#3486] by [@varyonic] +* Iconic has been removed. [#3553] by [@timoschilling] +* `config.show_comments_in_menu` has been removed, see `config.comments_menu`. [#4187] by [@drn] +* Rails 3.2 & Ruby 1.9.3 support has been dropped. [#4848] by [@deivid-rodriguez] +* Ruby 2.0.0 support has been dropped. [#4851] by [@deivid-rodriguez] +* Rails 4.0 & 4.1 support has been dropped. [#4870] by [@deivid-rodriguez] + +### Enhancements + +* Migration from Metasearch to Ransack. [#1979] by [@seanlinsley] +* Rails 4 support. [#2326] by many people :heart: +* Rails 4.2 support. [#3731] by [@gonzedge] and [@timoschilling] +* Rails 5 support. [#4254] by [@seanlinsley] +* Rails 5.1 support. [#4882] by [@varyonic] +* "Create another" checkbox for the new resource page. [#4477] by [@bolshakov] +* Page supports belongs_to. [#4759] by [@Fivell] and [@zorab47] +* Support for custom sorting strategies. [#4768] by [@Fivell] +* Stream CSV downloads as they're generated. [#3038] by [@craigmcnamara] +* Disable streaming in development for easier debugging. [#3535] by [@seanlinsley] +* Improved code reloading. [#3783] by [@chancancode] +* Do not auto link to inaccessible actions. [#3686] by [@pranas] +* Allow to enable comments on per-resource basis. [#3695] by [@pranas] +* Unify DSL for index `actions` and `actions dropdown: true`. [#3463] by [@timoschilling] +* Add DSL method `includes` for `ActiveRecord::Relation#includes`. [#3464] by [@timoschilling] +* BOM (byte order mark) configurable for CSV download. [#3519] by [@timoschilling] +* Column block on table index is now sortable by default. [#3075] by [@dmitry] +* Allow Arbre to be used inside ActiveAdmin forms. [#3486] by [@varyonic] +* Make AA ORM-agnostic. [#2545] by [@johnnyshields] +* Add multi-record support to `attributes_table_for`. [#2544] by [@zorab47] +* Table CSS classes are now prefixed to prevent clashes. [#2532] by [@TimPetricola] +* Allow Inherited Resources shorthand for redirection. [#2001] by [@seanlinsley] + + ```ruby + controller do + # Redirects to index page instead of rendering updated resource + def update + update!{ collection_path } + end + end + ``` + +* Accept block for download links. [#2040] by [@potatosalad] + + ```ruby + index download_links: ->{ can?(:view_all_download_links) || [:pdf] } + ``` + +* Comments menu can be customized via configuration passed to `config.comments_menu`. [#4187] by [@drn] +* Added `config.route_options` to namespace to customize routes. [#4731] by [@stereoscott] + +### Security Fixes + +* Prevents access to formats that the user not permitted to see. [#4867] by [@Fivell] and [@timoschilling] +* Prevents potential DOS attack via Ruby symbols. [#1926] by [@seanlinsley] + * [this isn't an issue for those using Ruby >= 2.2](https://rubykaigi.org/2014/presentation/S-NarihiroNakamura) ### Bug Fixes -* Fixed issues running on Ruby 1.9.2 - -## 0.1.0 - -* Initial release - - -[#21]: https://github.com/gregbell/active_admin/issues/21 -[#22]: https://github.com/gregbell/active_admin/issues/22 -[#28]: https://github.com/gregbell/active_admin/issues/28 -[#31]: https://github.com/gregbell/active_admin/issues/31 -[#32]: https://github.com/gregbell/active_admin/issues/32 -[#34]: https://github.com/gregbell/active_admin/issues/34 -[#38]: https://github.com/gregbell/active_admin/issues/38 -[#42]: https://github.com/gregbell/active_admin/issues/42 -[#45]: https://github.com/gregbell/active_admin/issues/45 -[#48]: https://github.com/gregbell/active_admin/issues/48 -[#52]: https://github.com/gregbell/active_admin/issues/52 -[#55]: https://github.com/gregbell/active_admin/issues/55 -[#69]: https://github.com/gregbell/active_admin/issues/69 -[#70]: https://github.com/gregbell/active_admin/issues/70 -[#77]: https://github.com/gregbell/active_admin/issues/77 -[#92]: https://github.com/gregbell/active_admin/issues/92 -[#95]: https://github.com/gregbell/active_admin/issues/95 -[#96]: https://github.com/gregbell/active_admin/issues/96 -[#99]: https://github.com/gregbell/active_admin/issues/99 -[#100]: https://github.com/gregbell/active_admin/issues/100 -[#101]: https://github.com/gregbell/active_admin/issues/101 -[#110]: https://github.com/gregbell/active_admin/issues/110 -[#122]: https://github.com/gregbell/active_admin/issues/122 -[#131]: https://github.com/gregbell/active_admin/issues/131 -[#135]: https://github.com/gregbell/active_admin/issues/135 -[#154]: https://github.com/gregbell/active_admin/issues/154 -[#171]: https://github.com/gregbell/active_admin/issues/171 -[#186]: https://github.com/gregbell/active_admin/issues/186 -[#197]: https://github.com/gregbell/active_admin/issues/197 -[#222]: https://github.com/gregbell/active_admin/issues/222 -[#235]: https://github.com/gregbell/active_admin/issues/235 -[#248]: https://github.com/gregbell/active_admin/issues/248 -[#255]: https://github.com/gregbell/active_admin/issues/255 -[#332]: https://github.com/gregbell/active_admin/issues/332 -[#369]: https://github.com/gregbell/active_admin/issues/369 -[#381]: https://github.com/gregbell/active_admin/issues/381 -[#409]: https://github.com/gregbell/active_admin/issues/409 -[#428]: https://github.com/gregbell/active_admin/issues/428 -[#468]: https://github.com/gregbell/active_admin/issues/468 -[#470]: https://github.com/gregbell/active_admin/issues/470 -[#496]: https://github.com/gregbell/active_admin/issues/496 -[#497]: https://github.com/gregbell/active_admin/issues/497 -[#505]: https://github.com/gregbell/active_admin/issues/505 -[#527]: https://github.com/gregbell/active_admin/issues/527 -[#551]: https://github.com/gregbell/active_admin/issues/551 -[#555]: https://github.com/gregbell/active_admin/issues/555 -[#590]: https://github.com/gregbell/active_admin/issues/590 -[#601]: https://github.com/gregbell/active_admin/issues/601 -[#605]: https://github.com/gregbell/active_admin/issues/605 -[#623]: https://github.com/gregbell/active_admin/issues/623 -[#624]: https://github.com/gregbell/active_admin/issues/624 -[#637]: https://github.com/gregbell/active_admin/issues/637 -[#638]: https://github.com/gregbell/active_admin/issues/638 -[#644]: https://github.com/gregbell/active_admin/issues/644 -[#689]: https://github.com/gregbell/active_admin/issues/689 -[#711]: https://github.com/gregbell/active_admin/issues/711 -[#723]: https://github.com/gregbell/active_admin/issues/723 -[#741]: https://github.com/gregbell/active_admin/issues/741 -[#751]: https://github.com/gregbell/active_admin/issues/751 -[#758]: https://github.com/gregbell/active_admin/issues/758 -[#780]: https://github.com/gregbell/active_admin/issues/780 -[#822]: https://github.com/gregbell/active_admin/issues/822 -[#865]: https://github.com/gregbell/active_admin/issues/865 -[#869]: https://github.com/gregbell/active_admin/issues/869 -[#897]: https://github.com/gregbell/active_admin/issues/897 -[#905]: https://github.com/gregbell/active_admin/issues/905 -[#931]: https://github.com/gregbell/active_admin/issues/931 -[#960]: https://github.com/gregbell/active_admin/issues/960 -[#971]: https://github.com/gregbell/active_admin/issues/971 -[#978]: https://github.com/gregbell/active_admin/issues/978 -[#983]: https://github.com/gregbell/active_admin/issues/983 -[#993]: https://github.com/gregbell/active_admin/issues/993 -[#994]: https://github.com/gregbell/active_admin/issues/994 -[#1013]: https://github.com/gregbell/active_admin/issues/1013 -[#1016]: https://github.com/gregbell/active_admin/issues/1016 -[#1023]: https://github.com/gregbell/active_admin/issues/1023 -[#1032]: https://github.com/gregbell/active_admin/issues/1032 -[#1033]: https://github.com/gregbell/active_admin/issues/1033 -[#1041]: https://github.com/gregbell/active_admin/issues/1041 -[#1063]: https://github.com/gregbell/active_admin/issues/1063 -[#1117]: https://github.com/gregbell/active_admin/issues/1117 -[#1439]: https://github.com/gregbell/active_admin/issues/1439 -[#1609]: https://github.com/gregbell/active_admin/issues/1609 -[#1626]: https://github.com/gregbell/active_admin/issues/1626 -[#1647]: https://github.com/gregbell/active_admin/issues/1647 -[#1664]: https://github.com/gregbell/active_admin/issues/1664 -[#1668]: https://github.com/gregbell/active_admin/issues/1668 -[#1681]: https://github.com/gregbell/active_admin/issues/1681 -[#1683]: https://github.com/gregbell/active_admin/issues/1683 -[#1699]: https://github.com/gregbell/active_admin/issues/1699 -[#1745]: https://github.com/gregbell/active_admin/issues/1745 -[#1752]: https://github.com/gregbell/active_admin/issues/1752 -[#1775]: https://github.com/gregbell/active_admin/issues/1775 -[#1782]: https://github.com/gregbell/active_admin/issues/1782 -[#1783]: https://github.com/gregbell/active_admin/issues/1783 -[#1788]: https://github.com/gregbell/active_admin/issues/1788 -[#1801]: https://github.com/gregbell/active_admin/issues/1801 -[#1804]: https://github.com/gregbell/active_admin/issues/1804 -[#1805]: https://github.com/gregbell/active_admin/issues/1805 -[#1817]: https://github.com/gregbell/active_admin/issues/1817 -[#1834]: https://github.com/gregbell/active_admin/issues/1834 -[#1861]: https://github.com/gregbell/active_admin/issues/1861 -[#1867]: https://github.com/gregbell/active_admin/issues/1867 -[#1871]: https://github.com/gregbell/active_admin/issues/1871 -[#1873]: https://github.com/gregbell/active_admin/issues/1873 -[#1893]: https://github.com/gregbell/active_admin/issues/1893 -[#1896]: https://github.com/gregbell/active_admin/issues/1896 -[#1908]: https://github.com/gregbell/active_admin/issues/1908 -[#1913]: https://github.com/gregbell/active_admin/issues/1913 -[#1929]: https://github.com/gregbell/active_admin/issues/1929 -[#1933]: https://github.com/gregbell/active_admin/issues/1933 -[#1937]: https://github.com/gregbell/active_admin/issues/1937 -[#1940]: https://github.com/gregbell/active_admin/issues/1940 -[#1947]: https://github.com/gregbell/active_admin/issues/1947 -[#1960]: https://github.com/gregbell/active_admin/issues/1960 -[#1961]: https://github.com/gregbell/active_admin/issues/1961 -[#1962]: https://github.com/gregbell/active_admin/issues/1962 -[#1966]: https://github.com/gregbell/active_admin/issues/1966 -[#1967]: https://github.com/gregbell/active_admin/issues/1967 -[#1973]: https://github.com/gregbell/active_admin/issues/1973 -[#2000]: https://github.com/gregbell/active_admin/issues/2000 -[#2018]: https://github.com/gregbell/active_admin/issues/2018 -[#2043]: https://github.com/gregbell/active_admin/issues/2043 -[#2044]: https://github.com/gregbell/active_admin/issues/2044 -[@Bishop]: https://github.com/Bishop -[@BoboFraggins]: https://github.com/BoboFraggins -[@DMajrekar]: https://github.com/DMajrekar -[@Daxter]: https://github.com/Daxter -[@EtienneDepaulis]: https://github.com/EtienneDepaulis -[@MoritzMoritz]: https://github.com/MoritzMoritz -[@TBAA]: https://github.com/TBAA -[@TiagoCardoso1983]: https://github.com/TiagoCardoso1983 -[@ZequeZ]: https://github.com/ZequeZ -[@adrienkohlbecker]: https://github.com/adrienkohlbecker -[@ai]: https://github.com/ai +* Fixes filters for `has_many :through` relationships. [#2541] by [@shekibobo] +* "New" action item now only shows up on the index page. bf659bc by [@seanlinsley] +* Fixes comment creation bug with aliased resources. 9a082486 by [@seanlinsley] +* Fixes the deletion of `:if` and `:unless` from filters. [#2523] by [@PChambino] + +### Deprecations + +* `ActiveAdmin::Event` (`ActiveAdmin::EventDispatcher`). [#3435] by [@timoschilling] + `ActiveAdmin::Event` will be removed in a future version, ActiveAdmin switched + to use `ActiveSupport::Notifications` + NOTE: The blog parameters has changed: + + ```ruby + ActiveSupport::Notifications.subscribe ActiveAdmin::Application::BeforeLoadEvent do |event, *args| + # some code + end + + ActiveSupport::Notifications.publish ActiveAdmin::Application::BeforeLoadEvent, "some data" + ``` + +* `action_item` without a name, to introduce a solution for removing action items (`remove_action_item(name)`). [#3091] by [@amiel] + +## Previous Changes + +Please check [0-6-stable] for previous changes. + +[0-6-stable]: https://github.com/activeadmin/activeadmin/blob/0-6-stable/CHANGELOG.md + +[#1926]: https://github.com/activeadmin/activeadmin/issues/1926 +[#1979]: https://github.com/activeadmin/activeadmin/issues/1979 +[#2001]: https://github.com/activeadmin/activeadmin/issues/2001 +[#2040]: https://github.com/activeadmin/activeadmin/issues/2040 +[#2326]: https://github.com/activeadmin/activeadmin/issues/2326 +[#2523]: https://github.com/activeadmin/activeadmin/issues/2523 +[#2532]: https://github.com/activeadmin/activeadmin/issues/2532 +[#2541]: https://github.com/activeadmin/activeadmin/issues/2541 +[#2544]: https://github.com/activeadmin/activeadmin/issues/2544 +[#2545]: https://github.com/activeadmin/activeadmin/issues/2545 +[#3038]: https://github.com/activeadmin/activeadmin/issues/3038 +[#3075]: https://github.com/activeadmin/activeadmin/issues/3075 +[#3463]: https://github.com/activeadmin/activeadmin/issues/3463 +[#3464]: https://github.com/activeadmin/activeadmin/issues/3464 +[#3486]: https://github.com/activeadmin/activeadmin/issues/3486 +[#3519]: https://github.com/activeadmin/activeadmin/issues/3519 +[#3535]: https://github.com/activeadmin/activeadmin/issues/3535 +[#3553]: https://github.com/activeadmin/activeadmin/issues/3553 +[#3606]: https://github.com/activeadmin/activeadmin/issues/3606 +[#3686]: https://github.com/activeadmin/activeadmin/issues/3686 +[#3695]: https://github.com/activeadmin/activeadmin/issues/3695 +[#3731]: https://github.com/activeadmin/activeadmin/issues/3731 +[#3783]: https://github.com/activeadmin/activeadmin/issues/3783 +[#3894]: https://github.com/activeadmin/activeadmin/issues/3894 +[#4118]: https://github.com/activeadmin/activeadmin/issues/4118 +[#4173]: https://github.com/activeadmin/activeadmin/issues/4173 +[#4187]: https://github.com/activeadmin/activeadmin/issues/4187 +[#4254]: https://github.com/activeadmin/activeadmin/issues/4254 +[#5043]: https://github.com/activeadmin/activeadmin/issues/5043 +[#5198]: https://github.com/activeadmin/activeadmin/issues/5198 + +[21b6138f]: https://github.com/activeadmin/activeadmin/pull/5740/commits/21b6138fdcf58cd54c3f1d3f60cb1127b174b40f + +[#3091]: https://github.com/activeadmin/activeadmin/pull/3091 +[#3435]: https://github.com/activeadmin/activeadmin/pull/3435 +[#4477]: https://github.com/activeadmin/activeadmin/pull/4477 +[#4731]: https://github.com/activeadmin/activeadmin/pull/4731 +[#4759]: https://github.com/activeadmin/activeadmin/pull/4759 +[#4768]: https://github.com/activeadmin/activeadmin/pull/4768 +[#4848]: https://github.com/activeadmin/activeadmin/pull/4848 +[#4851]: https://github.com/activeadmin/activeadmin/pull/4851 +[#4867]: https://github.com/activeadmin/activeadmin/pull/4867 +[#4870]: https://github.com/activeadmin/activeadmin/pull/4870 +[#4882]: https://github.com/activeadmin/activeadmin/pull/4882 +[#4940]: https://github.com/activeadmin/activeadmin/pull/4940 +[#4950]: https://github.com/activeadmin/activeadmin/pull/4950 +[#4951]: https://github.com/activeadmin/activeadmin/pull/4951 +[#4989]: https://github.com/activeadmin/activeadmin/pull/4989 +[#4996]: https://github.com/activeadmin/activeadmin/pull/4996 +[#4997]: https://github.com/activeadmin/activeadmin/pull/4997 +[#5003]: https://github.com/activeadmin/activeadmin/pull/5003 +[#5029]: https://github.com/activeadmin/activeadmin/pull/5029 +[#5037]: https://github.com/activeadmin/activeadmin/pull/5037 +[#5044]: https://github.com/activeadmin/activeadmin/pull/5044 +[#5046]: https://github.com/activeadmin/activeadmin/pull/5046 +[#5052]: https://github.com/activeadmin/activeadmin/pull/5052 +[#5060]: https://github.com/activeadmin/activeadmin/pull/5060 +[#5069]: https://github.com/activeadmin/activeadmin/pull/5069 +[#5078]: https://github.com/activeadmin/activeadmin/pull/5078 +[#5081]: https://github.com/activeadmin/activeadmin/pull/5081 +[#5088]: https://github.com/activeadmin/activeadmin/pull/5088 +[#5093]: https://github.com/activeadmin/activeadmin/pull/5093 +[#5099]: https://github.com/activeadmin/activeadmin/pull/5099 +[#5104]: https://github.com/activeadmin/activeadmin/pull/5104 +[#5119]: https://github.com/activeadmin/activeadmin/pull/5119 +[#5120]: https://github.com/activeadmin/activeadmin/pull/5120 +[#5125]: https://github.com/activeadmin/activeadmin/pull/5125 +[#5137]: https://github.com/activeadmin/activeadmin/pull/5137 +[#5143]: https://github.com/activeadmin/activeadmin/pull/5143 +[#5157]: https://github.com/activeadmin/activeadmin/pull/5157 +[#5167]: https://github.com/activeadmin/activeadmin/pull/5167 +[#5180]: https://github.com/activeadmin/activeadmin/pull/5180 +[#5187]: https://github.com/activeadmin/activeadmin/pull/5187 +[#5194]: https://github.com/activeadmin/activeadmin/pull/5194 +[#5208]: https://github.com/activeadmin/activeadmin/pull/5208 +[#5210]: https://github.com/activeadmin/activeadmin/pull/5210 +[#5223]: https://github.com/activeadmin/activeadmin/pull/5223 +[#5238]: https://github.com/activeadmin/activeadmin/pull/5238 +[#5240]: https://github.com/activeadmin/activeadmin/pull/5240 +[#5251]: https://github.com/activeadmin/activeadmin/pull/5251 +[#5253]: https://github.com/activeadmin/activeadmin/pull/5253 +[#5266]: https://github.com/activeadmin/activeadmin/pull/5266 +[#5272]: https://github.com/activeadmin/activeadmin/pull/5272 +[#5275]: https://github.com/activeadmin/activeadmin/pull/5275 +[#5284]: https://github.com/activeadmin/activeadmin/pull/5284 +[#5295]: https://github.com/activeadmin/activeadmin/pull/5295 +[#5299]: https://github.com/activeadmin/activeadmin/pull/5299 +[#5320]: https://github.com/activeadmin/activeadmin/pull/5320 +[#5324]: https://github.com/activeadmin/activeadmin/pull/5324 +[#5330]: https://github.com/activeadmin/activeadmin/pull/5330 +[#5334]: https://github.com/activeadmin/activeadmin/pull/5334 +[#5336]: https://github.com/activeadmin/activeadmin/pull/5336 +[#5341]: https://github.com/activeadmin/activeadmin/pull/5341 +[#5343]: https://github.com/activeadmin/activeadmin/pull/5343 +[#5357]: https://github.com/activeadmin/activeadmin/pull/5357 +[#5359]: https://github.com/activeadmin/activeadmin/pull/5359 +[#5368]: https://github.com/activeadmin/activeadmin/pull/5368 +[#5370]: https://github.com/activeadmin/activeadmin/pull/5370 +[#5375]: https://github.com/activeadmin/activeadmin/pull/5375 +[#5376]: https://github.com/activeadmin/activeadmin/pull/5376 +[#5399]: https://github.com/activeadmin/activeadmin/pull/5399 +[#5401]: https://github.com/activeadmin/activeadmin/pull/5401 +[#5408]: https://github.com/activeadmin/activeadmin/pull/5408 +[#5413]: https://github.com/activeadmin/activeadmin/pull/5413 +[#5417]: https://github.com/activeadmin/activeadmin/pull/5417 +[#5418]: https://github.com/activeadmin/activeadmin/pull/5418 +[#5433]: https://github.com/activeadmin/activeadmin/pull/5433 +[#5446]: https://github.com/activeadmin/activeadmin/pull/5446 +[#5448]: https://github.com/activeadmin/activeadmin/pull/5448 +[#5453]: https://github.com/activeadmin/activeadmin/pull/5453 +[#5458]: https://github.com/activeadmin/activeadmin/pull/5458 +[#5461]: https://github.com/activeadmin/activeadmin/pull/5461 +[#5464]: https://github.com/activeadmin/activeadmin/pull/5464 +[#5486]: https://github.com/activeadmin/activeadmin/pull/5486 +[#5501]: https://github.com/activeadmin/activeadmin/pull/5501 +[#5504]: https://github.com/activeadmin/activeadmin/pull/5504 +[#5517]: https://github.com/activeadmin/activeadmin/pull/5517 +[#5537]: https://github.com/activeadmin/activeadmin/pull/5537 +[#5548]: https://github.com/activeadmin/activeadmin/pull/5548 +[#5555]: https://github.com/activeadmin/activeadmin/pull/5555 +[#5583]: https://github.com/activeadmin/activeadmin/pull/5583 +[#5590]: https://github.com/activeadmin/activeadmin/pull/5590 +[#5608]: https://github.com/activeadmin/activeadmin/pull/5608 +[#5611]: https://github.com/activeadmin/activeadmin/pull/5611 +[#5627]: https://github.com/activeadmin/activeadmin/pull/5627 +[#5631]: https://github.com/activeadmin/activeadmin/pull/5631 +[#5632]: https://github.com/activeadmin/activeadmin/pull/5632 +[#5650]: https://github.com/activeadmin/activeadmin/pull/5650 +[#5660]: https://github.com/activeadmin/activeadmin/pull/5660 +[#5662]: https://github.com/activeadmin/activeadmin/pull/5662 +[#5710]: https://github.com/activeadmin/activeadmin/pull/5710 +[#5726]: https://github.com/activeadmin/activeadmin/pull/5726 +[#5738]: https://github.com/activeadmin/activeadmin/pull/5738 +[#5740]: https://github.com/activeadmin/activeadmin/pull/5740 +[#5751]: https://github.com/activeadmin/activeadmin/pull/5751 +[#5758]: https://github.com/activeadmin/activeadmin/pull/5758 +[#5777]: https://github.com/activeadmin/activeadmin/pull/5777 +[#5794]: https://github.com/activeadmin/activeadmin/pull/5794 +[#5800]: https://github.com/activeadmin/activeadmin/pull/5800 +[#5801]: https://github.com/activeadmin/activeadmin/pull/5801 +[#5802]: https://github.com/activeadmin/activeadmin/pull/5802 +[#5811]: https://github.com/activeadmin/activeadmin/pull/5811 +[#5816]: https://github.com/activeadmin/activeadmin/pull/5816 +[#5822]: https://github.com/activeadmin/activeadmin/pull/5822 +[#5826]: https://github.com/activeadmin/activeadmin/pull/5826 +[#5831]: https://github.com/activeadmin/activeadmin/pull/5831 +[#5842]: https://github.com/activeadmin/activeadmin/pull/5842 +[#5854]: https://github.com/activeadmin/activeadmin/pull/5854 +[#5855]: https://github.com/activeadmin/activeadmin/pull/5855 +[#5867]: https://github.com/activeadmin/activeadmin/pull/5867 +[#5870]: https://github.com/activeadmin/activeadmin/pull/5870 +[#5874]: https://github.com/activeadmin/activeadmin/pull/5874 +[#5877]: https://github.com/activeadmin/activeadmin/pull/5877 +[#5886]: https://github.com/activeadmin/activeadmin/pull/5886 +[#5887]: https://github.com/activeadmin/activeadmin/pull/5887 +[#5894]: https://github.com/activeadmin/activeadmin/pull/5894 +[#5895]: https://github.com/activeadmin/activeadmin/pull/5895 +[#5929]: https://github.com/activeadmin/activeadmin/pull/5929 +[#5931]: https://github.com/activeadmin/activeadmin/pull/5931 +[#5938]: https://github.com/activeadmin/activeadmin/pull/5938 +[#5943]: https://github.com/activeadmin/activeadmin/pull/5943 +[#5946]: https://github.com/activeadmin/activeadmin/pull/5946 +[#5956]: https://github.com/activeadmin/activeadmin/pull/5956 +[#5957]: https://github.com/activeadmin/activeadmin/pull/5957 +[#5994]: https://github.com/activeadmin/activeadmin/pull/5994 +[#6000]: https://github.com/activeadmin/activeadmin/pull/6000 +[#6002]: https://github.com/activeadmin/activeadmin/pull/6002 +[#6047]: https://github.com/activeadmin/activeadmin/pull/6047 +[#6086]: https://github.com/activeadmin/activeadmin/pull/6086 +[#6099]: https://github.com/activeadmin/activeadmin/pull/6099 +[#6124]: https://github.com/activeadmin/activeadmin/pull/6124 +[#6149]: https://github.com/activeadmin/activeadmin/pull/6149 +[#6195]: https://github.com/activeadmin/activeadmin/pull/6195 +[#6198]: https://github.com/activeadmin/activeadmin/pull/6198 +[#6210]: https://github.com/activeadmin/activeadmin/pull/6210 +[#6221]: https://github.com/activeadmin/activeadmin/pull/6221 +[#6232]: https://github.com/activeadmin/activeadmin/pull/6232 +[#6249]: https://github.com/activeadmin/activeadmin/pull/6249 +[#6315]: https://github.com/activeadmin/activeadmin/pull/6315 +[#6318]: https://github.com/activeadmin/activeadmin/pull/6318 +[#6341]: https://github.com/activeadmin/activeadmin/pull/6341 +[#6342]: https://github.com/activeadmin/activeadmin/pull/6342 +[#6368]: https://github.com/activeadmin/activeadmin/pull/6368 +[#6393]: https://github.com/activeadmin/activeadmin/pull/6393 +[#6422]: https://github.com/activeadmin/activeadmin/pull/6422 +[#6451]: https://github.com/activeadmin/activeadmin/pull/6451 +[#6460]: https://github.com/activeadmin/activeadmin/pull/6460 +[#6462]: https://github.com/activeadmin/activeadmin/pull/6462 +[#6482]: https://github.com/activeadmin/activeadmin/pull/6482 +[#6487]: https://github.com/activeadmin/activeadmin/pull/6487 +[#6523]: https://github.com/activeadmin/activeadmin/pull/6523 +[#6535]: https://github.com/activeadmin/activeadmin/pull/6535 +[#6536]: https://github.com/activeadmin/activeadmin/pull/6536 +[#6548]: https://github.com/activeadmin/activeadmin/pull/6548 +[#6583]: https://github.com/activeadmin/activeadmin/pull/6583 +[#6584]: https://github.com/activeadmin/activeadmin/pull/6584 +[#6872]: https://github.com/activeadmin/activeadmin/pull/6872 +[#6873]: https://github.com/activeadmin/activeadmin/pull/6873 +[#6884]: https://github.com/activeadmin/activeadmin/pull/6884 +[#6906]: https://github.com/activeadmin/activeadmin/pull/6906 +[#6915]: https://github.com/activeadmin/activeadmin/pull/6915 +[#6916]: https://github.com/activeadmin/activeadmin/pull/6916 +[#6922]: https://github.com/activeadmin/activeadmin/pull/6922 +[#6936]: https://github.com/activeadmin/activeadmin/pull/6936 +[#6954]: https://github.com/activeadmin/activeadmin/pull/6954 +[#6959]: https://github.com/activeadmin/activeadmin/pull/6959 +[#7002]: https://github.com/activeadmin/activeadmin/pull/7002 +[#7013]: https://github.com/activeadmin/activeadmin/pull/7013 +[#7095]: https://github.com/activeadmin/activeadmin/pull/7095 +[#7127]: https://github.com/activeadmin/activeadmin/pull/7127 +[#7144]: https://github.com/activeadmin/activeadmin/pull/7144 +[#7170]: https://github.com/activeadmin/activeadmin/pull/7170 +[#7181]: https://github.com/activeadmin/activeadmin/pull/7181 +[#7205]: https://github.com/activeadmin/activeadmin/pull/7205 +[#7235]: https://github.com/activeadmin/activeadmin/pull/7235 +[#7236]: https://github.com/activeadmin/activeadmin/pull/7236 +[#7262]: https://github.com/activeadmin/activeadmin/pull/7262 +[#7293]: https://github.com/activeadmin/activeadmin/pull/7293 +[#7332]: https://github.com/activeadmin/activeadmin/pull/7332 +[#7336]: https://github.com/activeadmin/activeadmin/pull/7336 +[#7340]: https://github.com/activeadmin/activeadmin/pull/7340 +[#7341]: https://github.com/activeadmin/activeadmin/pull/7341 +[#7349]: https://github.com/activeadmin/activeadmin/pull/7349 +[#7350]: https://github.com/activeadmin/activeadmin/pull/7350 +[#7377]: https://github.com/activeadmin/activeadmin/pull/7377 +[#7384]: https://github.com/activeadmin/activeadmin/pull/7384 +[#7394]: https://github.com/activeadmin/activeadmin/pull/7394 +[#7453]: https://github.com/activeadmin/activeadmin/pull/7453 +[#7475]: https://github.com/activeadmin/activeadmin/pull/7475 +[#7476]: https://github.com/activeadmin/activeadmin/pull/7476 +[#7479]: https://github.com/activeadmin/activeadmin/pull/7479 +[#7487]: https://github.com/activeadmin/activeadmin/pull/7487 +[#7488]: https://github.com/activeadmin/activeadmin/pull/7488 +[#7541]: https://github.com/activeadmin/activeadmin/pull/7541 +[#7556]: https://github.com/activeadmin/activeadmin/pull/7556 +[#7599]: https://github.com/activeadmin/activeadmin/pull/7599 +[#7653]: https://github.com/activeadmin/activeadmin/pull/7653 +[#7772]: https://github.com/activeadmin/activeadmin/pull/7772 +[#7920]: https://github.com/activeadmin/activeadmin/pull/7920 +[#7944]: https://github.com/activeadmin/activeadmin/pull/7944 +[#7984]: https://github.com/activeadmin/activeadmin/pull/7984 +[#7985]: https://github.com/activeadmin/activeadmin/pull/7985 +[#7986]: https://github.com/activeadmin/activeadmin/pull/7986 +[#7993]: https://github.com/activeadmin/activeadmin/pull/7993 +[#8009]: https://github.com/activeadmin/activeadmin/pull/8009 +[#8010]: https://github.com/activeadmin/activeadmin/pull/8010 +[#8098]: https://github.com/activeadmin/activeadmin/pull/8098 +[#8102]: https://github.com/activeadmin/activeadmin/pull/8102 +[#8105]: https://github.com/activeadmin/activeadmin/pull/8105 +[#8106]: https://github.com/activeadmin/activeadmin/pull/8106 + + +[@1000ship]: https://github.com/1000ship +[@5t111111]: https://github.com/5t111111 +[@aarek]: https://github.com/aarek +[@adler99]: https://github.com/adler99 +[@agrobbin]: https://github.com/agrobbin +[@ajw725]: https://github.com/ajw725 +[@alejandroperea]: https://github.com/alejandroperea +[@alex-bogomolov]: https://github.com/alex-bogomolov [@amiel]: https://github.com/amiel -[@bobbytables]: https://github.com/bobbytables -[@caifara]: https://github.com/caifara -[@comboy]: https://github.com/comboy -[@dapi]: https://github.com/dapi -[@dbussin]: https://github.com/dbussin -[@dhiemstra]: https://github.com/dhiemstra -[@doug316]: https://github.com/doug316 -[@ebeigarts]: https://github.com/ebeigarts -[@ejholmes]: https://github.com/ejholmes -[@emzeq]: https://github.com/emzeq -[@ericcumbee]: https://github.com/ericcumbee -[@fabiokr]: https://github.com/fabiokr -[@fabiormoura]: https://github.com/fabiormoura -[@fbuenemann]: https://github.com/fbuenemann -[@george]: https://github.com/george -[@ggilder]: https://github.com/ggilder -[@gregbell]: https://github.com/gregbell -[@hakanensari]: https://github.com/hakanensari -[@j]: https://github.com/j -[@jamesalmond]: https://github.com/jamesalmond -[@jancel]: https://github.com/jancel -[@jbarket]: https://github.com/jbarket +[@amit]: https://github.com/amit +[@amiuhle]: https://github.com/amiuhle +[@andreslemik]: https://github.com/andreslemik +[@bartoszkopinski]: https://github.com/bartoszkopinski +[@bliof]: https://github.com/bliof +[@blocknotes]: https://github.com/blocknotes +[@bolshakov]: https://github.com/bolshakov +[@brunoarueira]: https://github.com/brunoarueira +[@brunvez]: https://github.com/brunvez +[@buren]: https://github.com/buren +[@carlottostromstedt]: https://github.com/carlottostromstedt +[@chancancode]: https://github.com/chancancode +[@chrp]: https://github.com/chrp +[@chumakoff]: https://github.com/chumakoff +[@cprodhomme]: https://github.com/cprodhomme +[@craigmcnamara]: https://github.com/craigmcnamara +[@DanielHeath]: https://github.com/DanielHeath +[@deivid-rodriguez]: https://github.com/deivid-rodriguez +[@dennisvdvliet]: https://github.com/dennisvdvliet +[@dhyegofernando]: https://github.com/dhyegofernando +[@dkniffin]: https://github.com/dkniffin +[@dmitry]: https://github.com/dmitry +[@drn]: https://github.com/drn +[@eikes]: https://github.com/eikes +[@f1sherman]: https://github.com/f1sherman +[@FabioRos]: https://github.com/FabioRos +[@faucct]: https://github.com/faucct +[@Fivell]: https://github.com/Fivell +[@Fs00]: https://github.com/Fs00 +[@fuzziness]: https://github.com/fuzziness +[@gabo-cs]: https://github.com/gabo-cs +[@giapnhdev]: https://github.com/giapnhdev +[@gigorok]: https://github.com/gigorok +[@glebtv]: https://github.com/glebtv +[@gonzedge]: https://github.com/gonzedge +[@guigs]: https://github.com/guigs +[@HappyKadaver]: https://github.com/HappyKadaver +[@hfl]: https://github.com/hfl +[@imcvampire]: https://github.com/imcvampire +[@innparusu95]: https://github.com/innparusu95 +[@ionut998]: https://github.com/ionut998 +[@irmela]: https://github.com/irmela +[@ispyropoulos]: https://github.com/ispyropoulos +[@Ivanov-Anton]: https://github.com/Ivanov-Anton +[@jasl]: https://github.com/jasl +[@javawizard]: https://github.com/javawizard +[@javierjulio]: https://github.com/javierjulio +[@jawa]: https://github.com/jawa +[@jaynetics]: https://github.com/jaynetics +[@JewelSam]: https://github.com/JewelSam +[@JiiHu]: https://github.com/JiiHu +[@jiikko]: https://github.com/jiikko [@johnnyshields]: https://github.com/johnnyshields -[@joshuacollins85]: https://github.com/joshuacollins85 -[@jschwindt]: https://github.com/jschwindt -[@kerberoS]: https://github.com/kerberoS -[@knoopx]: https://github.com/knoopx -[@krug]: https://github.com/krug -[@laffinkippah]: https://github.com/laffinkippah -[@latortuga]: https://github.com/latortuga -[@macfanatic]: https://github.com/macfanatic -[@mattvague]: https://github.com/mattvague -[@mperham]: https://github.com/mperham -[@mwindwer]: https://github.com/mwindwer -[@orendon]: https://github.com/orendon -[@page_title]: https://github.com/page_title -[@pcreux]: https://github.com/pcreux -[@per_page]: https://github.com/per_page -[@ptn]: https://github.com/ptn -[@randym]: https://github.com/randym -[@rdsoze]: https://github.com/rdsoze -[@rheaton]: https://github.com/rheaton -[@rmw]: https://github.com/rmw -[@robdiciuccio]: https://github.com/robdiciuccio -[@rolfb]: https://github.com/rolfb -[@ronen]: https://github.com/ronen -[@ryansch]: https://github.com/ryansch -[@samvincent]: https://github.com/samvincent -[@sftsang]: https://github.com/sftsang -[@shayfrendt]: https://github.com/shayfrendt -[@shell]: https://github.com/shell -[@simonoff]: https://github.com/simonoff -[@snapapps]: https://github.com/snapapps +[@jscheid]: https://github.com/jscheid +[@juril33t]: https://github.com/juril33t +[@jwesorick]: https://github.com/jwesorick +[@Karoid]: https://github.com/Karoid +[@kjeldahl]: https://github.com/kjeldahl +[@ko-lem]: https://github.com/ko-lem +[@kobeumut]: https://github.com/kobeumut +[@Kris-LIBIS]: https://github.com/Kris-LIBIS +[@krzcho]: https://github.com/krzcho +[@kwent]: https://github.com/kwent +[@lanzhiheng]: https://github.com/lanzhiheng +[@leio10]: https://github.com/leio10 +[@littleforest]: https://github.com/littleforest +[@Looooong]: https://github.com/Looooong +[@lubosch]: https://github.com/lubosch +[@markstory]: https://github.com/markstory +[@mauriciopasquier]: https://github.com/mauriciopasquier +[@mconiglio]: https://github.com/mconiglio +[@mgrunberg]: https://github.com/mgrunberg +[@micred]: https://github.com/micred +[@mirelon]: https://github.com/mirelon +[@MmKolodziej]: https://github.com/MmKolodziej +[@mshalaby]: https://github.com/mshalaby +[@munen]: https://github.com/munen +[@mvz]: https://github.com/mvz +[@ndbroadbent]: https://github.com/ndbroadbent +[@ngouy]: https://github.com/ngouy +[@Nguyenanh]: https://github.com/Nguyenanh +[@orkhan]: https://github.com/orkhan +[@panasyuk]: https://github.com/panasyuk +[@PChambino]: https://github.com/PChambino +[@potatosalad]: https://github.com/potatosalad +[@pranas]: https://github.com/pranas +[@ray-curran]: https://github.com/ray-curran +[@renotocn]: https://github.com/renotocn +[@rn0rno]: https://github.com/rn0rno +[@RobinvanderVliet]: https://github.com/RobinvanderVliet +[@rogerkk]: https://github.com/rogerkk +[@roramirez]: https://github.com/roramirez +[@rs-phunt]: https://github.com/rs-phunt +[@sanfrecce-osaka]: https://github.com/sanfrecce-osaka +[@seanlinsley]: https://github.com/seanlinsley +[@sergey-alekseev]: https://github.com/sergey-alekseev +[@sgara]: https://github.com/sgara +[@ShallmentMo]: https://github.com/ShallmentMo +[@shekibobo]: https://github.com/shekibobo +[@shouya]: https://github.com/shouya +[@sjieg]: https://github.com/sjieg +[@sprql]: https://github.com/sprql +[@stefsava]: https://github.com/stefsava [@stereoscott]: https://github.com/stereoscott -[@sunny]: https://github.com/sunny -[@tracedwax]: https://github.com/tracedwax -[@tricknotes]: https://github.com/tricknotes -[@utkarshkukreti]: https://github.com/utkarshkukreti -[@vairix]: https://github.com/vairix -[@vairix-ssierra]: https://github.com/vairix-ssierra -[@watson]: https://github.com/watson -[@whatcould]: https://github.com/whatcould -[@yawn]: https://github.com/yawn -[@yorch]: https://github.com/yorch +[@tagliala]: https://github.com/tagliala +[@taralbass]: https://github.com/taralbass +[@tf]: https://github.com/tf +[@tiagotex]: https://github.com/tiagotex +[@timoschilling]: https://github.com/timoschilling +[@TimPetricola]: https://github.com/TimPetricola +[@timwis]: https://github.com/timwis +[@tomgilligan]: https://github.com/tomgilligan +[@TonyArra]: https://github.com/TonyArra +[@tordans]: https://github.com/tordans +[@tvziet]: https://github.com/tvziet +[@utkarsh2102]: https://github.com/utkarsh2102 +[@varyonic]: https://github.com/varyonic +[@vcsjones]: https://github.com/vcsjones +[@vfonic]: https://github.com/vfonic +[@violeta-p]: https://github.com/violeta-p +[@vlad-psh]: https://github.com/vlad-psh +[@WaKeMaTTa]: https://github.com/WaKeMaTTa +[@wasifhossain]: https://github.com/wasifhossain +[@westonganger]: https://github.com/westonganger +[@Wowu]: https://github.com/Wowu +[@wspurgin]: https://github.com/wspurgin +[@zorab47]: https://github.com/zorab47 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000000..a016f4e9c9f --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting one or more of the project maintainers. All complaints +will be reviewed and investigated and will result in a response that is deemed +necessary and appropriate to the circumstances. The project team is obligated to +maintain confidentiality with regard to the reporter of an incident. Further +details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available [here][source]. + +[homepage]: https://www.contributor-covenant.org +[source]: https://www.contributor-covenant.org/version/1/4/code-of-conduct.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e14063b3754..a485eb7a510 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,121 +1,123 @@ -## Contributing +# Contributing -This is a guide to contributing to Active Admin. It should walk you through the -major steps to contributing code to the project. +Thanks for your interest in contributing to ActiveAdmin! Please take a moment to review this document **before submitting a pull request**. -### 0. 'The test suite is failing locally!' +## Pull requests -If your test suite is failing locally and the -[Travis build](https://secure.travis-ci.org/gregbell/active_admin.png?branch=master) - is passing, you've likely already generated the rails app that is used by -ActiveAdmin's test suite and the generated rails app has erroneous files. +**Please ask first before starting work on any significant new features.** -To fix this, remove the generated rails app by deleting the directory and its -contents (`rm -rf spec/rails`) and then re-running the test suite. +It's never a fun experience to have your pull request declined after investing a lot of time and effort into a new feature. To avoid this from happening, we request that contributors create [a feature request](https://github.com/activeadmin/activeadmin/discussions/new?category=ideas) to first discuss any new ideas. Your ideas and suggestions are welcome! -### 1. Create an Issue on GitHub +Please ensure that the tests are passing when submitting a pull request. If you're adding new features to ActiveAdmin, please include tests. -The first step to contributing to Active Admin is creating a ticket in our -[ticketing system on GitHub](https://github.com/gregbell/active_admin/issues). -The community has been hard at work already, so please take a second to search -for the issue or feature before creating a new one. +## Where do I go from here? -All bug fixes should have a ticket. This makes it easy for everyone -to discuss the code and know if a fix is already in progress for an issue. +For any questions, support, or ideas, etc. [please create a GitHub discussion](https://github.com/activeadmin/activeadmin/discussions/new). If you've noticed a bug, [please submit an issue][new issue]. +### Fork and create a branch -### 2. Fork & Create a Feature Branch +If this is something you think you can fix, then [fork Active Admin] and create +a branch with a descriptive name. -The next step is to fork Active Admin (if you haven't already done so) and -create a new git branch based on the feature or issue you're working on. Please -use a descriptive name for your branch. +### Get the test suite running -For example a great branch name would be (where issue #325 is the ticket you're -working on): +Make sure you're using a recent Ruby and Node version. You'll also need Chrome installed in order to run Cucumber scenarios. - $> git checkout -b 325-add-japanese-translations +Now install the development dependencies: +```sh +gem install foreman +bundle install --all +npm install +``` -### 3. Get the test suite running - -Active Admin is a gem that many people and businesses rely on for managing data -in their production applications. Bugs are not cool. Although we're not perfect, -we pride ourselves on writing well tested code. I hope you do too :) - -Active Admin uses rspec and cucumber for it's test suite. - -Make sure you have a recent version of bundler: - - $> gem install bundler +Now you should be able to run the entire suite using: -Then install the development the development dependencies: +```sh +bin/rake +``` - $> bundle install +The task will generate a sample Rails application in `tmp/test_apps` to run the +test suite against. -Now you should be able to run the entire suite using: +If you want to test against a Rails version different from the latest, make sure +you use the correct Gemfile, for example: - $> rake test +```sh +export BUNDLE_GEMFILE=gemfiles/rails_61/Gemfile +``` -`rake test` runs the unit specs, integration specs and cucumber scenarios. The -test suite will generate a rails application in `spec/rails` to run the tests -against. +### Implement your fix or feature +At this point, you're ready to make your changes. Feel free to ask for help. -### 4. Implement your fix or feature +### View your changes in a Rails application -At this point, you should be ready to implement your feature! +Make sure to take a look at your changes in a browser. To boot up a test Rails app: +```sh +bin/rake local server +``` -### 5. View your changes in a Rails application +This will automatically create a Rails app if none already exists, and store it +in the `tmp/development_apps` folder. -Active Admin is meant to be used by humans, not cucumbers. So make sure to take -a look at your changes in a browser (preferably a few browsers if you made view -changes). +You should now be able to open in your browser and log in using `admin@example.com` and `password`. -To boot up a test rails application, use the provided script: +If you need to perform any other commands on the test application, just pass +them to the `local` rake task. For example, to boot the rails console: - $> ./script/local server +```sh +bin/rake local console +``` -This will generate a rails application at ./test-rails-app with some sane -defaults and use your local version of Active Admin. +Or to migrate the database for a new migration: -If you have any Bundler issues, call the provided `use_rails` script then prepend -the version of rails you would like to use in an environment variable: +```sh +bin/rake local db:migrate +``` - $> ./script/use_rails 3.1.0 - $> RAILS=3.1.0 ./script/local server +### Documentation -You should be able to open `http://localhost:3000/admin` and view a test -environment. +Run `npm run docs:dev` and open `/docs/` to preview v4 beta documentation in VitePress. +The version selector opens the matching page under `/docs/v3/`. -If you need to perform any other commands on the test application, use the -`local` script. For example to boot the rails console: +Edit the current documentation in `docs/` and the archived v3 documentation +in `docs/v3/`. The site includes `UPGRADING.md` directly as its v4 upgrade guide. - $> ./script/local console +Run `npm run docs:test` to build the site and check version links and legacy +redirects. The deployment artifact is `docs/.vitepress/dist`: it contains the +site in `docs/` and generated HTML redirects for the former root URLs. These +redirects target the matching pages under `/docs/v3/` and are generated from +the v3 archive, including all five files in `docs/v3/3-index-pages/`. These +redirects do not depend on the layout of the current v4 pages. +Publish this entire directory so links such as `/3-index-pages.html` and +`/3-index-pages/index-as-grid.html` keep working. -Or to migrate the database: +### Create a Pull Request - $> ./script/local rake db:migrate +At this point, if your changes look good and tests are passing, you are ready to create a pull request. +Github Actions will run our test suite against all supported Rails versions. It's possible that your changes pass tests in one Rails version but fail in another. In that case, you'll have to setup your development +environment with the Gemfile for the problematic Rails version, and investigate what's going on. -### 6. Run tests against major supported rails versions +## Commit messages -Once you've implemented your code, got the tests passing, previewed it in a -browser, you're ready to test it against multiple versions of Rails. +Try your best to follow these [seven rules for a great commit message](https://cbea.ms/git-commit/#seven-rules). - $> rake test:major_supported_rails +## Merging a PR (maintainers only) -This command runs the cukes and specs against a couple of major versions of -Rails. We will run this command when we review your pull request, if this -rake task isn't passing, the pull request will not be merged in. +A PR can only be merged into master by a maintainer if: CI is passing, approved by another maintainer and is up to date with the default branch. Any maintainer is allowed to merge a PR if all of these conditions ae met. +## Shipping a release (maintainers only) -### 7. Make a pull request +Maintainers need to do the following to push out a release: -At this point, you should switch back to your master branch and make sure it's -up to date with Active Admin's master branch. If there were any changes, you -should rebase your feature branch and make sure that it will merge correctly. If -there are any merge conflicts, your pull request will not be merged in. +* Create a feature branch from master and make sure it's up to date. +* Run `bin/prep-release [version]` and commit the changes. Use Ruby version format. NPM is handled automatically. +* Optional: To confirm the release contents, run `gem build` (extract contents) and `npm publish --dry-run`. +* Review and merge the PR. +* Run `bin/rake release` from the default branch once the PR is merged. +* [Create a GitHub Release](https://github.com/activeadmin/activeadmin/releases/new) by selecting the tag and generating the release notes. -Now push your changes up to your feature branch on GitHub and make a pull request! -We will pull your changes, run the test suite, review the code and merge it in. +[new issue]: https://github.com/activeadmin/activeadmin/issues/new diff --git a/Gemfile b/Gemfile index e9563bcc817..a9fc7947740 100644 --- a/Gemfile +++ b/Gemfile @@ -1,53 +1,52 @@ -source 'http://rubygems.org' +# frozen_string_literal: true +source "https://rubygems.org", cooldown: 5 -gemspec +group :development, :test do + gem "rake" -ACTIVE_ADMIN_PATH = File.dirname(__FILE__) unless defined?(ACTIVE_ADMIN_PATH) + gem "cancancan" + gem "pundit" -require File.expand_path('spec/support/detect_rails_version', ACTIVE_ADMIN_PATH) + gem "draper" + gem "devise" -rails_version = detect_rails_version -gem 'rails', rails_version -gem 'bourbon' + gem "rails", "~> 8.1.0" -case rails_version -when /^3\.0/ - # Do nothing, bundler should figure it out -when /^3\.(1|2)/ - # These are the gems you have to have for Rails 3.1 to be happy - gem 'sass-rails' - gem 'uglifier' -else - raise "Rails #{rails_version} is not supported yet" -end + gem "sprockets-rails" + gem "ransack", ">= 4.2.0" + gem "formtastic", ">= 6.0.0" -group :development do - gem 'better_errors', '~> 0.9.0' # Web UI to debug exceptions. Go to /__better_errors to access the latest one - gem 'binding_of_caller', '~> 0.7.1' # Retrieve the binding of a method's caller in MRI Ruby >= 1.9.2 + gem "cssbundling-rails" + gem "importmap-rails" end -group :development, :test do - gem 'haml', '~> 3.1.7', :require => false - gem 'rake', '~> 10.0.2', :require => false - gem 'rails-i18n' # Provides default i18n for many languages - gem 'rdiscount' # Markdown implementation (for yard) - gem 'sprockets' - gem 'yard' +group :test do + gem "cuprite" + gem "capybara" + gem "webrick" + + gem "simplecov", require: false # Test coverage generator. Go to /coverage/ after running tests + gem "simplecov-cobertura", require: false + gem "cucumber-rails", require: false + gem "database_cleaner-active_record" + gem "launchy" + gem "parallel_tests" + gem "rspec-rails" + gem "sqlite3", platform: :mri + + # Translations + gem "i18n-tasks" + gem "i18n-spec" + gem "rails-i18n" # Provides default i18n for many languages end -group :test do - gem 'cancan' - gem 'capybara', '1.1.2' - gem 'coveralls', '~> 0.6.7', require: false # Test coverage tool: www.coveralls.io - gem 'cucumber-rails', '1.3.0', :require => false - gem 'database_cleaner' - gem 'guard-coffeescript' - gem 'guard-rspec' - gem 'jasmine' - gem 'jslint_on_rails', '~> 1.1.1' - gem 'launchy' - gem 'parallel_tests' - gem 'rspec-rails', '~> 2.9.0' - gem 'shoulda-matchers' - gem 'sqlite3' +group :rubocop do + gem "rubocop" + gem "rubocop-capybara" + gem "rubocop-packaging" + gem "rubocop-performance" + gem "rubocop-rspec" + gem "rubocop-rails" end + +gemspec path: "." diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000000..54a9524a074 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,500 @@ +PATH + remote: . + specs: + activeadmin (4.0.0.beta23) + arbre (~> 2.0) + csv + formtastic (>= 6.0) + formtastic_i18n (>= 0.7) + inherited_resources (~> 2.0) + kaminari (>= 1.2.1) + railties (>= 7.2) + ransack (>= 4.0) + +GEM + remote: https://rubygems.org/ + specs: + action_text-trix (2.1.19) + railties + actioncable (8.1.4) + actionpack (= 8.1.4) + activesupport (= 8.1.4) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (8.1.4) + actionpack (= 8.1.4) + activejob (= 8.1.4) + activerecord (= 8.1.4) + activestorage (= 8.1.4) + activesupport (= 8.1.4) + mail (>= 2.8.0) + actionmailer (8.1.4) + actionpack (= 8.1.4) + actionview (= 8.1.4) + activejob (= 8.1.4) + activesupport (= 8.1.4) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.1.4) + actionview (= 8.1.4) + activesupport (= 8.1.4) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.1.4) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.4) + activerecord (= 8.1.4) + activestorage (= 8.1.4) + activesupport (= 8.1.4) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.1.4) + activesupport (= 8.1.4) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.1.4) + activesupport (= 8.1.4) + globalid (>= 0.3.6) + activemodel (8.1.4) + activesupport (= 8.1.4) + activemodel-serializers-xml (1.0.3) + activemodel (>= 5.0.0.a) + activesupport (>= 5.0.0.a) + builder (~> 3.1) + activerecord (8.1.4) + activemodel (= 8.1.4) + activesupport (= 8.1.4) + timeout (>= 0.4.0) + activestorage (8.1.4) + actionpack (= 8.1.4) + activejob (= 8.1.4) + activerecord (= 8.1.4) + activesupport (= 8.1.4) + marcel (~> 1.0) + activesupport (8.1.4) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + json + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + arbre (2.2.1) + activesupport (>= 7.0) + ast (2.4.3) + base64 (0.3.0) + bcrypt (3.1.22) + bigdecimal (4.1.3) + builder (3.3.0) + cancancan (3.6.1) + capybara (3.40.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + childprocess (5.1.0) + logger (~> 1.5) + concurrent-ruby (1.3.8) + connection_pool (3.0.2) + crass (1.0.7) + cssbundling-rails (1.4.3) + railties (>= 6.0.0) + csv (3.3.6) + cucumber (11.1.1) + base64 (~> 0.2) + builder (~> 3.2) + cucumber-ci-environment (> 9, < 12) + cucumber-core (>= 16.2.0, < 17) + cucumber-cucumber-expressions (> 17, < 20) + cucumber-html-formatter (> 21, < 24) + diff-lcs (~> 1.5) + logger (~> 1.6) + mini_mime (~> 1.1) + multi_test (~> 1.1) + sys-uname (~> 1.5) + cucumber-ci-environment (11.0.0) + cucumber-core (16.2.0) + cucumber-gherkin (> 36, < 40) + cucumber-messages (> 31, < 33) + cucumber-tag-expressions (> 6, < 9) + cucumber-cucumber-expressions (19.0.1) + bigdecimal + cucumber-gherkin (39.1.0) + cucumber-messages (>= 31, < 33) + cucumber-html-formatter (23.1.0) + cucumber-messages (> 23, < 33) + cucumber-messages (32.3.1) + cucumber-rails (4.1.0) + capybara (>= 3.25, < 4) + cucumber (>= 7, < 12) + railties (>= 6.1, < 9) + cucumber-tag-expressions (8.1.0) + cuprite (0.18) + capybara (~> 3.0) + ferrum (~> 0.18.0) + database_cleaner-active_record (2.2.2) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0) + database_cleaner-core (2.1.0) + date (3.5.1) + devise (5.0.4) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 7.0) + responders + warden (~> 1.2.3) + diff-lcs (1.6.2) + draper (4.0.6) + actionpack (>= 5.0) + activemodel (>= 5.0) + activemodel-serializers-xml (>= 1.0) + activesupport (>= 5.0) + request_store (>= 1.0) + ruby2_keywords + drb (2.2.3) + erb (6.0.7) + erubi (1.13.1) + ferrum (0.18.0) + addressable (~> 2.5) + base64 (~> 0.2) + concurrent-ruby (~> 1.1) + websocket-driver (~> 0.7) + ffi (1.17.4-aarch64-linux-gnu) + ffi (1.17.4-arm64-darwin) + ffi (1.17.4-x86_64-darwin) + ffi (1.17.4-x86_64-linux-gnu) + formtastic (6.0.0) + actionpack (>= 7.2.0) + formtastic_i18n (0.7.0) + globalid (1.4.0) + activesupport (>= 6.1) + has_scope (0.9.0) + actionpack (>= 7.0) + activesupport (>= 7.0) + highline (3.1.2) + reline + i18n (1.15.2) + concurrent-ruby (~> 1.0) + i18n-spec (0.6.0) + iso + i18n-tasks (1.1.2) + activesupport (>= 4.0.2) + ast (>= 2.1.0) + erubi + highline (>= 3.0.0) + i18n + parser (>= 3.2.2.1) + prism + rails-i18n + rainbow (>= 2.2.2, < 4.0) + ruby-progressbar (~> 1.8, >= 1.8.1) + terminal-table (>= 1.5.1) + importmap-rails (2.2.3) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + inherited_resources (2.1.0) + actionpack (>= 7.0) + has_scope (>= 0.6) + railties (>= 7.0) + responders (>= 2) + io-console (0.9.4) + irb (1.18.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + iso (0.4.0) + i18n + json (3.0.2) + kaminari (1.2.2) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.2.2) + kaminari-activerecord (= 1.2.2) + kaminari-core (= 1.2.2) + kaminari-actionview (1.2.2) + actionview + kaminari-core (= 1.2.2) + kaminari-activerecord (1.2.2) + activerecord + kaminari-core (= 1.2.2) + kaminari-core (1.2.2) + language_server-protocol (3.17.0.6) + launchy (3.1.1) + addressable (~> 2.8) + childprocess (~> 5.0) + logger (~> 1.6) + lint_roller (1.1.0) + logger (1.7.0) + loofah (2.25.2) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.9.1) + logger + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.2.1) + matrix (0.4.3) + memoist3 (1.0.0) + mini_mime (1.1.5) + minitest (6.0.6) + drb (~> 2.0) + prism (~> 1.5) + multi_test (1.1.0) + net-imap (0.6.7) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.4.0) + timeout + net-smtp (0.5.2) + net-protocol + nio4r (2.7.5) + nokogiri (1.19.4-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.4-arm64-darwin) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-linux-gnu) + racc (~> 1.4) + orm_adapter (0.5.0) + parallel (2.3.0) + parallel_tests (5.8.0) + parallel + parser (3.3.12.0) + ast (~> 2.4.1) + racc + pp (0.6.4) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + public_suffix (7.0.5) + pundit (2.5.2) + activesupport (>= 3.0.0) + racc (1.8.1) + rack (3.2.7) + rack-session (2.1.2) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.3.1) + rack (>= 3) + rails (8.1.4) + actioncable (= 8.1.4) + actionmailbox (= 8.1.4) + actionmailer (= 8.1.4) + actionpack (= 8.1.4) + actiontext (= 8.1.4) + actionview (= 8.1.4) + activejob (= 8.1.4) + activemodel (= 8.1.4) + activerecord (= 8.1.4) + activestorage (= 8.1.4) + activesupport (= 8.1.4) + bundler (>= 1.15.0) + railties (= 8.1.4) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.7.1) + loofah (~> 2.25, >= 2.25.2) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + rails-i18n (8.1.0) + i18n (>= 0.7, < 2) + railties (>= 8.0.0, < 9) + railties (8.1.4) + actionpack (= 8.1.4) + activesupport (= 8.1.4) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.4.2) + ransack (5.0.2) + activerecord (>= 7.2) + activesupport (>= 7.2) + i18n + rbs (4.2.0) + logger + prism (>= 1.6.0) + tsort + rdoc (8.1.0) + erb + prism (>= 1.6.0) + rbs (>= 4.0.0) + tsort + regexp_parser (2.13.1) + reline (0.7.0) + io-console (~> 0.5) + request_store (1.7.0) + rack (>= 1.4) + responders (3.2.1) + actionpack (>= 7.0) + railties (>= 7.0) + rexml (3.4.4) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.8) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (8.0.4) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) + rspec-core (>= 3.13.0, < 5.0.0) + rspec-expectations (>= 3.13.0, < 5.0.0) + rspec-mocks (>= 3.13.0, < 5.0.0) + rspec-support (>= 3.13.0, < 5.0.0) + rspec-support (3.13.7) + rubocop (1.91.0) + json (>= 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (>= 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.49.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.50.0) + parser (>= 3.3.7.2) + prism (~> 1.7) + rubocop-capybara (3.0.0) + lint_roller (~> 1.1) + rubocop (~> 1.81) + rubocop-packaging (0.6.0) + lint_roller (~> 1.1.0) + rubocop (>= 1.72.1, < 2.0) + rubocop-performance (1.27.0) + lint_roller (~> 1.1) + rubocop (>= 1.89.0, < 2.0) + rubocop-ast (>= 1.47.1, < 2.0) + rubocop-rails (2.38.0) + activesupport (>= 4.2.0) + lint_roller (~> 1.1) + rack (>= 1.1) + rubocop (>= 1.89.0, < 2.0) + rubocop-ast (>= 1.44.0, < 2.0) + rubocop-rspec (3.10.2) + lint_roller (~> 1.1) + regexp_parser (>= 2.0) + rubocop (~> 1.86, >= 1.86.2) + ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) + securerandom (0.4.1) + simplecov (1.3.2) + simplecov-cobertura (4.0.0) + rexml + simplecov (~> 1.0) + sprockets (4.4.1) + concurrent-ruby (~> 1.1) + logger + rack (>= 2.2.4, < 4) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) + sprockets (>= 3.0.0) + sqlite3 (2.9.6-aarch64-linux-gnu) + sqlite3 (2.9.6-arm64-darwin) + sqlite3 (2.9.6-x86_64-darwin) + sqlite3 (2.9.6-x86_64-linux-gnu) + sys-uname (1.5.1) + ffi (~> 1.1) + memoist3 (~> 1.0.0) + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) + thor (1.5.0) + timeout (0.6.1) + tsort (0.2.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.3.0) + unicode-emoji (~> 4.3) + unicode-emoji (4.3.0) + uri (1.1.1) + useragent (0.16.11) + warden (1.2.9) + rack (>= 2.0.9) + webrick (1.9.2) + websocket-driver (0.8.2) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.8.3) + +PLATFORMS + aarch64-linux + arm64-darwin + x86_64-darwin + x86_64-linux + +DEPENDENCIES + activeadmin! + cancancan + capybara + cssbundling-rails + cucumber-rails + cuprite + database_cleaner-active_record + devise + draper + formtastic (>= 6.0.0) + i18n-spec + i18n-tasks + importmap-rails + launchy + parallel_tests + pundit + rails (~> 8.1.0) + rails-i18n + rake + ransack (>= 4.2.0) + rspec-rails + rubocop + rubocop-capybara + rubocop-packaging + rubocop-performance + rubocop-rails + rubocop-rspec + simplecov + simplecov-cobertura + sprockets-rails + sqlite3 + webrick + +BUNDLED WITH + 4.0.22 diff --git a/Guardfile b/Guardfile deleted file mode 100644 index 882515b251f..00000000000 --- a/Guardfile +++ /dev/null @@ -1,7 +0,0 @@ -# More info at https://github.com/guard/guard#readme - -guard 'rspec', :all_on_start => false, :version => 2 do - watch(%r{^spec/.+_spec\.rb$}) - watch(%r{^lib/active_admin/(.+)\.rb$}) { |m| "spec/unit/#{m[1]}_spec.rb" } - watch('spec/spec_helper.rb') { "spec/" } -end diff --git a/LICENSE b/LICENSE index 414eaed980f..065d975dbb8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2013 Greg Bell, VersaPay Corporation +Copyright (c) Greg Bell, VersaPay Corporation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -18,8 +18,3 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -Iconic Icons are designed by P.J. Onori and are shared under -the Creative Commons Attribution-Share Alike 3.0 license: -http://creativecommons.org/licenses/by-sa/3.0/us -http://somerandomdude.com/projects/iconic/ diff --git a/README.md b/README.md new file mode 100644 index 00000000000..90b76bb70b3 --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +# Active Admin + +[Active Admin](https://activeadmin.info) is a Ruby on Rails framework for +creating elegant backends for website administration. + +[![Version][rubygems_badge]][rubygems] +[![Github Actions][actions_badge]][actions] +[![Coverage][coverage_badge]][coverage] +[![Tidelift][tidelift_badge]][tidelift] + +## Goals + +* Enable developers to quickly create good-looking administration interfaces. +* Build a DSL for developers and an interface for businesses. +* Ensure that developers can easily customize every nook and cranny. + +## Getting started + +* Review [the documentation][docs]. +* For help, questions, etc. [discuss ActiveAdmin on GitHub](https://github.com/activeadmin/activeadmin/discussions) or use [StackOverflow][stackoverflow] +* The [wiki] includes links to tutorials, articles and sample projects. + +## For enterprise + +Active Admin for enterprise is available via the Tidelift subscription. [Learn +More][tidelift_enterprise]. + +## Want to contribute? + +If you want to contribute through code or documentation, the [Contributing +guide is the best place to start][contributing]. If you have questions, feel free +to ask. + +## Want to support us? + +If you want to support us financially, you can [help fund the project +through a Tidelift subscription][tidelift_support]. By buying a Tidelift subscription +you make sure your whole dependency stack is properly maintained, while also +getting a comprehensive view of outdated dependencies, new releases, security +alerts, and licensing compatibility issues. + +You can also support us with a weekly tip via [Liberapay]. + +Finally, we have an [Open Collective][opencollective page] where you can become a backer or +sponsor for the project, and also submit expenses to it. + +## Dependencies + +We try not to reinvent the wheel, so Active Admin is built with other open source projects: + +* [Arbre] +* [Devise] +* [Flowbite](https://flowbite.com) +* [Formtastic] +* [Inherited Resources] +* [Kaminari] +* [Ransack] +* [Tailwind CSS](https://tailwindcss.com) + +## Security contact information + +Please use the Tidelift security contact to [report a security vulnerability][Tidelift security contact]. +Tidelift will coordinate the fix and disclosure. + +## Acknowledgements + +Thanks to [Greg Bell][Greg] for creating and sharing this project with the open source community. + +Thanks to [all the people that ever contributed through code][contributors] or +other means such as bug reports, issue triaging, feature suggestions, code +snippet tips, Slack discussions and so on. + +Thanks to [Tidelift][tidelift] and all our Tidelift subscribers. + +Thanks to [Open Collective][opencollective contributors] and all our Open Collective contributors. + +[Arbre]: https://github.com/activeadmin/arbre +[Devise]: https://github.com/heartcombo/devise +[Formtastic]: https://github.com/formtastic/formtastic +[Inherited Resources]: https://github.com/activeadmin/inherited_resources +[Kaminari]: https://github.com/kaminari/kaminari +[Ransack]: https://github.com/activerecord-hackery/ransack + +[rubygems_badge]: https://img.shields.io/gem/v/activeadmin.svg +[rubygems]: https://rubygems.org/gems/activeadmin +[actions_badge]: https://github.com/activeadmin/activeadmin/workflows/ci/badge.svg +[actions]: https://github.com/activeadmin/activeadmin/actions +[coverage_badge]: https://codecov.io/gh/activeadmin/activeadmin/branch/master/graph/badge.svg?token=NAjeBdkQXW +[coverage]: https://codecov.io/gh/activeadmin/activeadmin +[tidelift_badge]: https://tidelift.com/badges/github/activeadmin/activeadmin +[tidelift]: https://tidelift.com/subscription/pkg/rubygems-activeadmin?utm_source=rubygems-activeadmin&utm_medium=readme +[tidelift_enterprise]: https://tidelift.com/subscription/pkg/rubygems-activeadmin?utm_source=rubygems-activeadmin&utm_medium=referral&utm_campaign=enterprise +[tidelift_support]: https://tidelift.com/subscription/pkg/rubygems-activeadmin?utm_source=rubygems-activeadmin&utm_medium=referral&utm_campaign=github&utm_content=support + +[docs]: https://activeadmin.info/ +[wiki]: https://github.com/activeadmin/activeadmin/wiki +[stackoverflow]: https://stackoverflow.com/questions/tagged/activeadmin +[contributing]: https://github.com/activeadmin/activeadmin/blob/master/CONTRIBUTING.md +[Liberapay]: https://liberapay.com/Active-Admin/donate +[Tidelift security contact]: https://tidelift.com/security +[Greg]: https://github.com/gregbell +[contributors]: https://github.com/activeadmin/activeadmin/graphs/contributors +[opencollective page]: https://opencollective.com/activeadmin +[opencollective contributors]: https://opencollective.com/activeadmin#contributors diff --git a/README.rdoc b/README.rdoc deleted file mode 100644 index 238bffbfadd..00000000000 --- a/README.rdoc +++ /dev/null @@ -1,118 +0,0 @@ -= Active Admin - -Active Admin is a framework for creating administration style interfaces. It -abstracts common business application patterns to make it simple for developers -to implement beautiful and elegant interfaces with very little effort. - -{}[http://travis-ci.org/gregbell/active_admin] -{}[https://codeclimate.com/github/gregbell/active_admin] -{}[https://gemnasium.com/gregbell/active_admin] -{}[https://coveralls.io/r/gregbell/active_admin?branch=master] - -== Documentation & Support - -* Documentation & Guides: http://activeadmin.info/documentation.html -* Wiki: https://github.com/gregbell/active_admin/wiki -* RDoc: http://rubydoc.info/gems/activeadmin/frames -* Live demo: http://demo.activeadmin.info/admin -* Website: http://www.activeadmin.info -* Need Support? Ask the Mailing list: http://groups.google.com/group/activeadmin - -== Goals - -1. Allow developers to quickly create gorgeous administration interfaces - (Not Just CRUD) -2. Build a DSL for developers and an interface for businesses. -3. Ensure that developers can easily customize every nook and cranny of the interface. -4. Build common interfaces as shareable gems so that the entire community benefits. - -== Bugs Reports & Contributing - -* Bug Reports: https://github.com/gregbell/active_admin/issues -* Want to Contribute? Read the Guide: https://github.com/gregbell/active_admin/blob/master/CONTRIBUTING.md - -== Feature Requests - -Please don't put feature requests in Github Issues. They will be closed as soon as they -are reviewed by one of the core team members. If you would like a feature in Active Admin, -please submit a well tested pull request with the desired changes. If you're not a coder, -then the mailing list may be a good place to try to convince someone to help you out -with your cause. - -If you are going to submit a pull request, please read the contributing guide: -https://github.com/gregbell/active_admin/blob/master/CONTRIBUTING.md - - -== Getting Started - -Active Admin is released as a Ruby Gem. The gem is to be installed within a Ruby -on Rails 3 application. To install, simply add the following to your Gemfile: - - gem 'activeadmin' - -After updating your bundle, run the installer - - $> rails generate active_admin:install - -The installer creates an initializer used for configuring defaults used by Active Admin as well -as a new folder at app/admin to put all your admin configurations. - -Migrate your db and start the server: - - $> rake db:migrate - $> rails server - -Visit http://localhost:3000/admin and log in using: - -* *User*: admin@example.com -* *Password*: password - -Voila! You're on your brand new Active Admin dashboard. - -To register your first model, run: - - $> rails generate active_admin:resource [MyModelName] - -This creates a file at app/admin/my_model_names.rb for configuring the resource. -Refresh your web browser to see the interface. - -== Upgrading - -When upgrading to a new version of ActiveAdmin you may need to run - $> rails generate active_admin:assets - -If you get: - uninitialized constant Admin::DashboardController -when trying to view the dashboard (at +/admin+), ensure +app/admin/dashboards.rb+ looks like the current {default}[https://raw.github.com/gregbell/active_admin/master/lib/generators/active_admin/install/templates/dashboard.rb]. - - -== Next Steps - -The best place to get documentation is at http://activeadmin.info/documentation.html. - -To view a sample Active Admin application, checkout http://demo.activeadmin.info - -If you have any questions, please email the mailing list at http://groups.google.com/group/activeadmin - - -== Tools Being Used - -We believe strongly in not writing code unless we have to, so Active Admin is built using many -other open source projects: - -InheritedResources:: - Inherited Resources speeds up development by making your controllers inherit all restful - actions so you just have to focus on what is important. -Formtastic:: - A DSL for semantically building amazing forms. -Devise:: - User authentication is done using Devise -Kaminari:: - Pagination for rails apps -Iconic Icons:: - Excellent SVG icon set designed by P.J. Onori: http://somerandomdude.com/projects/iconic - - -== Copyright - -Copyright (c) 2011 Greg Bell, VersaPay Corporation. See LICENSE for details. diff --git a/Rakefile b/Rakefile index ce8f13a3693..e26229820b9 100644 --- a/Rakefile +++ b/Rakefile @@ -1,27 +1,21 @@ -require "bundler" -require 'rake' -Bundler.setup -Bundler::GemHelper.install_tasks +# frozen_string_literal: true +require "bundler/gem_tasks" +import "tasks/local.rake" +import "tasks/test.rake" +import "tasks/dependencies.rake" -def cmd(command) - puts command - raise unless system command -end - -require File.expand_path('../spec/support/detect_rails_version', __FILE__) +gemfile = ENV["BUNDLE_GEMFILE"] -# Import all our rake tasks -FileList['tasks/**/*.rake'].each { |task| import task } +if gemfile.nil? || File.expand_path(gemfile) == File.expand_path("Gemfile") + import "tasks/release.rake" +end -# Run the specs & cukes using parallel_tests -task :default => :parallel_tests +task default: :test -begin - require 'jasmine' - load 'jasmine/tasks/jasmine.rake' -rescue LoadError - task :jasmine do - abort "Jasmine is not available. In order to run jasmine, you must: (sudo) gem install jasmine" - end +task :console do + require "irb" + require "irb/completion" + ARGV.clear + IRB.start end diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 00000000000..d3e54a5cf25 --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,274 @@ +# Upgrading Guide + +## From v3 to v4 (beta) + +ActiveAdmin v4 uses Tailwind CSS v4. It has **mobile web, dark mode and RTL support** with a default theme that can be customized through partials and CSS. This release assumes `cssbundling-rails` and `importmap-rails` is installed and configured in the host app. Partials can be modified to include a different asset library, e.g. shakapacker. +**IMPORTANT**: there is **no sortable functionality for has-many forms** in this release so if needed, **do not upgrade**. We are [open to community proposals](https://github.com/activeadmin/activeadmin/discussions/new?category=ideas). The add/remove functionality for has-many forms remains supported. + +These instructions assume the `cssbundling-rails` and `importmap-rails` gems are already installed and you have run their install commands in your app. If you haven't done so, please do before continuing. + +Update your `Gemfile` with `gem "activeadmin", "4.0.0.beta23"` and then run `gem install activeadmin --pre`. + +Now, run `rails generate active_admin:assets` to replace the old assets with the new files. + +Then add the npm package and update the `build:css` script. + +``` +npm install @activeadmin/activeadmin@4.0.0-beta23 +npm pkg set scripts.build:css="npx @tailwindcss/cli -i ./app/assets/stylesheets/active_admin.css -o ./app/assets/builds/active_admin.css --minify" +``` + +If you are already using Tailwind in your app, then update the `build:css` script to chain the above command to your existing one, e.g. `"npx @tailwindcss/cli ... && npx @tailwindcss/cli ..."`, so both stylesheets are generated. + +Many configs have been removed (meta tags, asset registration, utility nav, etc.) that can now be modified more naturally through partials. + +Open the `config/initializers/active_admin.rb` file and remove these deleted configs. + +``` +site_title_link +site_title_image +logout_link_method +favicon +meta_tags +meta_tags_for_logged_out_pages +register_stylesheet +register_javascript +head +footer +use_webpacker +``` + +Now, run `rails g active_admin:views` which will copy the partials to your app so you can customize if needed. + +Note that the templates can and will change across releases. There are additional partials that can be copied but they are considered private so you do so at your own risk. You will have to keep those up to date per release. + +**IMPORTANT**: if your project has copied any ActiveAdmin, Devise, or Kaminari templates from earlier releases, those templates must be updated from this release to avoid potential errors. Path helpers in Devise templates may require using the `main_app` proxy. The Kaminari templates have moved to `app/views/active_admin/kaminari`. + +With the setup complete, please review the Breaking Changes section and resolve any that may or may not impact your integration. + +### Upgrading from an earlier 4.x beta release + +When upgrading from any earlier 4.0.0 beta release, please apply the changes outlined below. + +There were important template changes in 4.0.0.beta16. See [PR #8727](https://github.com/activeadmin/activeadmin/pull/8727) for details. +- The `_site_header.html.erb` partial has changed its main container class from `sticky` to `fixed`. +- The main layout for `active_admin.html.erb` now includes the `pt-16` utility class. + +Starting with 4.0.0.beta19, we've migrated to Tailwind CSS v4 which requires several updates. + +Update your `active_admin.css` file: + +```diff +-@tailwind base; +-@tailwind components; +-@tailwind utilities; ++@import "tailwindcss"; ++ ++@config "../../../tailwind-active_admin.config.js"; +``` + +Update the `build:css` script in your `package.json`: + +```diff +-"build:css": "tailwindcss -i ./app/assets/stylesheets/active_admin.css -o ./app/assets/builds/active_admin.css --minify -c tailwind-active_admin.config.js" ++"build:css": "npx @tailwindcss/cli -i ./app/assets/stylesheets/active_admin.css -o ./app/assets/builds/active_admin.css --minify" +``` + +You may see the following warning when upgrading: + +``` +[MODULE_TYPELESS_PACKAGE_JSON] Warning: Module type of tailwind-active_admin.config.js is not specified and it doesn't parse as CommonJS. +Reparsing as ES module because module syntax was detected. This incurs a performance overhead. +To eliminate this warning, add "type": "module" to ./package.json. +``` + +The Tailwind config file now uses ES module syntax. To fix it, either: +- Rename `tailwind-active_admin.config.js` to `tailwind-active_admin.config.mjs`; or +- Add `"type": "module"` to your `package.json` (your application may already be compatible with ESM). + +### Breaking Changes +- jQuery and jQuery UI have been removed. +- The `columns` component has been removed. Use `div`'s with Tailwind classes for modern, responsive layout. + +
+ Columns Component Migration Alternative + + If you did not specify any parameters for `column` and if all you need is equal width columns, then this single component will restore that functionality for any number of columns. + + ```ruby + # app/admin/components/columns.rb + class Columns < ActiveAdmin::Component + builder_method :columns + + def build(*args) + super + add_class "grid auto-cols-fr grid-flow-col gap-4 mb-4" + end + + def column(*args, &block) + insert_tag Arbre::HTML::Div, *args, &block + end + end + ``` + + Using Tailwind modifiers you can further customize the number of columns for responsive/mobile support. +
+ +- The `tabs` component has been removed. Use a CSS based or third party alternative. +- Replace `default_main_content` with `render "show_default"`. + +
+ Show Default Alternative + + If block form `default_main_content do ... end` was used or looking for a partial file + alternative, then replace with existing, public methods. + + ```ruby + attributes_table_for(resource) do + rows *active_admin_config.resource_columns + row :a + row :b + # ... + end + active_admin_comments_for(resource) if active_admin_config.comments? + ``` +
+ +- Replace `as: :datepicker` with Formtastic's `as: :date_picker` for native HTML date input. +- Replace `active_admin_comments` with `active_admin_comments_for(resource)`. +- In a sidebar section, replace `attributes_table` with `attributes_table_for(resource)`. +- The `IndexAsBlog`, `IndexAsBlock` and `IndexAsGrid` components have been removed. Please create your own custom index-as components which remain supported. +- Batch Actions Form DSL has been replaced with Rails partial support so you can supply your own custom form and modal. + +
+ Batch Action Partial Example + + Assuming a Post resource (in the default namespace) with a `mark_published` batch action, we set the partial name and a set of HTML data attributes to trigger a modal using Flowbite which is included by default. + + Note that you can use any modal JS library you want as long as it can be triggered to open using data attributes. Flowbite usage is not a requirement. + + ```ruby + batch_action( + :mark_published, + partial: "mark_published_batch_action", + link_html_options: { + "data-modal-target": "mark-published-modal", + "data-modal-show": "mark-published-modal" + } + ) do |ids, inputs| + # ... + end + ``` + + In the `app/views/admin/posts` directory, create a `_mark_published_batch_action.html.erb` partial file which will be rendered and included automatically in the posts index admin page. + + Now add the modal HTML where the `id` attribute must match the data attributes supplied in the `batch_action` example. The form must have an empty `data-batch-action-form` attribute. + + ``` + + ``` + + The `data-batch-action-form` attribute is a hook for a delegated JS event so when you submit the form, it will post and run your batch action block with the supplied form data, functioning as it did before. +
+ +- Deeply nested submenus has been reverted. Only one level nested menu, e.g. `menu parent: "Administrative"`, is supported. +- Removed `Panel#header_action` method. +- Removed `index_column` method from index table. + +
+ Implementation Example + + You can re-implement this column with the following: + + ```ruby + column "Number", sortable: false do |item| + @collection.offset_value + @collection.index(item) + 1 + end + ``` +
+- Using `row 'name'` in an `attributes_table` block, now prints the header text as is. For a localized lookup of the header with titlized fallback, use `row :name` instead. This matches the `column 'name'` behavior of index tables (`TableFor`). + +#### Resource named methods + +With the extraction to partials, resource named methods, e.g. `post` or `posts`, used in blocks for `action_item` and `sidebar` will raise an error. You must use the `resource` or `collection` public helper method instead. For example: + +```ruby +action_item :view, if: ->{ post.published? } do link_to(resource) end +sidebar :author, if: ->{ post.published? } +# The above must now change to the following: +action_item :view, if: ->{ resource.published? } do link_to(resource) end +sidebar :author, if: ->{ resource.published? } +``` + +Note that `@post` can also be used here but make sure to call `authorize!` on it if using the authorization feature. The `post` usage would continue to work for `sidebar :name do ... end` content blocks because they can include Arbre but we advise using `resource` or `collection` instead where possible. This may impact other DSL's. + +### Visual Related Changes +- The `sidebar do ... end` contents and the show resource `attributes_table`, are no longer wrapped in a panel so they can be customized. +- Links in custom `action_item`'s have no default styles. Apply your own or use the library's default `action-item-button` class. +- The index table `actions dropdown: true` option will be ignored, reverting to original output. +- An `Arbre::Component` will no longer add a CSS class using the component class name by default. +- Typographic elements (other than links in main content) [are not styled by default](https://tailwindcss.com/docs/preflight). Use the `@tailwindcss/typography` plugin or apply your own CSS alternative. + +### Enhancements +- Dark mode support. +- Mobile web support. For responsive `table_for`'s, wrap them in a div with overflow for horizontal scrolling. +- Customizable admin theme, including main menu and user menu, all through partials. +- RTL support improved. Now using CSS Logical Properties. +- Kaminari templates now consolidated into a single set you can customize. +- Datepicker's now use the native HTML date input. Apply a custom datepicker of your choosing. +- Batch Actions Form DSL has been replaced with partials and form builder for more customization. Please refer to earlier example. +- The `status_tag` component now uses unique labels for `false` and `nil` values. +- Several components: `table_for`, `status_tag`, etc. now use data attributes instead of classes for metadata: status, sort direction, column, etc. +- Arbre builder methods have been reduced to the minimum so you can use elements or DSLs without clashing e.g. `header`, `footer`, `columns`, etc. +- The [app-helpers-not-reloading bug has been fixed](https://github.com/activeadmin/activeadmin/pull/8180) and the engine namespace is now isolated. + +### Localization Updates + +This release includes several locale changes. Please [review the en.yml locale](https://github.com/activeadmin/activeadmin/blob/master/config/locales/en.yml) for the latest translations. + +- Removed keys: `dashboard_welcome`, `dropdown_actions`, `main_content` and `unsupported_browser`. +- New keys: `toggle_dark_mode`, `toggle_main_navigation_menu`, `toggle_section`, and `toggle_user_menu` have been added. +- The `active_admin.pagination` keys have been rewritten to be less verbose and include new entries: next and previous. + + ```diff + - one: "Displaying 1 %{model}" + + one: "Showing 1 of 1" + - one_page: "Displaying all %{n} %{model}" + + one_page: "Showing all %{n}" + - multiple: "Displaying %{model} %{from} - %{to} of %{total} in total" + + multiple: "Showing %{from}-%{to} of %{total}" + - multiple_without_total: "Displaying %{model} %{from} - %{to}" + + multiple_without_total: "Showing %{from}-%{to}" + - per_page: "Per page: " + + per_page: "Per page " + + previous: "Previous" + + next: "Next" + ``` + +- The `search_status` key contents has multiple, breaking changes: + + ```diff + - headline: "Search status:" + - current_scope: "Scope:" + - current_filters: "Current filters:" + + title: "Active Search" + + title_with_scope: "Active Search for %{name}" + - no_current_filters: "None" + + no_current_filters: "No filters applied" + ``` + +- The value for the `status_tag.unset` key has changed from "No" to "Unknown". +- The `comments.title_content` text has been updated with an "All " prefix. +- The `comments.delete_confirmation` text has been fixed to use singular form. +- The `batch_actions.succesfully_destroyed` key has been renamed to fix a typo. + + ```diff + - succesfully_destroyed: + + successfully_destroyed: + ``` +- Inconsistent use of login/sign-in related terms so text now uses "Sign in", Sign out", and "Sign up" throughout. diff --git a/_typos.toml b/_typos.toml new file mode 100644 index 00000000000..a89d7205417 --- /dev/null +++ b/_typos.toml @@ -0,0 +1,20 @@ +# https://github.com/crate-ci/typos#false-positives +[default] + +[default.extend-identifiers] + +[default.extend-words] +rememberable = "rememberable" + +[type.md] +extend-ignore-identifiers-re = [ + "succesfully_destroyed" +] + +[files] +extend-exclude = [ + "config/locales/*", + "!config/locales/en*.yml", + "features/step_definitions/batch_action_steps.rb", + "vendor/*" +] diff --git a/activeadmin.gemspec b/activeadmin.gemspec index a6554756158..805a955ddd2 100644 --- a/activeadmin.gemspec +++ b/activeadmin.gemspec @@ -1,32 +1,42 @@ -# -*- encoding: utf-8 -*- -$:.push File.expand_path("../lib", __FILE__) -require "active_admin/version" +# frozen_string_literal: true +require File.join(__dir__, "lib", "active_admin", "version") Gem::Specification.new do |s| - s.name = %q{activeadmin} - s.license = "MIT" - s.version = ActiveAdmin::VERSION - s.platform = Gem::Platform::RUBY - s.homepage = %q{http://activeadmin.info} - s.authors = ["Greg Bell"] - s.email = ["gregdbell@gmail.com"] - s.description = %q{The administration framework for Ruby on Rails.} - s.summary = %q{The administration framework for Ruby on Rails.} + s.name = "activeadmin" + s.license = "MIT" + s.version = ActiveAdmin::VERSION + s.homepage = "https://activeadmin.info" + s.authors = ["Charles Maresh", "David Rodríguez", "Greg Bell", "Igor Fedoronchuk", "Javier Julio", "Piers C", "Sean Linsley", "Timo Schilling"] + s.email = ["deivid.rodriguez@riseup.net"] + s.description = "The administration framework for Ruby on Rails." + s.summary = "Active Admin is a Ruby on Rails plugin for generating " \ + "administration style interfaces. It abstracts common business " \ + "application patterns to make it simple for developers to implement " \ + "beautiful and elegant interfaces with very little effort." - s.files = `git ls-files`.split("\n").sort - 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.files = Dir["LICENSE", "plugin.js", 'config/importmap.rb', "{app,config/locales,lib,vendor}/**/{.*,*}"].reject { |f| File.directory?(f) } - s.add_dependency("rails", ">= 3.0.0") - s.add_dependency("jquery-rails", ">= 1.0.0", "< 3.0.0") - s.add_dependency("bourbon", ">= 1.0.0") - s.add_dependency("meta_search", ">= 0.9.2") - s.add_dependency("devise", ">= 1.1.2") - s.add_dependency("formtastic", ">= 2.0.0") - s.add_dependency("inherited_resources", ">= 1.3.1") - s.add_dependency("kaminari", ">= 0.13.0") - s.add_dependency("sass", ">= 3.1.0") - s.add_dependency("fastercsv", ">= 0") - s.add_dependency("arbre", ">= 1.0.1") + s.extra_rdoc_files = %w[CHANGELOG.md CODE_OF_CONDUCT.md CONTRIBUTING.md README.md UPGRADING.md] + + s.metadata = { + "bug_tracker_uri" => "https://github.com/activeadmin/activeadmin/issues", + "changelog_uri" => "https://github.com/activeadmin/activeadmin/releases", + "documentation_uri" => "https://activeadmin.info", + "homepage_uri" => "https://activeadmin.info", + "mailing_list_uri" => "https://groups.google.com/group/activeadmin", + "rubygems_mfa_required" => "true", + "source_code_uri" => "https://github.com/activeadmin/activeadmin", + "wiki_uri" => "https://github.com/activeadmin/activeadmin/wiki" + } + + s.required_ruby_version = ">= 3.3" + + s.add_dependency "arbre", "~> 2.0" + s.add_dependency "csv" + s.add_dependency "formtastic", ">= 6.0" + s.add_dependency "formtastic_i18n", ">= 0.7" + s.add_dependency "inherited_resources", "~> 2.0" + s.add_dependency "kaminari", ">= 1.2.1" + s.add_dependency "railties", ">= 7.2" + s.add_dependency "ransack", ">= 4.0" end diff --git a/app/assets/config/active_admin_manifest.js b/app/assets/config/active_admin_manifest.js new file mode 100644 index 00000000000..5d4ceea5b82 --- /dev/null +++ b/app/assets/config/active_admin_manifest.js @@ -0,0 +1,2 @@ +//= link_tree ../../javascript .js +//= link_tree ../../../vendor/javascript .js diff --git a/app/assets/images/active_admin/admin_notes_icon.png b/app/assets/images/active_admin/admin_notes_icon.png deleted file mode 100644 index 1d240a1a1ee..00000000000 Binary files a/app/assets/images/active_admin/admin_notes_icon.png and /dev/null differ diff --git a/app/assets/images/active_admin/datepicker/datepicker-header-bg.png b/app/assets/images/active_admin/datepicker/datepicker-header-bg.png deleted file mode 100644 index ccecd966842..00000000000 Binary files a/app/assets/images/active_admin/datepicker/datepicker-header-bg.png and /dev/null differ diff --git a/app/assets/images/active_admin/datepicker/datepicker-input-icon.png b/app/assets/images/active_admin/datepicker/datepicker-input-icon.png deleted file mode 100644 index 40ad6f74074..00000000000 Binary files a/app/assets/images/active_admin/datepicker/datepicker-input-icon.png and /dev/null differ diff --git a/app/assets/images/active_admin/datepicker/datepicker-next-link-icon.png b/app/assets/images/active_admin/datepicker/datepicker-next-link-icon.png deleted file mode 100644 index eae8bbe0939..00000000000 Binary files a/app/assets/images/active_admin/datepicker/datepicker-next-link-icon.png and /dev/null differ diff --git a/app/assets/images/active_admin/datepicker/datepicker-nipple.png b/app/assets/images/active_admin/datepicker/datepicker-nipple.png deleted file mode 100644 index b04c8f00b8f..00000000000 Binary files a/app/assets/images/active_admin/datepicker/datepicker-nipple.png and /dev/null differ diff --git a/app/assets/images/active_admin/datepicker/datepicker-prev-link-icon.png b/app/assets/images/active_admin/datepicker/datepicker-prev-link-icon.png deleted file mode 100644 index 03c1a0adece..00000000000 Binary files a/app/assets/images/active_admin/datepicker/datepicker-prev-link-icon.png and /dev/null differ diff --git a/app/assets/images/active_admin/index_list_icons/block_icon.svg b/app/assets/images/active_admin/index_list_icons/block_icon.svg deleted file mode 100755 index 70bf534cd2e..00000000000 --- a/app/assets/images/active_admin/index_list_icons/block_icon.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/app/assets/images/active_admin/index_list_icons/blog_icon.svg b/app/assets/images/active_admin/index_list_icons/blog_icon.svg deleted file mode 100755 index b9d3216d6a7..00000000000 --- a/app/assets/images/active_admin/index_list_icons/blog_icon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/assets/images/active_admin/index_list_icons/grid_icon.svg b/app/assets/images/active_admin/index_list_icons/grid_icon.svg deleted file mode 100755 index b1645fad641..00000000000 --- a/app/assets/images/active_admin/index_list_icons/grid_icon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/app/assets/images/active_admin/index_list_icons/table_icon.svg b/app/assets/images/active_admin/index_list_icons/table_icon.svg deleted file mode 100755 index 13b8068e15a..00000000000 --- a/app/assets/images/active_admin/index_list_icons/table_icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/assets/images/active_admin/loading.gif b/app/assets/images/active_admin/loading.gif deleted file mode 100644 index da33c3e03b6..00000000000 Binary files a/app/assets/images/active_admin/loading.gif and /dev/null differ diff --git a/app/assets/images/active_admin/nested_menu_arrow.gif b/app/assets/images/active_admin/nested_menu_arrow.gif deleted file mode 100644 index 878357fe4c3..00000000000 Binary files a/app/assets/images/active_admin/nested_menu_arrow.gif and /dev/null differ diff --git a/app/assets/images/active_admin/nested_menu_arrow_dark.gif b/app/assets/images/active_admin/nested_menu_arrow_dark.gif deleted file mode 100644 index 006372c8243..00000000000 Binary files a/app/assets/images/active_admin/nested_menu_arrow_dark.gif and /dev/null differ diff --git a/app/assets/images/active_admin/orderable.png b/app/assets/images/active_admin/orderable.png deleted file mode 100644 index 427009e7207..00000000000 Binary files a/app/assets/images/active_admin/orderable.png and /dev/null differ diff --git a/app/assets/javascripts/active_admin/application.js b/app/assets/javascripts/active_admin/application.js deleted file mode 100644 index 7860adf1cc2..00000000000 --- a/app/assets/javascripts/active_admin/application.js +++ /dev/null @@ -1,3 +0,0 @@ -//= require_tree ./lib/ -//= require_tree ./components/ -//= require_tree ./pages/ diff --git a/app/assets/javascripts/active_admin/base.js b/app/assets/javascripts/active_admin/base.js deleted file mode 100644 index f88ab6c237f..00000000000 --- a/app/assets/javascripts/active_admin/base.js +++ /dev/null @@ -1,5 +0,0 @@ -//= require jquery -//= require jquery-ui -//= require jquery_ujs - -//= require active_admin/application diff --git a/app/assets/javascripts/active_admin/components/jquery.aa.checkbox-toggler.js.coffee b/app/assets/javascripts/active_admin/components/jquery.aa.checkbox-toggler.js.coffee deleted file mode 100644 index fcbad1236ee..00000000000 --- a/app/assets/javascripts/active_admin/components/jquery.aa.checkbox-toggler.js.coffee +++ /dev/null @@ -1,37 +0,0 @@ -window.AA.CheckboxToggler = class AA.CheckboxToggler - constructor: (@options, @container)-> - defaults = {} - @options = $.extend {}, defaults, options - @_init() - @_bind() - - _init: -> - if not @container - throw new Error('Container element not found') - else - @$container = $(@container) - - if not @$container.find('.toggle_all').length - throw new Error('"toggle all" checkbox not found') - else - @toggle_all_checkbox = @$container.find '.toggle_all' - - @checkboxes = @$container.find(':checkbox').not @toggle_all_checkbox - - _bind: -> - @checkboxes.change (e)=> @_didChangeCheckbox e.target - @toggle_all_checkbox.change => @_didChangeToggleAllCheckbox() - - _didChangeCheckbox: (checkbox)-> - switch @checkboxes.filter(':checked').length - when @checkboxes.length - 1 then @toggle_all_checkbox.prop checked: null - when @checkboxes.length then @toggle_all_checkbox.prop checked: true - - _didChangeToggleAllCheckbox: -> - setting = if @toggle_all_checkbox.prop 'checked' then true else null - @checkboxes.each (index, el)=> - $(el).prop checked: setting - @_didChangeCheckbox(el) - -jQuery ($)-> - $.widget.bridge 'checkboxToggler', AA.CheckboxToggler diff --git a/app/assets/javascripts/active_admin/components/jquery.aa.dropdown-menu.js.coffee b/app/assets/javascripts/active_admin/components/jquery.aa.dropdown-menu.js.coffee deleted file mode 100644 index f23d7c25925..00000000000 --- a/app/assets/javascripts/active_admin/components/jquery.aa.dropdown-menu.js.coffee +++ /dev/null @@ -1,105 +0,0 @@ -window.AA.DropdownMenu = class AA.DropdownMenu - - constructor: (@options, @element) -> - - @$element = $(@element) - - defaults = { - fadeInDuration: 20, - fadeOutDuration: 100, - onClickActionItemCallback: null - } - - @options = $.extend({}, defaults, options) - - @$menuButton = @$element.find(".dropdown_menu_button") - @$menuList = @$element.find(".dropdown_menu_list_wrapper") - - @isOpen = false - - @_buildMenuList() - @_bind() - - open: -> - @isOpen = true - @$menuList.fadeIn @options.fadeInDuration - - @_positionMenuList() - @_positionNipple() - - return @ - - - close: -> - @isOpen = false - @$menuList.fadeOut this.options.fadeOutDuration - - return @ - - destroy: -> - @$element.unbind() - @$element = null - - return @ - - isDisabled: -> - @$menuButton.hasClass("disabled") - - disable: -> - @$menuButton.addClass("disabled") - - enable: -> - @$menuButton.removeClass("disabled") - - option: (key, value) -> - if $.isPlainObject(key) - return @options = $.extend(true, @options, key) - - else if key? - return @options[key] - - else - return @options[key] = value - - # Private - - _buildMenuList: -> - @$menuList.prepend("
") - @$menuList.hide() - - _bind: -> - $("body").bind 'click', () => - if @isOpen is true - @close() - - @$menuButton.bind 'click', () => - unless @isDisabled() - if @isOpen is true - @close() - else - @open() - - # Return false so that the event is stopped - false - - _positionMenuList: -> - centerOfButtonFromLeft = @$menuButton.position().left + @$menuButton.outerWidth() / 2 - centerOfmenuListFromLeft = @$menuList.outerWidth() / 2 - menuListLeftPos = centerOfButtonFromLeft - centerOfmenuListFromLeft - @$menuList.css "left", menuListLeftPos - - _positionNipple: -> - centerOfmenuListFromLeft = @$menuList.outerWidth() / 2 - bottomOfButtonFromTop = @$menuButton.position().top + @$menuButton.outerHeight() + 10 - @$menuList.css "top", bottomOfButtonFromTop - $nipple = @$menuList.find(".dropdown_menu_nipple") - centerOfnippleFromLeft = $nipple.outerWidth() / 2 - nippleLeftPos = centerOfmenuListFromLeft - centerOfnippleFromLeft - $nipple.css "left", nippleLeftPos - -(($) -> - $.widget.bridge 'aaDropdownMenu', AA.DropdownMenu - - $ -> - $(".dropdown_menu").aaDropdownMenu() -)(jQuery) diff --git a/app/assets/javascripts/active_admin/components/jquery.aa.popover.js.coffee b/app/assets/javascripts/active_admin/components/jquery.aa.popover.js.coffee deleted file mode 100644 index 0b63f3637ba..00000000000 --- a/app/assets/javascripts/active_admin/components/jquery.aa.popover.js.coffee +++ /dev/null @@ -1,97 +0,0 @@ - -window.AA.Popover = class AA.Popover - - constructor: (@options, @element) -> - - @$element = $(@element) - - defaults = { - fadeInDuration: 20, - fadeOutDuration: 100, - autoOpen: true, - pageWrapperElement: "#wrapper", - onClickActionItemCallback: null - } - - @options = $.extend({}, defaults, options) - - @$popover = null - @isOpen = false - - if $(@$element.attr("href")).length > 0 - @$popover = $(@$element.attr("href")) - else - @$popover = @$element.next(".popover") - - - @_buildPopover() - @_bind() - - open: -> - @isOpen = true - @$popover.fadeIn @options.fadeInDuration - - @_positionPopover() - @_positionNipple() - - return @ - - - close: -> - @isOpen = false; - @$popover.fadeOut this.options.fadeOutDuration; - - return @ - - destroy: -> - @$element.removeData('popover'); - @$element.unbind(); - @$element = null; - - return @ - - option: -> - # ?? - - # Private - - _buildPopover: -> - @$popover.prepend("
") - - @$popover.hide() - - @$popover.addClass "popover" - - - _bind: -> - $(@options.pageWrapperElement).bind 'click', (e) => - if @isOpen is true - @close() - - if @options.autoOpen is true - @$element.bind 'click', () => - if @isOpen is true - @close() - else - @open() - - false - - _positionPopover: -> - centerOfButtonFromLeft = @$element.offset().left + @$element.outerWidth() / 2 - centerOfPopoverFromLeft = @$popover.outerWidth() / 2 - popoverLeftPos = centerOfButtonFromLeft - centerOfPopoverFromLeft - @$popover.css "left", popoverLeftPos - - _positionNipple: -> - centerOfPopoverFromLeft = @$popover.outerWidth() / 2 - bottomOfButtonFromTop = @$element.offset().top + @$element.outerHeight() + 10 - @$popover.css "top", bottomOfButtonFromTop - $nipple = @$popover.find(".popover_nipple") - centerOfnippleFromLeft = $nipple.outerWidth() / 2 - nippleLeftPos = centerOfPopoverFromLeft - centerOfnippleFromLeft - $nipple.css "left", nippleLeftPos - -(($) -> - $.widget.bridge 'popover', AA.Popover -)(jQuery) diff --git a/app/assets/javascripts/active_admin/components/jquery.aa.table-checkbox-toggler.js.coffee b/app/assets/javascripts/active_admin/components/jquery.aa.table-checkbox-toggler.js.coffee deleted file mode 100644 index f397cfa95a0..00000000000 --- a/app/assets/javascripts/active_admin/components/jquery.aa.table-checkbox-toggler.js.coffee +++ /dev/null @@ -1,25 +0,0 @@ -window.AA.TableCheckboxToggler = class AA.TableCheckboxToggler extends AA.CheckboxToggler - _init: -> - super - - _bind: -> - super - - @$container.find('tbody td').click (e)=> - @_didClickCell(e.target) if e.target.type isnt 'checkbox' - - _didChangeCheckbox: (checkbox) -> - super - - $row = $(checkbox).parents 'tr' - - if checkbox.checked - $row.addClass 'selected' - else - $row.removeClass 'selected' - - _didClickCell: (cell) -> - $(cell).parent('tr').find(':checkbox').click() - -jQuery ($)-> - $.widget.bridge 'tableCheckboxToggler', AA.TableCheckboxToggler diff --git a/app/assets/javascripts/active_admin/lib/namespace.js.coffee b/app/assets/javascripts/active_admin/lib/namespace.js.coffee deleted file mode 100644 index 72ab2d31361..00000000000 --- a/app/assets/javascripts/active_admin/lib/namespace.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Make a namespace for ActiveAdmin plugins - -window.AA = {} diff --git a/app/assets/javascripts/active_admin/pages/application.js.coffee b/app/assets/javascripts/active_admin/pages/application.js.coffee deleted file mode 100644 index 5c74d6559fd..00000000000 --- a/app/assets/javascripts/active_admin/pages/application.js.coffee +++ /dev/null @@ -1,21 +0,0 @@ -# Initializers -$ -> - # jQuery datepickers (also evaluates dynamically added HTML) - $(document).on 'focus', '.datepicker:not(.hasDatepicker)', -> - $(@).datepicker dateFormat: 'yy-mm-dd' - - # Clear Filters button - $('.clear_filters_btn').click -> - window.location.search = '' - - # Batch Actions dropdown - $('.dropdown_button').popover() - - # Filter form: don't send any inputs that are empty - $('.filter_form').submit -> - $(@).find(':input').filter(-> @value is '').prop 'disabled', true - - # Filter form: for filters that let you choose the query method from - # a dropdown, apply that choice to the filter input field. - $('.filter_form_field.select_and_search select').change -> - $(@).siblings('input').prop name: "q[#{@value}]" diff --git a/app/assets/javascripts/active_admin/pages/batch_actions.js.coffee b/app/assets/javascripts/active_admin/pages/batch_actions.js.coffee deleted file mode 100644 index 413532aed06..00000000000 --- a/app/assets/javascripts/active_admin/pages/batch_actions.js.coffee +++ /dev/null @@ -1,26 +0,0 @@ -jQuery ($) -> - - # - # Use Rails.js click handler to allow for Rails' confirm dialogs - # - - $(document).delegate "#batch_actions_selector li a", "click.rails", -> - $("#batch_action").val $(@).attr("data-action") - $("#collection_selection").submit() - - # - # Add checkbox selection to resource tables and lists if batch actions are enabled - # - - if $("#batch_actions_selector").length && $(":checkbox.toggle_all").length - - if $(".paginated_collection table.index_table").length - $(".paginated_collection table.index_table").tableCheckboxToggler() - else - $(".paginated_collection").checkboxToggler() - - $(".paginated_collection").find(":checkbox").bind "change", -> - if $(".paginated_collection").find(":checkbox").filter(":checked").length > 0 - $("#batch_actions_selector").aaDropdownMenu("enable") - else - $("#batch_actions_selector").aaDropdownMenu("disable") diff --git a/app/assets/stylesheets/active_admin/_base.css.scss b/app/assets/stylesheets/active_admin/_base.css.scss deleted file mode 100644 index 9489c33d14a..00000000000 --- a/app/assets/stylesheets/active_admin/_base.css.scss +++ /dev/null @@ -1,41 +0,0 @@ -/* Active Admin CSS */ -// Reset Away! -@include global-reset; - -// Partials -@import "active_admin/typography"; -@import "active_admin/header"; -@import "active_admin/forms"; -@import "active_admin/components/comments"; -@import "active_admin/components/flash_messages"; -@import "active_admin/components/date_picker"; -@import "active_admin/components/popovers"; -@import "active_admin/components/tables"; -@import "active_admin/components/batch_actions"; -@import "active_admin/components/blank_slates"; -@import "active_admin/components/breadcrumbs"; -@import "active_admin/components/dropdown_menu"; -@import "active_admin/components/buttons"; -@import "active_admin/components/grid"; -@import "active_admin/components/links"; -@import "active_admin/components/pagination"; -@import "active_admin/components/panels"; -@import "active_admin/components/columns"; -@import "active_admin/components/scopes"; -@import "active_admin/components/status_tags"; -@import "active_admin/components/table_tools"; -@import "active_admin/components/index_list"; -@import "active_admin/pages/logged_out"; -@import "active_admin/structure/footer"; -@import "active_admin/structure/main_structure"; -@import "active_admin/structure/title_bar"; - -body { - @include sans-family; - line-height: 150%; - font-size: 72%; - background: #fff; - margin: 0; - padding: 0; - color: $text-color; -} diff --git a/app/assets/stylesheets/active_admin/_forms.css.scss b/app/assets/stylesheets/active_admin/_forms.css.scss deleted file mode 100644 index 6bf0e206ef9..00000000000 --- a/app/assets/stylesheets/active_admin/_forms.css.scss +++ /dev/null @@ -1,281 +0,0 @@ -// -------------------------------------- Active Admin Forms -form { - /* Reset margins & Padding */ - ul, ol, li, fieldset, legend, input, textarea, select, p { margin:0; padding:0; } - ol, ul { list-style: none } - - fieldset { - border: 0; - padding: 10px 0; - margin-bottom: 20px; - - &.inputs { @include section-background; } - - legend { - width: 100%; - span { display: block; @include section-header; } - } - - ol > li { - padding: 10px; - label { - display: block; - width: 20%; - float: left; - font-size: 1.0em; - font-weight: bold; - color: $section-header-text-color; - abbr { border: none; color: #aaa; } - } - } - - ol > div.has_many { - padding: 20px 10px; - h3 { - font-size: 12px; - font-weight: bold; - } - } - - ol > li > li label { - line-height:100%; - padding-top:0; - input { - line-height:100%; - vertical-align:middle; - margin-top:-0.1em; - } - } - } - - /* Nested Fieldsets and Legends */ - fieldset > ol > li { - fieldset { - position:relative; - padding: 0; - margin-bottom: 0; - - legend { - position:absolute; - width:95%; - padding-top:0.1em; - left: 0px; - font-size: 100%; - font-weight: normal; - span { position:absolute; } - &.label label { position:absolute; } - } - - ol { - float:left; - width:74%; - margin:0; - padding:0 0 0 20%; - - li { - padding:0; - border:0; - } - } - } - } - - /* Text Fields */ - input[type=text], input[type=password], input[type=email], input[type=number], input[type=url], input[type=tel], textarea { - width: 76%; - border: 1px solid #c9d0d6; - @include rounded; - font-size: 0.95em; - @include sans-family; - outline: none; - padding: 8px $text-input-horizontal-padding 7px; - - &:focus { - border: 1px solid #99a2aa; - @include shadow(0,0,4px,#99a2aa); - } - } - - fieldset > ol > li { - - /* Hints */ - p.inline-hints { - font-size: 0.95em; - font-style: italic; - color:#666; - margin: 0.5em 0 0 20%; - } - - /* Date and Time Fields */ - &.date, &.time, &.datetime, &.date_select { - fieldset ol li { - float:left; width:auto; margin:0 0.5em 0 0; - label { display: none; } - input { display:inline; margin:0; padding:0; } - } - } - - /* Check Boxes or Radio fields */ - &.check_boxes, &.radio { - fieldset ol { - margin-bottom:-0.6em; - li { - margin:0.1em 0 0.5em 0; - label { - float:none; - width:100%; - input { margin-right:0.2em; } - } - } - } - } - - /* Boolean Field */ - &.boolean { - height: 1.1em; - label { - width: 80%; - padding-left:20%; - padding-right: 10px; - text-transform: none !important; - font-weight: normal; - input { margin:0 0.5em 0 0.2em; } - } - } - - /* Errors */ - p.inline-errors { - color: $error-color; - font-weight: bold; - margin:0.3em 0 0 20%; - } - ul.errors { - color: $error-color; - margin:0.5em 0 0 20%; - list-style:square; - li { padding:0; border:none; display:list-item; } - } - - &.error { - input[type=text], input[type=password], input[type=email], input[type=url], input[type=tel], textarea { - border: 1px solid $error-color; - } - } - } - - /* semantic_errors */ - ul.errors { - background: lighten($error-color, 60%); - @include rounded(4px); - color: $error-color; - font-weight: bold; - margin-bottom: 10px; - padding: 10px; - list-style:square; - li { margin-left:15px; padding:0; border:none; display:list-item; } - } - - /* Buttons */ - - input[type=submit] { - @include dark-button; - cursor: pointer; - - } - - .buttons, .actions { - margin-top: 15px; - input[type=submit] { margin-right: 10px; } - } - - fieldset.buttons li, fieldset.actions li { - float:left; - padding: 0; - - &.cancel { - a { - @include light-button; @include icon(#777, 9px); - } - } - } - -} - -// -------------------------------------- Sidebar Forms - -$sidebar-inner-content-width: $sidebar-width - ($section-padding * 2); - -.sidebar_section { - - label { - display: block; - text-transform: uppercase; - color: $section-header-text-color; - font-size: 0.9em; - font-weight: bold; - } - - select { - width: $sidebar-inner-content-width; - } - - input[type=text], input[type=password], input[type=email], input[type=url], input[type=tel], textarea { - width: $sidebar-inner-content-width - ($text-input-horizontal-padding * 2); - } - -} - -// -------------------------------------- Filter Forms - -$filter-field-seperator-width: 12px; - -$side-by-side-filter-input-width: ($sidebar-inner-content-width / 2) - ($text-input-horizontal-padding * 2) - $filter-field-seperator-width; -$side-by-side-filter-select-width: ($sidebar-inner-content-width / 2) - $filter-field-seperator-width; - -$date-range-filter-input-right-padding: 27px; -$date-range-filter-input-horizontal-padding: $date-range-filter-input-right-padding + $text-input-horizontal-padding; -$date-range-filter-input-width: ($sidebar-inner-content-width / 2) - $filter-field-seperator-width - $date-range-filter-input-horizontal-padding; - -form.filter_form { - .filter_form_field { - margin-bottom: 10px; - clear: both; - - &.select_and_search { - input[type=text] { - margin-left: $filter-field-seperator-width + 4; - width: $side-by-side-filter-input-width; - } - select { - width: $side-by-side-filter-select-width; - } - } - - &.filter_check_boxes { - label { margin-bottom: 3px; } - .check_boxes_wrapper label { - font-weight: normal; - margin-bottom: 3px; - text-transform: none; - font-size: 1.0em; - input { vertical-align: baseline; } - } - } - - &.filter_date_range { - .seperator { - display: inline-block; - text-align: center; - width: $filter-field-seperator-width; - } - - input[type=text] { - background: #fff url(active_admin_image_path('datepicker/datepicker-input-icon.png')) no-repeat 100% 7px; - padding-right: $date-range-filter-input-right-padding; - width: $date-range-filter-input-width; - } - } - } - a.clear_filters_btn { @include light-button; } -} - diff --git a/app/assets/stylesheets/active_admin/_header.css.scss b/app/assets/stylesheets/active_admin/_header.css.scss deleted file mode 100644 index b0fca081b4e..00000000000 --- a/app/assets/stylesheets/active_admin/_header.css.scss +++ /dev/null @@ -1,152 +0,0 @@ -// ----------------------------------- Header -#header { - @include primary-gradient; - @include shadow; - @include text-shadow(#000); - display: table; - height: 20px; - width: 100%; - overflow: visible; - position: inherit; - padding: 5px 0; - z-index: 900; - - h1 { - display: table-cell; - vertical-align: middle; - white-space: nowrap; - color: #cdcdcd; - margin-right: 20px; - margin-bottom: 0px; - padding: 3px $horizontal-page-margin 0 $horizontal-page-margin; - font-size: 1.3em; - font-weight: normal; - line-height: 1.2; - - a { - text-decoration: none; - - &:hover { - color: #fff; - } - } - - img { - position: relative; - top: -2px; - } - } - - a, a:link { color: #cdcdcd; } - - .header-item { - top: 2px; - position: relative; - height: 20px - } - - ul.tabs { - display: table-cell; - vertical-align: middle; - height: 100%; - margin: 0; - padding: 0; - - & > li { - display: inline-block; - margin-right: 4px; - margin-top: 5px; - margin-bottom: 5px; - font-size: 1.0em; - position: relative; - - a { - text-decoration: none; - padding: 6px 10px 4px 10px; - position: relative; - @include rounded(10px); - } - - &.current > a { - background: $current-menu-item-background; - color: #fff; - } - - &.has_nested > a { - background: url(active_admin_image_path('nested_menu_arrow.gif')) no-repeat 89% 50%; - padding-right: 20px; - z-index: 1050; - } - - &.has_nested.current > a { - background: $current-menu-item-background url(active_admin_image_path('nested_menu_arrow_dark.gif')) no-repeat 89% 50%; - padding-right: 20px; - } - - &:hover > a { - background: $hover-menu-item-background; - color: #fff; - } - - &.has_nested:hover > a { - @include rounded-top(10px); - border-bottom: 5px solid $hover-menu-item-background; - background: $hover-menu-item-background url(active_admin_image_path('nested_menu_arrow_dark.gif')) no-repeat 89% 50%; - } - - - /* Hover on li, display the ul */ - &:hover ul { display: block;} - /* Drop down menus */ - ul { - background: $hover-menu-item-background; - @include rounded-all(0,10px,10px,10px); - @include shadow(0, 1px, 3px, #444); - position: absolute; - width: 175px; - margin-top: 5px; - float: left; - display: none; - padding: 3px 0px 5px 0; - list-style: none; - z-index: 1010; - - li { - margin: 0px; - a { - background: none; - display: block; - &:hover { color: #fff; background: none; } - } - - &.current { - a { @include rounded(0) } - } - } - } - } - - } - - #tabs { - width: 100%; - } - - #utility_nav { - color: #aaa; - display: table-cell; - white-space: nowrap; - margin: 0; - padding: 0; - padding-right: 26px; - text-align: right; - - a { text-decoration: none; } - a:hover { color: #fff; } - - li { - display:inline; - } - } - -} diff --git a/app/assets/stylesheets/active_admin/_mixins.css.scss b/app/assets/stylesheets/active_admin/_mixins.css.scss deleted file mode 100644 index a82c92adf52..00000000000 --- a/app/assets/stylesheets/active_admin/_mixins.css.scss +++ /dev/null @@ -1 +0,0 @@ -@import "active_admin/mixins/all"; diff --git a/app/assets/stylesheets/active_admin/_typography.css.scss b/app/assets/stylesheets/active_admin/_typography.css.scss deleted file mode 100644 index ddb369f37fb..00000000000 --- a/app/assets/stylesheets/active_admin/_typography.css.scss +++ /dev/null @@ -1,100 +0,0 @@ -// Adapted from Blueprint CSS Framework -// -// Copyright (c) 2007 - 2010 blueprintcss.org -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. - -// Default font settings. The font-size percentage is of 16px. (0.75 * 16px = 12px) */ -html { font-size:100.01%; } -body { font-size: 75%; font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; } - -// Headings -h1,h2,h3,h4,h5,h6 { - font-weight: normal; - color: $primary-color; - img { margin: 0; } -} - -h1 { font-size: 3em; line-height: 1; margin-bottom: 0.5em; } -h2 { font-size: 2em; margin-bottom: 0.75em; } -h3 { font-size: 1.5em; line-height: 1; margin-bottom: 1em; } -h4 { font-size: 1.2em; line-height: 1.25; margin-bottom: 1.25em; } -h5 { font-size: 1em; font-weight: bold; margin-bottom: 1.5em; } -h6 { font-size: 1em; font-weight: bold; } - - -p { - margin: 0 0 1.5em; - - .left { margin: 1.5em 1.5em 1.5em 0; padding: 0; } - .right { margin: 1.5em 0 1.5em 1.5em; padding: 0; } -} - -.left { float: left !important; } -.right { float: right !important; } - -blockquote { margin: 1.5em; color: #666; font-style: italic; } -strong,dfn { font-weight: bold; } -em,dfn { font-style: italic; } -sup, sub { line-height: 0; } - -abbr, -acronym { border-bottom: 1px dotted #666; } -address { margin: 0 0 1.5em; font-style: italic; } -del { color:#666; } - -pre { margin: 1.5em 0; white-space: pre; } -pre,code,tt { font: 1em 'andale mono', 'lucida console', monospace; line-height: 1.5; } - -// Lists -li ul, -li ol { margin: 0; } -ul, ol { margin: 0 1.5em 1.5em 0; padding-left: 1.5em; } - -ul { list-style-type: disc; } -ol { list-style-type: decimal; } - -dl { margin: 0 0 1.5em 0; } -dl dt { font-weight: bold; } -dd { margin-left: 1.5em;} - -// Tables -table { margin-bottom: 1.4em; width:100%; } -th { font-weight: bold; } -thead th { background: #c3d9ff; } -th,td,caption { padding: 4px 10px 4px 5px; } - -// Helper Classes -.small { font-size: .8em; margin-bottom: 1.875em; line-height: 1.875em; } -.large { font-size: 1.2em; line-height: 2.5em; margin-bottom: 1.25em; } -.hide { display: none; } - -.quiet { color: #666; } -.loud { color: #000; } -.highlight { background:#ff0; } -.added { background:#060; color: #fff; } -.removed { background:#900; color: #fff; } - -.first { margin-left:0; padding-left:0; } -.last { margin-right:0; padding-right:0; } -.top { margin-top:0; padding-top:0; } -.bottom { margin-bottom:0; padding-bottom:0; } diff --git a/app/assets/stylesheets/active_admin/components/_batch_actions.css.scss b/app/assets/stylesheets/active_admin/components/_batch_actions.css.scss deleted file mode 100644 index 21daea22546..00000000000 --- a/app/assets/stylesheets/active_admin/components/_batch_actions.css.scss +++ /dev/null @@ -1,11 +0,0 @@ -#collection_selection_toggle_panel { - @include clearfix; - >.resource_selection_toggle_cell { - float:left; - } - #collection_selection_toggle_explaination { - float:left; - margin-left:5px; - font-style:italic; - } -} diff --git a/app/assets/stylesheets/active_admin/components/_blank_slates.scss b/app/assets/stylesheets/active_admin/components/_blank_slates.scss deleted file mode 100644 index 80052e33dc1..00000000000 --- a/app/assets/stylesheets/active_admin/components/_blank_slates.scss +++ /dev/null @@ -1,31 +0,0 @@ -.blank_slate_container { - clear: both; - text-align: center; - - .blank_slate { - @include rounded; - -webkit-font-smoothing: antialiased; - border: 1px dashed #DADADA; - color: #AAA; - display: inline-block; - font-size: 1.2em; - font-weight: bold; - padding: 14px 25px; - text-align: center; - - small { - display: block; - font-size: 0.9em; - font-weight: normal; - } - } -} - -.admin_dashboard .blank_slate_container .blank_slate { - margin-top: 40px; - margin-bottom: 40px; -} - -.with_sidebar .blank_slate_container .blank_slate { - margin-top: 80px; -} diff --git a/app/assets/stylesheets/active_admin/components/_breadcrumbs.scss b/app/assets/stylesheets/active_admin/components/_breadcrumbs.scss deleted file mode 100644 index 164053fff82..00000000000 --- a/app/assets/stylesheets/active_admin/components/_breadcrumbs.scss +++ /dev/null @@ -1,20 +0,0 @@ -.breadcrumb { - display: block; - font-size: 0.9em; - font-weight: normal; - line-height: 1.0em; - margin-bottom: 12px; - text-transform: uppercase; - - a, a:link, a:visited, a:active { - color: #8a949e ; - text-decoration: none; - } - - a:hover { text-decoration: underline; } - - .breadcrumb_sep { - margin: 0 2px; - color: #aab2ba; - } -} diff --git a/app/assets/stylesheets/active_admin/components/_buttons.scss b/app/assets/stylesheets/active_admin/components/_buttons.scss deleted file mode 100644 index f7e416465fe..00000000000 --- a/app/assets/stylesheets/active_admin/components/_buttons.scss +++ /dev/null @@ -1,11 +0,0 @@ -td, p { - @include icon(#B3BCC1, 0.8em); - span.icon { margin: 0 3px; } -} - -a.member_link { - margin-right: 7px; - white-space: nowrap; -} - -a.button, a:link.button, a:visited.button, input[type=submit] { @include dark-button; } diff --git a/app/assets/stylesheets/active_admin/components/_columns.scss b/app/assets/stylesheets/active_admin/components/_columns.scss deleted file mode 100644 index 3587763ddf0..00000000000 --- a/app/assets/stylesheets/active_admin/components/_columns.scss +++ /dev/null @@ -1,3 +0,0 @@ -.columns { - margin-bottom: 10px; -} diff --git a/app/assets/stylesheets/active_admin/components/_comments.css.scss b/app/assets/stylesheets/active_admin/components/_comments.css.scss deleted file mode 100644 index f8862670e30..00000000000 --- a/app/assets/stylesheets/active_admin/components/_comments.css.scss +++ /dev/null @@ -1,41 +0,0 @@ -// -------------------------------------- Admin Notes -.comments { - - .active_admin_comment { - clear: both; - margin-top: 10px; - margin-bottom: 40px; - max-width: 700px; - - .active_admin_comment_meta { - width: 130px; - float: left; - overflow: hidden; - font-size: 0.9em; - color: lighten($primary-color, 10%); - .active_admin_comment_author { - font-size: 1.2em; - font-weight: bold; - margin: 0; - color: $primary-color; - } - } - .active_admin_comment_body { - margin-left: 150px; - } - } - form.active_admin_comment { - margin: 0; - padding: 0; - margin-left: 150px; - - fieldset.inputs { - margin: 0; - padding: 0; - background: none; - @include no-shadow; - } - li { padding: 0; } - fieldset.buttons { padding: 0; margin-top: 5px;} - } -} diff --git a/app/assets/stylesheets/active_admin/components/_date_picker.css.scss b/app/assets/stylesheets/active_admin/components/_date_picker.css.scss deleted file mode 100644 index 7d47ee8cd2d..00000000000 --- a/app/assets/stylesheets/active_admin/components/_date_picker.css.scss +++ /dev/null @@ -1,123 +0,0 @@ -// -------------------------------------- Date Picker -.ui-datepicker { - background: #fff; - -webkit-background-clip: padding-box; - -moz-background-clip: padding-box; - background-clip: padding-box; - color: #fff; - display: none; - margin-top: 2px; - padding: 0; - text-align: center; - width: 160px; - - background: url(active_admin_image_path('datepicker/datepicker-nipple.png')) no-repeat 0 -40px; - - a { - text-decoration: none; - &:hover { - cursor: pointer; - } - } - - .ui-datepicker-header { - background: url(active_admin_image_path('datepicker/datepicker-header-bg.png')) no-repeat 0px 0px; - height: 12px; - padding: 16px 7px 8px; - position: relative; - z-index: 2000; - - .ui-datepicker-title { - @include text-shadow(#000); - color: #fff; - display: block; - font-size: 1.1em; - font-weight: bold; - line-height: 0.8em; - text-align: center; - } - - a { - color: #fff; - display: block; - height: 19px; - margin-top: -4px; - width: 20px; - - &.ui-datepicker-prev { - float: left; - background: url(active_admin_image_path('datepicker/datepicker-prev-link-icon.png')) no-repeat 2px 5px; - } - &.ui-datepicker-next { - float: right; - background: url(active_admin_image_path('datepicker/datepicker-next-link-icon.png')) no-repeat 12px 5px; - } - &:active { - margin-top: -3px; - height: 18px; - } - - span { - display: none; - } - } - } - - table.ui-datepicker-calendar { - @include rounded-bottom; - @include shadow(0,1px,6px,rgba(0,0,0,0.26)); - background-color: #f4f4f4; - border: solid 1px #63686e; - left: 2px; - margin-bottom: 0px; - position: relative; - top: -2px; - width: 156px; - - td, th { - padding: 0px; - text-align: center; - } - - thead th { - background-color: #dbdddf; - color: #333333; - font-weight: normal; - font-size: 0.8em; - padding-top: 1px; - } - - tbody { - color: #666666; - - td { - border: none; - height: 24px; - width: 22px; - - a { - @include rounded; - color: #666666; - font-weight: bold; - font-size: 0.85em; - padding: 4px; - - &.ui-state-active { - background-color: #5a5f64; - color: #fff; - &.ui-state-hover { - background-color: #5a5f64; - color: #fff; - } - } - &.ui-state-hover { - background-color: #eceef0; - } - &.ui-state-highlight { - background-color: #dbdddf; - } - } - } - } - } -} diff --git a/app/assets/stylesheets/active_admin/components/_dropdown_menu.scss b/app/assets/stylesheets/active_admin/components/_dropdown_menu.scss deleted file mode 100644 index 5cbb15c2a91..00000000000 --- a/app/assets/stylesheets/active_admin/components/_dropdown_menu.scss +++ /dev/null @@ -1,151 +0,0 @@ -.dropdown_menu { - display: inline; - - .dropdown_menu_button { - @include light-button; - position: relative; - padding-right: 22px !important; - cursor: pointer; - - &:before { - content: ' '; - position: absolute; - width: 0; - height: 0; - border-width: 3px 3px 0; - border-style: solid; - border-color: #FFF transparent; - right: 12px; - top: 45%; - } - - &:after { - content: ' '; - position: absolute; - width: 0; - height: 0; - border-width: 3px 3px 0; - border-style: solid; - border-color: #777 transparent; - right: 12px; - top: 45%; - } - } - - .dropdown_menu_nipple { - - // The nipple's border - content: ""; - position: absolute; - top: -6px; - display: block; - width: 0; - height: 0; - border-width: 0 6px 6px; - border-style: solid; - border-color: darken($primary-color, 4%) transparent; - z-index: 100; - - // The nipple's inner shadow - - &:before { - content: ' '; - position: absolute; - width: 0; - height: 0; - border-width: 0 5px 5px; - border-style: solid; - border-color: lighten($primary-color, 15%) transparent; - left: -5px; - top: 1px; - } - - // The nipple's background color - - &:after { - content: ' '; - position: absolute; - width: 0; - height: 0; - border-width: 0 5px 5px; - border-style: solid; - border-color: lighten($primary-color, 4%) transparent; - left: -5px; - top: 2px; - } - } - - .dropdown_menu_list_wrapper { - display: inline-block; - position: absolute; - background-color: white; - padding: 2px; - box-shadow: rgba(0,0,0,0.4) 0 1px 3px, lighten($primary-color, 15%) 0px 1px 0px 0px inset; - background-color: $primary-color; - @include gradient(lighten($primary-color, 4%), darken($primary-color, 5%)); - border: solid 1px darken($primary-color, 10%); - border-top-color: darken($primary-color, 4%); - border-bottom-color: darken($primary-color, 17%); - - border-radius: 4px; - - .dropdown_menu_list { - display: block; - background-color: #FFF; - border: solid 1px darken($primary-color, 10%); - box-shadow: lighten($primary-color, 5%) 0px 1px 0px 0px; - border-radius: 3px; - margin: 0; - overflow: hidden; - padding: 8px; - - list-style-type: none; - padding: 0; - - li { - display: block; - border-bottom: solid 1px #ebebeb; - @include box-sizing(border-box); - - a { - display: block; - @include box-sizing(padding-box); - font-size: 0.95em; - font-weight: bold; - padding: 7px 16px 5px; - text-decoration: none; - text-align: center; - -webkit-font-smoothing: antialiased; - - &:hover { - @include highlight-gradient; - @include text-shadow(#5a83aa); - color: #FFF; - } - - &:active { - @include reverse-highlight-gradient; - color: #FFF; - } - - } - - &:first-child { - a { - border-top-left-radius: 2px; - border-top-right-radius: 2px; - } - - } - - &:last-child { - a { - border-bottom-left-radius: 2px; - border-bottom-right-radius: 2px; - } - border: none; - } - } - } - } -} diff --git a/app/assets/stylesheets/active_admin/components/_flash_messages.css.scss b/app/assets/stylesheets/active_admin/components/_flash_messages.css.scss deleted file mode 100644 index e1d1ea20d7d..00000000000 --- a/app/assets/stylesheets/active_admin/components/_flash_messages.css.scss +++ /dev/null @@ -1,39 +0,0 @@ -body.logged_in { - .flash { - @include gradient(#f7f1d3, #f5edc5); - @include text-shadow(#fafafa); - border-bottom: 1px solid #eee098; - color: #cb9810; - font-weight: bold; - font-size: 1.1em; - line-height: 1.0em; - margin-bottom: 10px; - padding: 13px 30px 11px; - position: relative; - top: -15px; - - &.flash_notice { - @include gradient(#dce9dd, #ccdfcd); - border-bottom: 1px solid #adcbaf; - color: #416347; - } - &.flash_error { - @include gradient(#f5e4e4, #f1dcdc); - border-bottom: 1px solid #e0c2c0; - color: #b33c33; - } - } -} - -body.logged_out { - .flash { - @include no-shadow; - @include text-shadow(#fff); - background: none; - color: #666; - font-weight: bold; - line-height: 1.0em; - margin-bottom: 10px; - padding: 0; - } -} diff --git a/app/assets/stylesheets/active_admin/components/_grid.scss b/app/assets/stylesheets/active_admin/components/_grid.scss deleted file mode 100644 index 7a8efbc551b..00000000000 --- a/app/assets/stylesheets/active_admin/components/_grid.scss +++ /dev/null @@ -1,9 +0,0 @@ -// -------------------------------------- Index as Grid -table.index_grid td { border: none; background: none; padding: 0 20px 20px 0; margin: 0;} - -// -------------------------------------- Columns -.columns { - clear: both; - padding: 0; - .column { float: left; } -} diff --git a/app/assets/stylesheets/active_admin/components/_index_list.scss b/app/assets/stylesheets/active_admin/components/_index_list.scss deleted file mode 100644 index c81cc6998a5..00000000000 --- a/app/assets/stylesheets/active_admin/components/_index_list.scss +++ /dev/null @@ -1,12 +0,0 @@ -.indexes { - float: right; - - li { - .count { - color: #8e979e; - font-weight: normal; - font-size: 0.9em; - line-height: 10px; - } - } -} diff --git a/app/assets/stylesheets/active_admin/components/_links.scss b/app/assets/stylesheets/active_admin/components/_links.scss deleted file mode 100644 index 0e6a62c2d24..00000000000 --- a/app/assets/stylesheets/active_admin/components/_links.scss +++ /dev/null @@ -1,5 +0,0 @@ -a, a:link, a:visited { - color: $link-color; - text-decoration: underline; -} -a:hover { text-decoration: none; } diff --git a/app/assets/stylesheets/active_admin/components/_pagination.scss b/app/assets/stylesheets/active_admin/components/_pagination.scss deleted file mode 100644 index f3fede99ef8..00000000000 --- a/app/assets/stylesheets/active_admin/components/_pagination.scss +++ /dev/null @@ -1,34 +0,0 @@ -.paginated_collection_contents { - clear: both; -} - -.pagination { - float: right; - font-size: 0.9em; - margin-left: 10px; - - a { - @include light-button; - } - - span.page.current { - @include default-button; - } - - a, span.page.current { - @include rounded(0px); - margin-right: 4px; - padding: 2px 5px; - } -} - -.pagination_information { - float: right; - margin-bottom: 5px; - color: #b3bcc1; - b { color: #5c6469; } -} - -.download_links { - float: left; -} diff --git a/app/assets/stylesheets/active_admin/components/_panels.scss b/app/assets/stylesheets/active_admin/components/_panels.scss deleted file mode 100644 index dc28c04a829..00000000000 --- a/app/assets/stylesheets/active_admin/components/_panels.scss +++ /dev/null @@ -1,6 +0,0 @@ -// ----------------------------------- Helper class to apply to elements to make them sections -.section, .panel{ @include section; } - -// ----------------------------------- Sidebar Sections - -.sidebar_section { @include section; } diff --git a/app/assets/stylesheets/active_admin/components/_popovers.css.scss b/app/assets/stylesheets/active_admin/components/_popovers.css.scss deleted file mode 100644 index 290a4b5a26a..00000000000 --- a/app/assets/stylesheets/active_admin/components/_popovers.css.scss +++ /dev/null @@ -1,123 +0,0 @@ -.popover { - display: inline-block; - position: absolute; - background-color: white; - padding: 2px; - box-shadow: rgba(0,0,0,0.4) 0 1px 3px, lighten($primary-color, 15%) 0px 1px 0px 0px inset; - background-color: $primary-color; - @include gradient(lighten($primary-color, 4%), darken($primary-color, 5%)); - border: solid 1px darken($primary-color, 10%); - border-top-color: darken($primary-color, 4%); - border-bottom-color: darken($primary-color, 17%); - - border-radius: 4px; - - .popover_nipple{ - - // The nipple's border - content: ""; - position: absolute; - top: -6px; - display: block; - width: 0; - height: 0; - border-width: 0 6px 6px; - border-style: solid; - border-color: darken($primary-color, 4%) transparent; - z-index: 100; - - // The nipple's inner shadow - - &:before { - content: ' '; - position: absolute; - width: 0; - height: 0; - border-width: 0 5px 5px; - border-style: solid; - border-color: lighten($primary-color, 15%) transparent; - left: -5px; - top: 1px; - } - - // The nipple's background color - - &:after { - content: ' '; - position: absolute; - width: 0; - height: 0; - border-width: 0 5px 5px; - border-style: solid; - border-color: lighten($primary-color, 4%) transparent; - left: -5px; - top: 2px; - } - } - - .popover_contents { - display: block; - background-color: #FFF; - border: solid 1px darken($primary-color, 10%); - box-shadow: lighten($primary-color, 5%) 0px 1px 0px 0px; - border-radius: 3px; - margin: 0; - overflow: hidden; - padding: 8px; - } - -} - -//-------------------------------------- Popover with action list - -.popover { - ul.popover_contents { - list-style-type: none; - padding: 0; - - li { - display: block; - border-bottom: solid 1px #ebebeb; - @include box-sizing(border-box); - - a { - display: block; - @include box-sizing(padding-box); - font-size: 0.95em; - font-weight: bold; - padding: 7px 16px 5px; - text-decoration: none; - text-align: center; - -webkit-font-smoothing: antialiased; - - &:hover { - @include highlight-gradient; - @include text-shadow(#5a83aa); - color: #FFF; - } - - &:active { - @include reverse-highlight-gradient; - color: #FFF; - } - - } - - &:first-child { - a { - border-top-left-radius: 2px; - border-top-right-radius: 2px; - } - - } - - &:last-child { - a { - border-bottom-left-radius: 2px; - border-bottom-right-radius: 2px; - } - border: none; - } - } - } -} diff --git a/app/assets/stylesheets/active_admin/components/_scopes.scss b/app/assets/stylesheets/active_admin/components/_scopes.scss deleted file mode 100644 index 1164fc2f612..00000000000 --- a/app/assets/stylesheets/active_admin/components/_scopes.scss +++ /dev/null @@ -1,10 +0,0 @@ -.scopes { - li { - .count { - color: #8e979e; - font-weight: normal; - font-size: 0.9em; - line-height: 10px; - } - } -} diff --git a/app/assets/stylesheets/active_admin/components/_status_tags.scss b/app/assets/stylesheets/active_admin/components/_status_tags.scss deleted file mode 100644 index bd82c3a20c1..00000000000 --- a/app/assets/stylesheets/active_admin/components/_status_tags.scss +++ /dev/null @@ -1,12 +0,0 @@ -.status_tag { - background: darken($secondary-color, 15%); - color: #fff; - text-transform: uppercase; - letter-spacing: 0.15em; - padding: 3px 5px 2px 5px; - font-size: 0.8em; - - &.ok, &.published, &.complete, &.completed, &.green { background: #8daa92; } - &.warn, &.warning, &.orange { background: #e29b20; } - &.error, &.errored, &.red { background: #d45f53; } -} diff --git a/app/assets/stylesheets/active_admin/components/_table_tools.css.scss b/app/assets/stylesheets/active_admin/components/_table_tools.css.scss deleted file mode 100644 index 0e6e017a505..00000000000 --- a/app/assets/stylesheets/active_admin/components/_table_tools.css.scss +++ /dev/null @@ -1,95 +0,0 @@ -.table_tools { - display: block; - margin-bottom: 16px; - width: 100%; - height: 20px; -} - -.table_tools { - .table_tools_button + .table_tools_button { - margin-left: 6px; - } - - .table_tools_button + .table_tools_segmented_control { - margin-left: 6px; - } - - .table_tools_segmented_control + .table_tools_button { - margin-left: 6px; - } - -} - - -a.table_tools_button, .table_tools .dropdown_menu_button { - @include light-button; - @include gradient(#FFFFFF, #F0F0F0); - @include border-colors(#d9d9d9, #d0d0d0, #c5c5c5); - font-size: 0.9em; - text-decoration: none; - padding: 4px 14px 4px; - position: relative; - -webkit-transition: all 0.08s ease-in; - - &:not(.disabled) { - &:hover { - @include gradient(#FFFFFF, #F6F6F6); - } - - &:active { - @include border-colors(#d7d7d7, #c8c8c8, #c3c3c3); - box-shadow: 0 1px 1px 0 rgba(0,0,0,0.17) inset; - @include gradient(#FFFFFF, #E8E8E8); - } - } -} - -.table_tools_segmented_control { - display: inline-block; - list-style-type: none; - margin: 0; - padding: 0; - - li { - display: inline-block; - list-style-image: none; - list-style-type: none; - margin-left: -7px; - white-space: nowrap; - - a { - border-radius: 0px; - } - - &:first-child { - margin-left: 0; - - a { - border-top-left-radius: 12px; - border-bottom-left-radius: 12px; - } - } - - &:last-child { - a { - border-top-right-radius: 12px; - border-bottom-right-radius: 12px; - } - } - - &.selected { - - a { - @include gradient(#F0F0F0, #FDFDFD); - box-shadow: 0 1px 1px 0 rgba(0,0,0,0.1) inset; - cursor: default; - - &:hover { - @include gradient(#F0F0F0, #FDFDFD); - } - } - } - - } - -} diff --git a/app/assets/stylesheets/active_admin/components/_tables.css.scss b/app/assets/stylesheets/active_admin/components/_tables.css.scss deleted file mode 100644 index eed959b37da..00000000000 --- a/app/assets/stylesheets/active_admin/components/_tables.css.scss +++ /dev/null @@ -1,108 +0,0 @@ -// ----------------------------------- Tables - -table tr { - td { - vertical-align: top; - } -} - -// --------- Index Tables - -table.index_table { - width: 100%; - margin-bottom: 10px; - border: 0; - border-spacing: 0; - - th { - @include section-header; - border-right: none; - text-align: left; - padding-left: $cell-horizontal-padding; - padding-right: $cell-horizontal-padding; - - a, a:link, a:visited { - color: $section-header-text-color; - text-decoration: none; - display: block; - white-space: nowrap; - -webkit-font-smoothing: antialiased; - } - - &.sortable a { - background: url(active_admin_image_path('orderable.png')) no-repeat 0 4px; padding-left: 13px; - } - - &.sorted-asc a { background-position: 0 -27px; } - &.sorted-desc a { background-position: 0 -56px;} - - &.sorted-asc, &.sorted-desc { - @include gradient(darken($secondary-gradient-start, 5%), darken($secondary-gradient-stop, 5%)); - } - - &:last-child { - border-right: solid 1px #d4d4d4; - } - - } - - tr.even td { background: $table-stripe-color; } - - tr.selected td { - background: $table-selected-color; - } - - td { - padding: 10px $cell-horizontal-padding 8px $cell-horizontal-padding; - border-bottom: 1px solid #e8e8e8; - vertical-align: top; - } -} - -// --------- Tables inside Panels - -.panel_contents table { - margin-top: 5px; - th { - padding-top: 10px; - background: none; - color: $primary-color; - @include no-shadow; - @include text-shadow; - text-transform: uppercase; - border-bottom: 1px solid #ccc; - } - tr.odd td { background: darken($table-stripe-color, 3%); } - tr.even td { background: $table-stripe-color; } -} - -// -------------------------------------- Resource Attributes Table -.attributes_table { overflow: hidden; } - -.attributes_table table { - th, td { - padding: 8px $cell-horizontal-padding 6px $cell-horizontal-padding; - vertical-align: top; - border-bottom: 1px solid #e8e8e8; - } - th { - @include no-shadow; - @include no-gradient; - width: 150px; - font-size: 0.9em; - padding-left: 0; - text-transform: uppercase; - color: $primary-color; - @include text-shadow; - } - td { - .empty { - color: #bbb; - font-size: 0.8em; - text-transform: uppercase; - letter-spacing: 0.2em; - } - } -} - -.sidebar_section .attributes_table th { width: 50px; } diff --git a/app/assets/stylesheets/active_admin/mixins/_all.css.scss b/app/assets/stylesheets/active_admin/mixins/_all.css.scss deleted file mode 100644 index 20b64e78314..00000000000 --- a/app/assets/stylesheets/active_admin/mixins/_all.css.scss +++ /dev/null @@ -1,11 +0,0 @@ -@import "active_admin/mixins/variables"; -@import "active_admin/mixins/reset"; -@import "active_admin/mixins/gradients"; -@import "active_admin/mixins/shadows"; -@import "active_admin/mixins/icons"; -@import "active_admin/mixins/rounded"; -@import "active_admin/mixins/buttons"; -@import "active_admin/mixins/sections"; -@import "active_admin/mixins/utilities"; -@import "active_admin/mixins/typography"; -@import "bourbon"; diff --git a/app/assets/stylesheets/active_admin/mixins/_buttons.css.scss b/app/assets/stylesheets/active_admin/mixins/_buttons.css.scss deleted file mode 100644 index dc5625781ab..00000000000 --- a/app/assets/stylesheets/active_admin/mixins/_buttons.css.scss +++ /dev/null @@ -1,66 +0,0 @@ -@mixin basic-button { - @include rounded(200px); - display: inline-block; - font-weight: bold; - font-size: 1.0em; - @include sans-family; - line-height: 12px; - margin-right: 3px; - padding: 7px 16px 6px; - text-decoration: none; - -webkit-font-smoothing: antialiased; - - &.disabled { - opacity: 0.5; - cursor: default; - } - -} - -@mixin default-button { - @include basic-button; - @include gradient(lighten($primary-color, 15%), darken($primary-color, 12%)); - @include text-shadow(#000); - box-shadow: 0 1px 1px rgba(0,0,0,0.10), 0 1px 0 0px rgba(255,255,255, 0.2) inset; - border: solid 1px #484e53; - @include border-colors(#616a71, #484e53, #363b3f); - color: #efefef; - - &:not(.disabled) { - &:hover{ - @include gradient(lighten($primary-color, 18%), darken($primary-color, 9%)); - } - - &:active { - box-shadow: 0 1px 3px rgba(0,0,0,0.40) inset, 0 1px 0 0px #FFF; - @include gradient(lighten($primary-color, 8%), darken($primary-color, 17%)); - } - } -} - -@mixin light-button { - @include basic-button; - @include gradient(#FFFFFF, #E7E7E7); - box-shadow: 0 1px 1px rgba(0,0,0,0.10), 0 1px 0 0 rgba(255,255,255, 0.8) inset; - border: solid 1px #c7c7c7; - @include border-colors(#d3d3d3, #c7c7c7, #c2c2c2); - @include text-shadow; - color: $primary-color; - - &:not(.disabled) { - &:hover { - @include gradient(#FFFFFF, #F1F1F1); - } - - &:active { - box-shadow: 0 1px 2px rgba(0,0,0,0.22) inset, 0 1px 0 0px #EEE; - @include border-colors(#c2c2c2, #b9b9b9, #b7b7b7); - @include gradient(#F3F3F3, #D8D8D8); - } - } - -} - -@mixin dark-button { - @include default-button; -} diff --git a/app/assets/stylesheets/active_admin/mixins/_gradients.css.scss b/app/assets/stylesheets/active_admin/mixins/_gradients.css.scss deleted file mode 100644 index 5a10d959d9a..00000000000 --- a/app/assets/stylesheets/active_admin/mixins/_gradients.css.scss +++ /dev/null @@ -1,38 +0,0 @@ -$secondary-gradient-start: #efefef !default; -$secondary-gradient-stop: #dfe1e2 !default; - -@mixin gradient($start, $end){ - background: $start; - background: -webkit-linear-gradient(-90deg, $start, $end); - background: -moz-linear-gradient(-90deg, $start, $end); - background: linear-gradient(-90deg, $start, $end); - // IE 6 & 7 - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#{ie-hex-str($start)}', endColorstr='#{ie-hex-str($end)}'); - // IE 8 - -ms-filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#{ie-hex-str($start)}', endColorstr='#{ie-hex-str($end)}'); -} - -@mixin primary-gradient { - @include gradient(lighten($primary-color, 5%), darken($primary-color, 7%)); - border-bottom: 1px solid darken($primary-color, 11%); -} - -@mixin secondary-gradient { - @include gradient($secondary-gradient-start, $secondary-gradient-stop); -} - -@mixin highlight-gradient { - @include gradient(#75a1c2, #608cb4); -} - -@mixin reverse-highlight-gradient { - @include gradient(#608cb4, #75a1c2); -} - -@mixin no-gradient { - background: none; - // IE 6 & 7 - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - // IE 8 - -ms-filter: "progid:DXImageTransform.Microsoft.gradient(enabled=false)"; -} diff --git a/app/assets/stylesheets/active_admin/mixins/_icons.css.scss b/app/assets/stylesheets/active_admin/mixins/_icons.css.scss deleted file mode 100644 index 105486295c0..00000000000 --- a/app/assets/stylesheets/active_admin/mixins/_icons.css.scss +++ /dev/null @@ -1,21 +0,0 @@ -span.icon { vertical-align: middle; display: inline-block; } -span.icon svg { vertical-align: baseline; } - - -@mixin icon-color ($color) { - span.icon svg { - path, polygon, rect, circle { fill: $color !important; } - } -} - -@mixin icon-size ($size) { - span.icon { width: $size; height: $size; } - span.icon svg { width: $size; height: $size; } -} - -@mixin icon($color, $size) { - @include icon-color($color); - @include icon-size($size); -} - -@include icon-size(0.8em); diff --git a/app/assets/stylesheets/active_admin/mixins/_reset.css.scss b/app/assets/stylesheets/active_admin/mixins/_reset.css.scss deleted file mode 100644 index 7b89b89b278..00000000000 --- a/app/assets/stylesheets/active_admin/mixins/_reset.css.scss +++ /dev/null @@ -1,165 +0,0 @@ -// FROM The Compass Framework (compass-style.org) -// -// Copyright (c) 2009 Christopher M. Eppstein -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. No attribution is required by -// products that make use of this software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// -// Except as contained in this notice, the name(s) of the above copyright holders -// shall not be used in advertising or otherwise to promote the sale, use or other -// dealings in this Software without prior written authorization. -// -// Contributors to this project agree to grant all rights to the copyright holder -// of the primary product. Attribution is maintained in the source control history -// of the product. -// -// Based on [Eric Meyer's reset](http://meyerweb.com/eric/thoughts/2007/05/01/reset-reloaded/) -// Global reset rules. -// For more specific resets, use the reset mixins provided below -// -// *Please Note*: tables still need `cellspacing="0"` in the markup. -@mixin global-reset { - html, body, div, span, applet, object, iframe, - h1, h2, h3, h4, h5, h6, p, blockquote, pre, - a, abbr, acronym, address, big, cite, code, - del, dfn, em, font, img, ins, kbd, q, s, samp, - small, strike, strong, sub, sup, tt, var, - dl, dt, dd, ol, ul, li, - fieldset, form, label, legend, - table, caption, tbody, tfoot, thead, tr, th, td { - @include reset-box-model; - @include reset-font; } - body { - @include reset-body; } - ol, ul { - @include reset-list-style; } - table { - @include reset-table; } - caption, th, td { - @include reset-table-cell; } - q, blockquote { - @include reset-quotation; } - a img { - @include reset-image-anchor-border; } } - -// Reset all elements within some selector scope. To reset the selector itself, -// mixin the appropriate reset mixin for that element type as well. This could be -// useful if you want to style a part of your page in a dramatically different way. -// -// *Please Note*: tables still need `cellspacing="0"` in the markup. -@mixin nested-reset { - div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, - pre, a, abbr, acronym, address, code, del, dfn, em, img, - dl, dt, dd, ol, ul, li, fieldset, form, label, legend, caption, tbody, tfoot, thead, tr { - @include reset-box-model; - @include reset-font; } - table { - @include reset-table; } - caption, th, td { - @include reset-table-cell; } - q, blockquote { - @include reset-quotation; } - a img { - @include reset-image-anchor-border; } } - -// Reset the box model measurements. -@mixin reset-box-model { - margin: 0; - padding: 0; - border: 0; - outline: 0; } - -// Reset the font and vertical alignment. -@mixin reset-font { - font: { - weight: inherit; - style: inherit; - size: 100%; - family: inherit; }; - vertical-align: baseline; } - -// Resets the outline when focus. -// For accessibility you need to apply some styling in its place. -@mixin reset-focus { - outline: 0; } - -// Reset a body element. -@mixin reset-body { - line-height: 1; - color: black; - background: white; } - -// Reset the list style of an element. -@mixin reset-list-style { - list-style: none; } - -// Reset a table -@mixin reset-table { - border-collapse: separate; - border-spacing: 0; - vertical-align: middle; } - -// Reset a table cell (`th`, `td`) -@mixin reset-table-cell { - text-align: left; - font-weight: normal; - vertical-align: middle; } - -// Reset a quotation (`q`, `blockquote`) -@mixin reset-quotation { - quotes: "" ""; - &:before, &:after { - content: ""; } } - -// Resets the border. -@mixin reset-image-anchor-border { - border: none; } - -// Unrecognized elements are displayed inline. -// This reset provides a basic reset for html5 elements -// so they are rendered correctly in browsers that don't recognize them -// and reset in browsers that have default styles for them. -@mixin reset-html5 { - article, aside, canvas, details, figcaption, figure, footer, header, hgroup, menu, nav, section, summary { - @include reset-box-model; - display: block; } } - -// Resets the display of inline and block elements to their default display -// according to their tag type. Elements that have a default display that varies across -// versions of html or browser are not handled here, but this covers the 90% use case. -// Usage Example: -// -// // Turn off the display for both of these classes -// .unregistered-only, .registered-only -// display: none -// // Now turn only one of them back on depending on some other context. -// body.registered -// +reset-display(".registered-only") -// body.unregistered -// +reset-display(".unregistered-only") -@mixin reset-display($selector: "", $important: false) { - #{append-selector(elements-of-type("inline"), $selector)} { - @if $important { - display: inline !important; } - @else { - display: inline; } } - #{append-selector(elements-of-type("block"), $selector)} { - @if $important { - display: block !important; } - @else { - display: block; } } } diff --git a/app/assets/stylesheets/active_admin/mixins/_rounded.css.scss b/app/assets/stylesheets/active_admin/mixins/_rounded.css.scss deleted file mode 100644 index 32f90689b2d..00000000000 --- a/app/assets/stylesheets/active_admin/mixins/_rounded.css.scss +++ /dev/null @@ -1,43 +0,0 @@ -@mixin rounded($radius: 3px) { - -webkit-border-radius: $radius; - -moz-border-radius: $radius; - border-radius: $radius; -} - -@mixin rounded-all($top-left:3px, $top-right:3px, $bottom-right:3px, $bottom-left:3px) { - border-top-right-radius: $top-right; - -moz-border-radius-topright: $top-right; - -webkit-border-top-right-radius: $top-right; - - border-top-left-radius: $top-left; - -moz-border-radius-topleft: $top-left; - -webkit-border-top-left-radius: $top-left; - - border-bottom-right-radius: $bottom-right; - -moz-border-radius-bottomright: $bottom-right; - -webkit-border-bottom-right-radius: $bottom-right; - - border-bottom-left-radius: $bottom-left; - -moz-border-radius-bottomleft: $bottom-left; - -webkit-border-bottom-left-radius: $bottom-left; -} - -@mixin rounded-top($radius: 3px) { - @include rounded(0); - border-top-right-radius: $radius; - border-top-left-radius: $radius; - -moz-border-radius-topright: $radius; - -moz-border-radius-topleft: $radius; - -webkit-border-top-right-radius: $radius; - -webkit-border-top-left-radius: $radius; -} - -@mixin rounded-bottom($radius: 3px) { - @include rounded(0); - border-bottom-right-radius: $radius; - border-bottom-left-radius: $radius; - -moz-border-radius-bottomright: $radius; - -moz-border-radius-bottomleft: $radius; - -webkit-border-bottom-right-radius: $radius; - -webkit-border-bottom-left-radius: $radius; -} diff --git a/app/assets/stylesheets/active_admin/mixins/_sections.css.scss b/app/assets/stylesheets/active_admin/mixins/_sections.css.scss deleted file mode 100644 index 435d0fbe8a6..00000000000 --- a/app/assets/stylesheets/active_admin/mixins/_sections.css.scss +++ /dev/null @@ -1,36 +0,0 @@ -@mixin section-header { - @include secondary-gradient; - @include text-shadow; - border: solid 1px #cdcdcd; - @include border-colors(#e6e6e6, #d4d4d4, #cdcdcd); - box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 0 1px #FFF inset; - - font-size: 1.0em; - font-weight: bold; - line-height: 18px; - margin-bottom: 0.5em; - color: $section-header-text-color; - @include icon($section-header-text-color, 1.0em); - - padding: 5px 10px 3px 10px; -} - -@mixin section-background { - background: #f4f4f4; - @include rounded(4px); - @include inset-shadow(0,1px,4px, #ddd); -} - -@mixin section { - @include section-background; - margin-bottom: 20px; - - > h3 { @include section-header; } - - > div { padding: 3px $section-padding $section-padding $section-padding; } - - hr { - border: none; - border-bottom: 1px solid #E8E8E8; - } -} diff --git a/app/assets/stylesheets/active_admin/mixins/_shadows.css.scss b/app/assets/stylesheets/active_admin/mixins/_shadows.css.scss deleted file mode 100644 index 3b5699eaae7..00000000000 --- a/app/assets/stylesheets/active_admin/mixins/_shadows.css.scss +++ /dev/null @@ -1,22 +0,0 @@ -@mixin shadow($x: 0, $y: 1px, $blur: 2px, $color: rgba(0,0,0,0.37)) { - box-shadow: $x $y $blur $color; - -moz-box-shadow: $x $y $blur $color; - -webkit-box-shadow: $x $y $blur $color; -} - -@mixin no-shadow { - box-shadow: none; - -moz-box-shadow: none; - -webkit-box-shadow: none; -} - -@mixin inset-shadow($x: 0, $y: 1px, $blur: 2px, $color: #aaa) { - box-shadow: inset $x $y $blur $color; - -moz-box-shadow: inset $x $y $blur $color; - -webkit-box-shadow: inset $x $y $blur $color; -} - -@mixin text-shadow($color: #fff, $x: 0, $y: 1px, $blur: 0) { - text-shadow: $color $x $y $blur; -} - diff --git a/app/assets/stylesheets/active_admin/mixins/_typography.scss b/app/assets/stylesheets/active_admin/mixins/_typography.scss deleted file mode 100644 index ac3bbff0b67..00000000000 --- a/app/assets/stylesheets/active_admin/mixins/_typography.scss +++ /dev/null @@ -1,3 +0,0 @@ -@mixin sans-family { - font-family: Helvetica, Arial, sans-serif; -} diff --git a/app/assets/stylesheets/active_admin/mixins/_utilities.scss b/app/assets/stylesheets/active_admin/mixins/_utilities.scss deleted file mode 100644 index 0d0742feca8..00000000000 --- a/app/assets/stylesheets/active_admin/mixins/_utilities.scss +++ /dev/null @@ -1,26 +0,0 @@ -@mixin clearfix { - &:after { - visibility: hidden; - display: block; - content: ""; - clear: both; - height: 0; - } -} - -@mixin box-sizing($value: border-box) { - -moz-box-sizing: $value; - -webkit-box-sizing: $value; - -o-box-sizing: $value; - -ms-box-sizing: $value; - box-sizing: $value; -} - - -@mixin border-colors($top, $sides, $bottom) { - border-color: $sides; - border-top-color: $top; - border-right-color: $sides; - border-bottom-color: $bottom; - border-left-color: $sides; -} diff --git a/app/assets/stylesheets/active_admin/mixins/_variables.css.scss b/app/assets/stylesheets/active_admin/mixins/_variables.css.scss deleted file mode 100644 index b35f66c7a8e..00000000000 --- a/app/assets/stylesheets/active_admin/mixins/_variables.css.scss +++ /dev/null @@ -1,24 +0,0 @@ -// Variables used throughout Active Admin. -// They can be overridden by prepending your own -// to 'app/assets/stylesheets/active_admin.css.scss'. - -// Colors -$primary-color: #5E6469 !default; -$secondary-color: #f0f0f0 !default; -$text-color: #323537 !default; -$link-color: #38678b !default; -$section-header-text-color: $primary-color !default; -$current-menu-item-background: lighten($primary-color, 12%) !default; -$hover-menu-item-background: lighten($primary-color, 12%) !default; -$table-stripe-color: lighten($primary-color, 57%) !default; -$table-selected-color: #d9e4ec !default; -$error-color: #932419 !default; - -// Sizes -$horizontal-page-margin: 30px !default; -$sidebar-width: 270px !default; -$cell-padding: 5px 10px 3px 10px !default; -$cell-horizontal-padding: 12px !default; -$section-padding: 15px !default; -$text-input-horizontal-padding: 10px !default; - diff --git a/app/assets/stylesheets/active_admin/pages/_logged_out.scss b/app/assets/stylesheets/active_admin/pages/_logged_out.scss deleted file mode 100644 index cb8c3b58673..00000000000 --- a/app/assets/stylesheets/active_admin/pages/_logged_out.scss +++ /dev/null @@ -1,44 +0,0 @@ -body.logged_out { - background: #e8e9ea; - - #content_wrapper{ - width: 500px; - margin: 70px auto; - #active_admin_content { - @include shadow; - background: #fff; - padding: 13px 30px; - } - } - - h2 { - @include section-header; - @include primary-gradient; - @include text-shadow(#000); - box-shadow: 0 1px 3px rgba(0,0,0,0.3); - border: none; - color: #fff; - margin: -13px -30px 20px -30px; - } - - #login { - /* Login Form */ - form { - fieldset { - @include no-shadow; - background: none; - padding: 0; - li { padding: 10px 0; } - - input[type=text], input[type=email], input[type=password] { - width: 70%; - } - &.buttons { margin-left: 20%; } - margin-bottom: 0; - } - } - - a { float: right; margin-top: -32px; } - } - -} diff --git a/app/assets/stylesheets/active_admin/print.css.scss b/app/assets/stylesheets/active_admin/print.css.scss deleted file mode 100644 index 3908089c69e..00000000000 --- a/app/assets/stylesheets/active_admin/print.css.scss +++ /dev/null @@ -1,284 +0,0 @@ -/* Active Admin Print Stylesheet */ - -// Set colors used elsewhere -$primary-color: black; -$text-color: black; - -// Reset -@import "active_admin/mixins/reset"; -@include global-reset; - -// Partials -@import "active_admin/typography"; - -body { - font-family: Helvetica, Arial, sans-serif; - line-height: 150%; - font-size: 72%; - background: #fff; - width: 99%; - margin: 0; - padding: .5%; - color: $text-color; -} - -a { - color: $text-color; - text-decoration: none; -} - -h3 { - font-weight: bold; - margin-bottom: .5em; -} - -// Header -#header { - float: left; - - #tabs, .tabs, #utility_nav { - display: none; - } - - h1{ - font-weight: bold; - } -} - -.flashes { - display: none; -} - -#title_bar { - float: right; - - h2 { - line-height: 2em; - margin: 0; - } - - .breadcrumb, #titlebar_right { - display: none; - } -} - -// Content -#active_admin_content { - border-top: thick solid black; - clear: both; - margin-top: 2em; - padding-top: 3em; -} - -// Footer -#footer { - display: none; -} - -// Tables -.table_tools { - ul { - padding: 0; - margin: 0; - list-style-type: none; - - li { - display: none; - padding: 0; - margin-bottom: 1em; - - &.scope.selected, &.index.selected { - display: block; - - &:before { - content: "Showing "; - } - - a { - font-weight: bold; - } - - span { - display: inline-block; - font-weight: normal; - font-size: .9em; - } - } - } - - } -} - -table { - margin-bottom: 1.5em; - text-align: left; - width: 100%; - - thead { - display: table-header-group; - - th { - background: none; - border-bottom: medium solid black; - font-weight: bold; - - a{ - text-decoration: none; - } - } - } - - th, td { - padding: .5em 1em; - - .member_link { - display: none; - } - } - - td { - border-bottom: thin solid black; - } - - tr{ - page-break-inside: avoid; - } -} - -// Index -#index_footer, .pagination_information { - display: none; -} - -.index_grid { - td { - border: none; - text-align: center; - vertical-align: middle; - - img { - max-width: 1in; - } - } -} - -// Show -.panel { - border-bottom: thick solid #ccc; - margin-bottom: 3em; - padding-bottom: 2em; - page-break-inside: avoid; - - &:last-child { - border-bottom: none; - } -} - -.comments { - form { - display: none; - } - - .active_admin_comment { - border-top: thin solid black; - padding-top: 1em; - - .active_admin_comment_meta { - h4 { - font-size: 1em; - font-weight: bold; - float: left; - margin-right: .5em; - margin-bottom: 0; - } - - span { - font-size: .9em; - font-style: italic; - vertical-align: top; - } - } - - .active_admin_comment_body { - clear: both; - margin-bottom: 1em; - } - } -} - - -// Attribute Tables -.attributes_table { - border-top: medium solid black; - - th { - border-bottom: thin solid black; - vertical-align: top; - - &:after { - content: ':'; - } - } - - td { - img { - max-height: 4in; - max-width: 6in; - } - } -} - -// Sidebars -#filters_sidebar_section { - display: none; -} - -// Forms -form { - fieldset { - border-top: thick solid #ccc; - padding-top: 2em; - margin-bottom: 2em; - - &:last-child { - border-bottom: none; - } - } - - .buttons, abbr { - display: none; - } - ol { - list-style-type: none; - padding: 0; - margin: 0; - - li{ - border-top: thin solid black; - margin: 0; - padding: 1em 0; - overflow: hidden; - - &.password, &.hidden { - display: none; - } - - label { - font-weight: bold; - float: left; - width: 20%; - } - - input, textarea, select { - background: none; - border: 0; - font: Arial, Helvetica, sans-serif; - } - - input[type=file] { - display: none; - } - - } - } -} diff --git a/app/assets/stylesheets/active_admin/structure/_footer.scss b/app/assets/stylesheets/active_admin/structure/_footer.scss deleted file mode 100644 index f387d207e1d..00000000000 --- a/app/assets/stylesheets/active_admin/structure/_footer.scss +++ /dev/null @@ -1,14 +0,0 @@ -#footer { - padding: 30px 30px; - font-size: 0.8em; - clear: both; - - p { - padding-top: 10px - } -} - -// -------------------------------------- Index Footer (Under Table) -#index_footer { padding-top: 5px; text-align: right; font-size: 0.85em; } - -.index_content { clear: both; } diff --git a/app/assets/stylesheets/active_admin/structure/_main_structure.scss b/app/assets/stylesheets/active_admin/structure/_main_structure.scss deleted file mode 100644 index d54c8ce0893..00000000000 --- a/app/assets/stylesheets/active_admin/structure/_main_structure.scss +++ /dev/null @@ -1,26 +0,0 @@ -#wrapper { - display: table; - width: 100%; -} - -#active_admin_content { - margin: 0; - padding: 25px $horizontal-page-margin; - - #main_content_wrapper { - float: left; - width: 100%; - - #main_content{ - margin-right: 300px; - } - } - - &.without_sidebar #main_content_wrapper #main_content{ margin-right: 0; } - - #sidebar { - float: left; - width: $sidebar-width; - margin-left: -$sidebar-width; - } -} diff --git a/app/assets/stylesheets/active_admin/structure/_title_bar.scss b/app/assets/stylesheets/active_admin/structure/_title_bar.scss deleted file mode 100644 index 2c4d1d945e1..00000000000 --- a/app/assets/stylesheets/active_admin/structure/_title_bar.scss +++ /dev/null @@ -1,44 +0,0 @@ -#title_bar { - @include section-header; - @include clearfix; - @include box-sizing(border-box); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.37); - display: table; - border-bottom-color: #EEE; - width: 100%; - position: relative; - margin: 0; - margin-bottom: 15px; - padding: 10px $horizontal-page-margin; - z-index: 800; - - #titlebar_left, #titlebar_right { - height: 50px; - vertical-align: middle; - display: table-cell; - } - - #titlebar_right { - text-align: right; - } - - h2 { - margin: 0; - padding: 0; - font-size: 2.6em; - font-weight: bold; - } - - .action_items { - a { - @include light-button; - padding: 12px 17px 10px; - span.icon { vertical-align: bottom; margin-right: 4px;} - margin: 0px; - - &:hover{ - @include icon-color(#000); - } - } - } -} diff --git a/lib/active_admin/base_controller.rb b/app/controllers/active_admin/base_controller.rb similarity index 50% rename from lib/active_admin/base_controller.rb rename to app/controllers/active_admin/base_controller.rb index 8eb56cc087b..c2dd73ffaa0 100644 --- a/lib/active_admin/base_controller.rb +++ b/app/controllers/active_admin/base_controller.rb @@ -1,18 +1,20 @@ -require 'inherited_resources' - -require 'active_admin/base_controller/authorization' -require 'active_admin/base_controller/menu' - +# frozen_string_literal: true module ActiveAdmin # BaseController for ActiveAdmin. # It implements ActiveAdmin controllers core features. class BaseController < ::InheritedResources::Base - helper ::ActiveAdmin::ViewHelpers + helper MethodOrProcHelper + helper LayoutHelper + helper FormHelper + helper BreadcrumbHelper + helper AutoLinkHelper + helper DisplayHelper + helper IndexHelper - layout :determine_active_admin_layout + layout "active_admin" - before_filter :only_render_implemented_actions - before_filter :authenticate_active_admin_user + before_action :only_render_implemented_actions + before_action :authenticate_active_admin_user class << self # Ensure that this method is available for the DSL @@ -23,18 +25,18 @@ class << self attr_accessor :active_admin_config end - # By default Rails will render un-implemented actions when the view exists. Becuase Active + include BaseController::Authorization + include BaseController::Menu + + private + + # By default Rails will render un-implemented actions when the view exists. Because Active # Admin allows you to not render any of the actions by using the #actions method, we need # to check if they are implemented. def only_render_implemented_actions raise AbstractController::ActionNotFound unless action_methods.include?(params[:action]) end - include Menu - include Authorization - - private - # Calls the authentication method as defined in ActiveAdmin.authentication_method def authenticate_active_admin_user send(active_admin_namespace.authentication_method) if active_admin_namespace.authentication_method @@ -46,7 +48,7 @@ def current_active_admin_user helper_method :current_active_admin_user def current_active_admin_user? - !current_active_admin_user.nil? + !!current_active_admin_user end helper_method :current_active_admin_user? @@ -60,19 +62,49 @@ def active_admin_namespace end helper_method :active_admin_namespace - ACTIVE_ADMIN_ACTIONS = [:index, :show, :new, :create, :edit, :update, :destroy] - # Determine which layout to use. - # - # 1. If we're rendering a standard Active Admin action, we want layout(false) - # because these actions are subclasses of the Base page (which implements - # all the required layout code) - # 2. If we're rendering a custom action, we'll use the active_admin layout so - # that users can render any template inside Active Admin. - def determine_active_admin_layout - ACTIVE_ADMIN_ACTIONS.include?(params[:action].to_sym) ? false : 'active_admin' + def active_admin_root + controller, action = active_admin_namespace.root_to.split "#" + { controller: controller, action: action } + end + + def page_presenter + active_admin_config.get_page_presenter(params[:action].to_sym) || default_page_presenter + end + helper_method :page_presenter + + def default_page_presenter + PagePresenter.new + end + + def page_title + if page_presenter[:title] + helpers.render_or_call_method_or_proc_on(self, page_presenter[:title]) + else + default_page_title + end + end + helper_method :page_title + + def default_page_title + active_admin_config.name + end + + DEFAULT_DOWNLOAD_FORMATS = [:csv, :xml, :json] + + def build_download_formats(download_links) + download_links = instance_exec(&download_links) if download_links.is_a?(Proc) + if download_links.is_a?(Array) && !download_links.empty? + download_links + elsif download_links == false + [] + else + DEFAULT_DOWNLOAD_FORMATS + end end + helper_method :build_download_formats + ActiveSupport.run_load_hooks(:active_admin_controller, self) end end diff --git a/app/controllers/active_admin/base_controller/authorization.rb b/app/controllers/active_admin/base_controller/authorization.rb new file mode 100644 index 00000000000..f4c036f8f39 --- /dev/null +++ b/app/controllers/active_admin/base_controller/authorization.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true +module ActiveAdmin + class BaseController < ::InheritedResources::Base + module Authorization + extend ActiveSupport::Concern + + ACTIONS_DICTIONARY = { + index: ActiveAdmin::Authorization::READ, + show: ActiveAdmin::Authorization::READ, + new: ActiveAdmin::Authorization::NEW, + create: ActiveAdmin::Authorization::CREATE, + edit: ActiveAdmin::Authorization::EDIT, + update: ActiveAdmin::Authorization::UPDATE, + destroy: ActiveAdmin::Authorization::DESTROY + } + + included do + rescue_from ActiveAdmin::AccessDenied, with: :dispatch_active_admin_access_denied + + helper_method :authorized? + helper_method :authorize! + helper_method :active_admin_authorization + end + + protected + + # Authorize the action and subject. Available in the controller + # as well as all the views. + # + # @param [Symbol] action The action to check if the user has permission + # to perform on the subject. + # + # @param [any] subject The subject that the user is trying to perform + # the action on. + # + # @return [Boolean] + # + def authorized?(action, subject = nil) + active_admin_authorization.authorized?(action, subject) + end + + # Authorize the action and subject. Available in the controller + # as well as all the views. If the action is not allowed, it raises + # an ActiveAdmin::AccessDenied exception. + # + # @param [Symbol] action The action to check if the user has permission + # to perform on the subject. + # + # @param [any] subject The subject that the user is trying to perform + # the action on. + # + # @return [Boolean] True if authorized, otherwise raises + # an ActiveAdmin::AccessDenied. + def authorize!(action, subject = nil) + unless authorized? action, subject + raise ActiveAdmin::AccessDenied.new( + current_active_admin_user, + action, + subject) + end + end + + # Performs authorization on the resource using the current controller + # action as the permission action. + # + def authorize_resource!(resource) + permission = action_to_permission(params[:action]) + authorize! permission, resource + end + + # Retrieve or instantiate the authorization instance for this resource + # + # @return [ActiveAdmin::AuthorizationAdapter] + def active_admin_authorization + @active_admin_authorization ||= + active_admin_authorization_adapter.new active_admin_config, current_active_admin_user + end + + # Returns the class to be used as the authorization adapter + # + # @return [Class] + def active_admin_authorization_adapter + adapter = active_admin_namespace.authorization_adapter + if adapter.is_a? String + adapter.constantize + else + adapter + end + end + + # Converts a controller action into one of the correct Active Admin + # authorization names. Uses the ACTIONS_DICTIONARY to convert the + # action name to permission. + # + # @param [String, Symbol] action The controller action name. + # + # @return [Symbol] The permission name to use. + def action_to_permission(action) + if action && action = action.to_sym + Authorization::ACTIONS_DICTIONARY[action] || action + end + end + + def dispatch_active_admin_access_denied(exception) + instance_exec(self, exception, &active_admin_namespace.on_unauthorized_access.to_proc) + end + + def rescue_active_admin_access_denied(exception) + error = exception.message + + respond_to do |format| + format.html do + flash[:error] = error + redirect_backwards_or_to_root + end + + format.csv { render body: error, status: :unauthorized } + format.json { render json: { error: error }, status: :unauthorized } + format.xml { render xml: "#{error}", status: :unauthorized } + end + end + + def redirect_backwards_or_to_root + redirect_back fallback_location: active_admin_root + end + + end + end +end diff --git a/app/controllers/active_admin/base_controller/menu.rb b/app/controllers/active_admin/base_controller/menu.rb new file mode 100644 index 00000000000..afb9e8306b2 --- /dev/null +++ b/app/controllers/active_admin/base_controller/menu.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true +module ActiveAdmin + class BaseController < ::InheritedResources::Base + module Menu + extend ActiveSupport::Concern + + included do + before_action :set_current_menu_item + + helper_method :current_menu + helper_method :current_menu_item? + end + + protected + + def current_menu + active_admin_config.navigation_menu + end + + def current_menu_item?(item, children: true) + item.current?(@current_menu_item, children: children) + end + + def set_current_menu_item + @current_menu_item = if current_menu && active_admin_config.belongs_to? && parent? + parent_item = active_admin_config.belongs_to_config.target.menu_item + if current_menu.include? parent_item + parent_item + else + active_admin_config.menu_item + end + else + active_admin_config.menu_item + end + end + + end + end +end diff --git a/lib/active_admin/page_controller.rb b/app/controllers/active_admin/page_controller.rb similarity index 74% rename from lib/active_admin/page_controller.rb rename to app/controllers/active_admin/page_controller.rb index 4048d313838..e9979e53b43 100644 --- a/lib/active_admin/page_controller.rb +++ b/app/controllers/active_admin/page_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin # All Pages controllers inherit from this controller. @@ -8,16 +9,12 @@ class PageController < BaseController actions :index - before_filter :authorize_access! + before_action :authorize_access! - def index(options={}, &block) + def index(options = {}, &block) render "active_admin/page/index" end - def clear_page_actions! - active_admin_config.clear_page_actions! - end - private def authorize_access! diff --git a/app/controllers/active_admin/resource_controller.rb b/app/controllers/active_admin/resource_controller.rb new file mode 100644 index 00000000000..bedd3483f1c --- /dev/null +++ b/app/controllers/active_admin/resource_controller.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true +require "active_admin/collection_decorator" + +module ActiveAdmin + # All Resources Controller inherits from this controller. + # It implements actions and helpers for resources. + class ResourceController < BaseController + respond_to :html, :xml, :json + respond_to :csv, only: :index + + before_action :restrict_download_format_access!, only: [:index, :show] + + include ResourceController::ActionBuilder + include ResourceController::Decorators + include ResourceController::DataAccess + include ResourceController::PolymorphicRoutes + include ResourceController::Scoping + include ResourceController::Streaming + extend ResourceClassMethods + + def self.active_admin_config=(config) + if @active_admin_config = config + defaults resource_class: config.resource_class, + route_prefix: config.route_prefix, + instance_name: config.resource_name.singular + end + end + + # Inherited Resources uses the `self.inherited(base)` hook to add + # in `self.resource_class`. To override it, we need to install + # our resource_class method each time we're inherited from. + def self.inherited(base) + super(base) + base.override_resource_class_methods! + end + + private + + def page_presenter + case params[:action].to_sym + when :index + active_admin_config.get_page_presenter(params[:action], params[:as]) + when :new, :edit, :create, :update + active_admin_config.get_page_presenter(:form) + end || super + end + + def default_page_presenter + case params[:action].to_sym + when :index + PagePresenter.new(as: :table) + when :new, :edit + PagePresenter.new + end || super + end + + def page_title + if page_presenter[:title] + case params[:action].to_sym + when :index + case page_presenter[:title] + when Symbol, Proc + instance_exec(&page_presenter[:title]) + else + page_presenter[:title] + end + else + helpers.render_or_call_method_or_proc_on(resource, page_presenter[:title]) + end + else + default_page_title + end + end + + def default_page_title + case params[:action].to_sym + when :index + active_admin_config.plural_resource_label + when :show + helpers.display_name(resource) + when :new, :edit, :create, :update + normalized_action = params[:action] + normalized_action = 'new' if normalized_action == 'create' + normalized_action = 'edit' if normalized_action == 'update' + + ActiveAdmin::Localizers.resource(active_admin_config).t("#{normalized_action}_model") + else + I18n.t("active_admin.#{params[:action]}", default: params[:action].to_s.titleize) + end + end + + def restrict_download_format_access! + unless request.format.html? + presenter = active_admin_config.get_page_presenter(:index) + download_formats = (presenter || {}).fetch(:download_links, active_admin_config.namespace.download_links) + unless build_download_formats(download_formats).include?(request.format.symbol) + raise ActiveAdmin::AccessDenied.new(current_active_admin_user, :index) + end + end + end + end +end diff --git a/lib/active_admin/resource_controller/action_builder.rb b/app/controllers/active_admin/resource_controller/action_builder.rb similarity index 53% rename from lib/active_admin/resource_controller/action_builder.rb rename to app/controllers/active_admin/resource_controller/action_builder.rb index fa0954dad4c..bc85be26849 100644 --- a/lib/active_admin/resource_controller/action_builder.rb +++ b/app/controllers/active_admin/resource_controller/action_builder.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin class ResourceController < BaseController @@ -7,12 +8,22 @@ module ActionBuilder module ClassMethods def clear_member_actions! + remove_action_methods(:member) active_admin_config.clear_member_actions! end def clear_collection_actions! + remove_action_methods(:collection) active_admin_config.clear_collection_actions! end + + private + + def remove_action_methods(actions_type) + active_admin_config.public_send(:"#{actions_type}_actions").each do |action| + remove_method action.name + end + end end end diff --git a/app/controllers/active_admin/resource_controller/data_access.rb b/app/controllers/active_admin/resource_controller/data_access.rb new file mode 100644 index 00000000000..cd543223345 --- /dev/null +++ b/app/controllers/active_admin/resource_controller/data_access.rb @@ -0,0 +1,374 @@ +# frozen_string_literal: true +module ActiveAdmin + class ResourceController < BaseController + + # This module overrides most of the data access methods in Inherited + # Resources to provide Active Admin with it's data. + # + # The module also deals with authorization and resource callbacks. + # + module DataAccess + + def self.included(base) + base.class_exec do + include Callbacks + include ScopeChain + + define_active_admin_callbacks :build, :create, :update, :save, :destroy + + helper_method :current_scope + end + end + + protected + + COLLECTION_APPLIES = [ + :authorization_scope, + :filtering, + :scoping, + :sorting, + :includes, + :pagination, + :collection_decorator + ].freeze + + # Retrieve, memoize and authorize the current collection from the db. This + # method delegates the finding of the collection to #find_collection. + # + # Once #collection has been called, the collection is available using + # either the @collection instance variable or an instance variable named + # after the resource that the collection is for. eg: Post => @post. + # + # @return [ActiveRecord::Relation] The collection for the index + def collection + get_collection_ivar || begin + collection = find_collection + authorize! Authorization::READ, active_admin_config.resource_class + set_collection_ivar collection + end + end + + # Does the actual work of retrieving the current collection from the db. + # This is a great method to override if you would like to perform + # some additional db # work before your controller returns and + # authorizes the collection. + # + # @return [ActiveRecord::Relation] The collection for the index + def find_collection(options = {}) + collection = scoped_collection + collection_applies(options).each do |applyer| + collection = send(:"apply_#{applyer}", collection) + end + collection + end + + # Override this method in your controllers to modify the start point + # of our searches and index. + # + # This method should return an ActiveRecord::Relation object so that + # the searching and filtering can be applied on top + # + # Note, unless you are doing something special, you should use the + # scope_to method from the Scoping module instead of overriding this + # method. + def scoped_collection + end_of_association_chain + end + + # `super` is `resource_collection_name`, which honours `defaults collection_name:`. An + # `as:` alias makes it the alias's plural, which usually names no association on the + # parent, so the model's plural is derived instead. The chain parent is asked rather + # than the registration, because only it knows which of the two it actually has. + def method_for_association_chain + return super unless active_admin_config&.belongs_to? + return super if chain_parent_association?(resource_collection_name) + + active_admin_config.resource_class.model_name.element.pluralize.to_sym + end + + # Ask the actual chain parent (`scope_to` can make it something other than the + # `belongs_to` target) and require the association to target the resource's model, + # because an alias's plural can coincide with an unrelated association's name. + def chain_parent_association?(name) + reflection = association_chain.last.class.try(:reflect_on_association, name) + reflection.present? && reflection.klass <= active_admin_config.resource_class + rescue NameError + false + end + + # Retrieve, memoize and authorize a resource based on params[:id]. The + # actual work of finding the resource is done in #find_resource. + # + # This method is used on all the member actions: + # + # * show + # * edit + # * update + # * destroy + # + # @return [ActiveRecord::Base] An active record object + def resource + get_resource_ivar || begin + resource = find_resource + resource = apply_decorations(resource) + authorize_resource! resource + + set_resource_ivar resource + end + end + + # Does the actual work of finding a resource in the database. This + # method uses the finder method as defined in InheritedResources. + # + # @return [ActiveRecord::Base] An active record object. + def find_resource + scoped_collection.send method_for_find, params[:id] + end + + # Builds, memoize and authorize a new instance of the resource. The + # actual work of building the new instance is delegated to the + # #build_new_resource method. + # + # This method is used to instantiate and authorize new resources in the + # new and create controller actions. + # + # @return [ActiveRecord::Base] An un-saved active record base object + def build_resource + get_resource_ivar || begin + resource = build_new_resource + resource = apply_decorations(resource) + resource = assign_attributes(resource, resource_params) + run_build_callbacks resource + authorize_resource! resource + + set_resource_ivar resource + end + end + + # Builds a new resource. This method uses the method_for_build provided + # by Inherited Resources. + # + # @return [ActiveRecord::Base] An un-saved active record base object + def build_new_resource + apply_authorization_scope(scoped_collection).send( + method_for_build, + *resource_params.map { |params| params.slice(active_admin_config.resource_class.inheritance_column) } + ) + end + + # Calls all the appropriate callbacks and then creates the new resource. + # + # @param [ActiveRecord::Base] object The new resource to create + # + # @return [void] + def create_resource(object) + run_create_callbacks object do + save_resource(object) + end + end + + # Calls all the appropriate callbacks and then saves the new resource. + # + # @param [ActiveRecord::Base] object The new resource to save + # + # @return [void] + def save_resource(object) + run_save_callbacks object do + object.save + end + end + + # Update an object with the given attributes. Also calls the appropriate + # callbacks for update action. + # + # @param [ActiveRecord::Base] object The instance to update + # + # @param [Array] attributes An array with the attributes in the first position + # and the Active Record "role" in the second. The role + # may be set to nil. + # + # @return [void] + def update_resource(object, attributes) + status = nil + ActiveRecord::Base.transaction do + object = assign_attributes(object, attributes) + + run_update_callbacks object do + status = save_resource(object) + raise ActiveRecord::Rollback unless status + end + end + status + end + + # Destroys an object from the database and calls appropriate callbacks. + # + # @return [void] + def destroy_resource(object) + run_destroy_callbacks object do + object.destroy + end + end + + # + # Collection Helper Methods + # + + # Gives the authorization library a change to pre-scope the collection. + # + # In the case of the CanCan adapter, it calls `#accessible_by` on + # the collection. + # + # @param [ActiveRecord::Relation] collection The collection to scope + # + # @return [ActiveRecord::Relation] a scoped collection of query + def apply_authorization_scope(collection) + action_name = action_to_permission(params[:action]) + active_admin_authorization.scope_collection(collection, action_name) + end + + def apply_sorting(chain) + params[:order] ||= active_admin_config.sort_order + order_clause = active_admin_config.order_clause.new(active_admin_config, params[:order]) + + if order_clause.valid? + order_clause.apply(chain) + else + chain # just return the chain + end + end + + # Applies any Ransack search methods to the currently scoped collection. + # Both `search` and `ransack` are provided, but we use `ransack` to prevent conflicts. + def apply_filtering(chain) + @search = chain.ransack(params[:q] || {}, auth_object: active_admin_authorization) + @search.result + end + + def apply_scoping(chain) + @collection_before_scope = chain + + if current_scope + scope_chain(current_scope, chain) + else + chain + end + end + + def apply_includes(chain) + if active_admin_config.includes.any? + chain.includes(*active_admin_config.includes) + else + chain + end + end + + def collection_before_scope + @collection_before_scope + end + + def current_scope + @current_scope ||= if params[:scope] + active_admin_config.get_scope_by_id(params[:scope]) + else + active_admin_config.default_scope(self) + end + end + + def apply_pagination(chain) + # skip pagination if CSV format was requested + return chain if params["format"] == "csv" + # skip pagination if already was paginated by scope + return chain if chain.respond_to?(:total_pages) + page = params[Kaminari.config.param_name] + + paginate(chain, page, per_page) + end + + def collection_applies(options = {}) + only = Array(options.fetch(:only, COLLECTION_APPLIES)) + except = Array(options.fetch(:except, [])) + + COLLECTION_APPLIES & only - except + end + + def in_paginated_batches(&block) + ActiveRecord::Base.uncached do + (1..paginated_collection.total_pages).each do |page| + paginated_collection(page).each do |resource| + yield apply_decorator(resource) + end + end + end + end + + def per_page + if active_admin_config.paginate + dynamic_per_page || configured_per_page + else + active_admin_config.max_per_page + end + end + + def dynamic_per_page + params[:per_page] || @per_page + end + + def configured_per_page + Array(active_admin_config.per_page).first + end + + # @param resource [ActiveRecord::Base] + # @param attributes [Array { + klass = self.class + name = if klass.respond_to?(:model_name) + if klass.respond_to?(:primary_key) + "#{klass.model_name.human} ##{send(klass.primary_key)}" + else + klass.model_name.human + end + elsif klass.respond_to?(:primary_key) + " ##{send(klass.primary_key)}" + end + name.present? ? name : to_s + } + + def DISPLAY_NAME_FALLBACK.inspect + "DISPLAY_NAME_FALLBACK" + end + + # Attempts to call any known display name methods on the resource. + # See the setting in `application.rb` for the list of methods and their priority. + def display_name(resource) + unless resource.nil? + result = render_in_context(resource, display_name_method_for(resource)) + if result.to_s.strip.present? + ERB::Util.html_escape(result) + else + ERB::Util.html_escape(render_in_context(resource, DISPLAY_NAME_FALLBACK)) + end + end + end + + def format_attribute(resource, attr) + value = find_value resource, attr + + if value.is_a?(Arbre::Element) + value + elsif boolean_attr?(resource, attr, value) + Arbre::Context.new { status_tag value } + else + pretty_format value + end + end + + # Attempts to create a human-readable string for any object + def pretty_format(object) + case object + when String, Numeric, Symbol, Arbre::Element + object.to_s + when Date, Time + I18n.localize object, format: active_admin_application.localize_format + when Array + format_collection(object) + else + if defined?(::ActiveRecord) && object.is_a?(ActiveRecord::Base) || + defined?(::Mongoid) && object.class.include?(Mongoid::Document) + auto_link object + elsif defined?(::ActiveRecord) && object.is_a?(ActiveRecord::Relation) + format_collection(object) + else + display_name object + end + end + end + + private + + # Looks up and caches the first available display name method. + # To prevent conflicts, we exclude any methods that happen to be associations. + # If no methods are available and we're about to use the Kernel's `to_s`, provide our own. + def display_name_method_for(resource) + @@display_name_methods_cache ||= {} + @@display_name_methods_cache[resource.class] ||= begin + methods = active_admin_application.display_name_methods - association_methods_for(resource) + method = methods.detect { |method| resource.respond_to? method } + + if method != :to_s || resource.method(method).source_location + method + else + DISPLAY_NAME_FALLBACK + end + end + end + + def association_methods_for(resource) + return [] unless resource.class.respond_to? :reflect_on_all_associations + resource.class.reflect_on_all_associations.map(&:name) + end + + def find_value(resource, attr) + if attr.is_a? Proc + attr.call resource + elsif resource.respond_to? attr + resource.public_send attr + elsif resource.respond_to? :[] + resource[attr] + end + end + + def format_collection(collection) + safe_join(collection.map { |item| pretty_format(item) }, ", ") + end + + def boolean_attr?(resource, attr, value) + case value + when TrueClass, FalseClass + true + else + if resource.class.respond_to? :attribute_types + resource.class.attribute_types[attr.to_s].is_a?(ActiveModel::Type::Boolean) + end + end + end + end +end diff --git a/app/helpers/active_admin/form_helper.rb b/app/helpers/active_admin/form_helper.rb new file mode 100644 index 00000000000..e9a0b391cc0 --- /dev/null +++ b/app/helpers/active_admin/form_helper.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true +module ActiveAdmin + module FormHelper + RESERVED_PARAMS = %w(controller action commit utf8).freeze + + def active_admin_form_for(resource, options = {}, &block) + Arbre::Context.new({}, self) do + active_admin_form_for resource, options, &block + end.content + end + + def hidden_field_tags_for(params, options = {}) + fields_for_params(params.to_unsafe_hash, options).map do |kv| + k, v = kv.first + hidden_field_tag k, v, id: sanitize_to_id("hidden_active_admin_#{k}") + end.join("\n").html_safe + end + + # Flatten a params Hash to an array of fields. + # + # @param params [Hash] + # @param options [Hash] :namespace and :except + # + # @return [Array] of [Hash] with one element. + # + # @example + # fields_for_params(scope: "all", users: ["greg"]) + # => [ {"scope" => "all"} , {"users[]" => "greg"} ] + # + def fields_for_params(params, options = {}) + namespace = options[:namespace] + except = Array.wrap(options[:except]).map(&:to_s) + + params.flat_map do |k, v| + next if namespace.nil? && RESERVED_PARAMS.include?(k.to_s) + next if except.include?(k.to_s) + + if namespace + k = "#{namespace}[#{k}]" + end + + case v + when String, TrueClass, FalseClass + { k => v } + when Symbol + { k => v.to_s } + when Hash + fields_for_params(v, namespace: k) + when Array + v.map do |v| + { "#{k}[]" => v } + end + when nil + { k => "" } + else + raise TypeError, "Cannot convert #{v.class} value: #{v.inspect}" + end + end.compact + end + + # Helper method to render a filter form + def active_admin_filters_form_for(search, filters, options = {}) + defaults = { builder: ActiveAdmin::Filters::FormBuilder, url: collection_path, html: { class: "filters-form" } } + required = { html: { method: :get }, as: :q } + options = defaults.deep_merge(options).deep_merge(required) + + form_for search, options do |f| + f.template.concat hidden_field_tags_for(params, except: except_hidden_fields) + + filters.each do |attribute, opts| + next if opts.key?(:if) && !call_method_or_proc_on(self, opts[:if]) + next if opts.key?(:unless) && call_method_or_proc_on(self, opts[:unless]) + + filter_opts = opts.except(:if, :unless) + filter_opts[:input_html] = instance_exec(&filter_opts[:input_html]) if filter_opts[:input_html].is_a?(Proc) + + f.filter attribute, filter_opts + end + + buttons = content_tag :div, class: "filters-form-buttons" do + f.submit(I18n.t("active_admin.filters.buttons.filter"), class: "filters-form-submit") + + link_to(I18n.t("active_admin.filters.buttons.clear"), collection_path, class: "filters-form-clear") + end + + f.template.concat buttons + end + end + + private + + def except_hidden_fields + [:q, :page] + end + end +end diff --git a/app/helpers/active_admin/index_helper.rb b/app/helpers/active_admin/index_helper.rb new file mode 100644 index 00000000000..013201d37a0 --- /dev/null +++ b/app/helpers/active_admin/index_helper.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true +module ActiveAdmin + module IndexHelper + def scope_name(scope) + case scope.name + when Proc then + self.instance_exec(&scope.name).to_s + else + scope.name.to_s + end + end + + def batch_actions_to_display + @batch_actions_to_display ||= begin + if active_admin_config && active_admin_config.batch_actions.any? + active_admin_config.batch_actions.select do |batch_action| + call_method_or_proc_on(self, batch_action.display_if_block) + end + else + [] + end + end + end + + # 1. removes `select` and `order` to prevent invalid SQL + # 2. correctly handles the Hash returned when `group by` is used + def collection_size(c = collection) + return c.count if c.is_a?(Array) + return c.length if c.limit_value + + c = c.except :select, :order + + c.group_values.present? ? c.count.count : c.count + end + + def collection_empty?(c = collection) + collection_size(c) == 0 + end + end +end diff --git a/app/helpers/active_admin/layout_helper.rb b/app/helpers/active_admin/layout_helper.rb new file mode 100644 index 00000000000..0a82a466194 --- /dev/null +++ b/app/helpers/active_admin/layout_helper.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true +module ActiveAdmin + module LayoutHelper + # Returns the current Active Admin application instance + def active_admin_application + ActiveAdmin.application + end + + def set_page_title(title) + @page_title = title + end + + def site_title + # Prioritize namespace and account for Devise views where namespace is not available + namespace = active_admin_namespace if respond_to?(:active_admin_namespace) + (namespace || active_admin_application).site_title(self) + end + + def html_head_site_title(separator: "-") + safe_join([@page_title || page_title, site_title], " #{separator} ") + end + + def action_items_for_action + @action_items_for_action ||= begin + if active_admin_config&.action_items? + active_admin_config.action_items_for(params[:action], self) + else + [] + end + end + end + + def sidebar_sections_for_action + @sidebar_sections_for_action ||= begin + if active_admin_config&.sidebar_sections? + active_admin_config.sidebar_sections_for(params[:action], self) + else + [] + end + end + end + + def skip_sidebar! + @skip_sidebar = true + end + + def skip_sidebar? + @skip_sidebar == true + end + + def flash_messages + @flash_messages ||= flash.to_hash.except(*active_admin_application.flash_keys_to_except) + end + + def url_for_comments(*args) + parts = [] + parts << active_admin_namespace.name unless active_admin_namespace.root? + parts << active_admin_namespace.comments_registration_name.underscore + parts << "path" + send parts.join("_"), *args + end + end +end diff --git a/app/javascript/active_admin.js b/app/javascript/active_admin.js new file mode 100644 index 00000000000..fcaaf86ec2b --- /dev/null +++ b/app/javascript/active_admin.js @@ -0,0 +1,10 @@ +import "flowbite" +import Rails from "@rails/ujs" +import "active_admin/features/batch_actions" +import "active_admin/features/dark_mode_toggle" +import "active_admin/features/has_many" +import "active_admin/features/filters" +import "active_admin/features/main_menu" +import "active_admin/features/per_page" + +Rails.start() diff --git a/app/javascript/active_admin/features/batch_actions.js b/app/javascript/active_admin/features/batch_actions.js new file mode 100644 index 00000000000..a9bf3ecd408 --- /dev/null +++ b/app/javascript/active_admin/features/batch_actions.js @@ -0,0 +1,95 @@ +import Rails from '@rails/ujs'; + +const submitForm = function() { + let form = document.getElementById("collection_selection") + if (form) { + form.submit() + } +} + +const batchActionClick = function(event) { + event.preventDefault() + let batchAction = document.getElementById("batch_action") + if (batchAction) { + batchAction.value = this.dataset.action + } + + if (!event.target.dataset.confirm && !event.target.dataset.modalTarget) { submitForm() } +} + +const batchActionConfirmComplete = function(event) { + event.preventDefault() + if (event.detail[0] === true) { + let batchAction = document.getElementById("batch_action") + if (batchAction) { + batchAction.value = this.dataset.action + } + submitForm() + } +} + +const batchActionFormSubmit = function(event) { + event.preventDefault(); + let json = JSON.stringify(Object.fromEntries(new FormData(this).entries())); + let inputsField = document.getElementById('batch_action_inputs') + let form = document.getElementById("collection_selection") + if (json && inputsField && form) { + inputsField.value = json + form.submit() + } +} + +Rails.delegate(document, "[data-batch-action-item]", "confirm:complete", batchActionConfirmComplete) +Rails.delegate(document, "[data-batch-action-item]", "click", batchActionClick) +Rails.delegate(document, "form[data-batch-action-form]", "submit", batchActionFormSubmit) + +const disableDropdown = function(condition) { + const button = document.querySelector(".batch-actions-dropdown-toggle") + if (button) { + button.disabled = condition + } +} + +const toggleAllChange = function(event) { + const checkboxes = document.querySelectorAll(".batch-actions-resource-selection") + for (const checkbox of checkboxes) { + checkbox.checked = this.checked + } + + const rows = document.querySelectorAll(".paginated-collection tbody tr") + for (const row of rows) { + row.classList.toggle("selected", this.checked); + } + + disableDropdown(!this.checked) +} + +Rails.delegate(document, ".batch-actions-toggle-all", "change", toggleAllChange) + +const toggleCheckboxChange = function(event) { + const numChecked = document.querySelectorAll(".batch-actions-resource-selection:checked").length; + const allChecked = numChecked === document.querySelectorAll(".batch-actions-resource-selection").length; + const someChecked = (numChecked > 0) && (numChecked < document.querySelectorAll(".batch-actions-resource-selection").length); + + const toggleAll = document.querySelector(".batch-actions-toggle-all") + if (toggleAll) { + toggleAll.checked = allChecked + toggleAll.indeterminate = someChecked + } + + disableDropdown(numChecked === 0) +} + +Rails.delegate(document, ".batch-actions-resource-selection", "change", toggleCheckboxChange) + +const tableRowClick = function(event) { + const type = event.target.type; + if (typeof type === "undefined" || (type !== "checkbox" && type !== "button" && type !== "")) { + const checkbox = event.target.closest("tr").querySelector("input[type=checkbox]") + if (checkbox) { + checkbox.click() + } + } +} + +Rails.delegate(document, ".paginated-collection tbody td", "click", tableRowClick) diff --git a/app/javascript/active_admin/features/dark_mode_toggle.js b/app/javascript/active_admin/features/dark_mode_toggle.js new file mode 100644 index 00000000000..417f36177b3 --- /dev/null +++ b/app/javascript/active_admin/features/dark_mode_toggle.js @@ -0,0 +1,37 @@ +import Rails from '@rails/ujs'; + +const THEME_KEY = "theme"; +const darkModeMedia = window.matchMedia('(prefers-color-scheme: dark)'); + +const setTheme = () => { + // On page load or when changing themes, best to add inline in `head` to avoid FOUC + if (localStorage.getItem(THEME_KEY) === 'dark' || (!(THEME_KEY in localStorage) && darkModeMedia.matches)) { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } +} + +// Detect when user changes their system level preference to set theme. +darkModeMedia.addEventListener("change", setTheme); + +// When the page loads, set theme. By default, uses the system preference. +document.addEventListener("DOMContentLoaded", setTheme); + +// If user deletes the Local Storage key, then re-apply theme. +window.addEventListener("storage", (event) => { + if (event.key === THEME_KEY) { + setTheme() + } +}); + +const toggleTheme = () => { + if (localStorage.getItem(THEME_KEY) === 'dark' || (!(THEME_KEY in localStorage) && darkModeMedia.matches)) { + localStorage.setItem(THEME_KEY, 'light'); + } else { + localStorage.setItem(THEME_KEY, 'dark'); + } + setTheme(); +}; + +Rails.delegate(document, ".dark-mode-toggle", "click", toggleTheme); diff --git a/app/javascript/active_admin/features/filters.js b/app/javascript/active_admin/features/filters.js new file mode 100644 index 00000000000..6e8c5e25263 --- /dev/null +++ b/app/javascript/active_admin/features/filters.js @@ -0,0 +1,34 @@ +import Rails from '@rails/ujs'; +import { nextSibling } from 'active_admin/utils/dom' + +const disableEmptyFields = function(event) { + Array.from(this.querySelectorAll("input, select, textarea")) + .filter((el) => el.value === "") + .forEach((el) => el.disabled = true) +}; + +Rails.delegate(document, ".filters-form", "submit", disableEmptyFields) + +const setSearchType = function(event) { + const input = nextSibling(this, "input") + if (input) { + input.name = `q[${this.value}]` + } +}; + +Rails.delegate(document, ".filters-form-field [data-search-methods]", "change", setSearchType) + +const clearFiltersForm = function(event) { + event.preventDefault() + + const regex = /^(q\[|page|utf8|commit)/ + const params = new URLSearchParams(window.location.search) + + Array.from(params.keys()) + .filter(k => k.match(regex)) + .forEach(k => params.delete(k)) + + window.location.search = params.toString() +} + +Rails.delegate(document, ".filters-form-clear", "click", clearFiltersForm) diff --git a/app/javascript/active_admin/features/has_many.js b/app/javascript/active_admin/features/has_many.js new file mode 100644 index 00000000000..524e093c18d --- /dev/null +++ b/app/javascript/active_admin/features/has_many.js @@ -0,0 +1,28 @@ +import Rails from '@rails/ujs'; + +const hasManyRemoveClick = function(event) { + event.preventDefault() + const oldGroup = this.closest("fieldset") + if (oldGroup) { + oldGroup.remove() + } +} + +Rails.delegate(document, "form .has-many-remove", "click", hasManyRemoveClick) + +const hasManyAddClick = function(event) { + event.preventDefault() + const parent = this.closest(".has-many-container") + + let index = parseInt(parent.dataset.has_many_index || (parent.querySelectorAll('fieldset').length - 1), 10) + parent.dataset.has_many_index = ++index + + const regex = new RegExp(this.dataset.placeholder, 'g') + const html = this.dataset.html.replace(regex, index) + + const tempEl = document.createElement("div"); + tempEl.innerHTML = html + this.before(tempEl.firstChild) +} + +Rails.delegate(document, "form .has-many-add", "click", hasManyAddClick) diff --git a/app/javascript/active_admin/features/main_menu.js b/app/javascript/active_admin/features/main_menu.js new file mode 100644 index 00000000000..03e7332f505 --- /dev/null +++ b/app/javascript/active_admin/features/main_menu.js @@ -0,0 +1,13 @@ +import Rails from '@rails/ujs'; + +const toggleMenu = function() { + const parent = this.closest([`[data-item-id="${this.dataset.parentId}"]`]) || this.parentNode + + if (!("open" in parent.dataset)) { + parent.dataset.open = "" + } else { + delete parent.dataset.open + } +} + +Rails.delegate(document, "#main-menu [data-menu-button]", "click", toggleMenu) diff --git a/app/javascript/active_admin/features/per_page.js b/app/javascript/active_admin/features/per_page.js new file mode 100644 index 00000000000..499c6b45fd1 --- /dev/null +++ b/app/javascript/active_admin/features/per_page.js @@ -0,0 +1,9 @@ +import Rails from '@rails/ujs'; + +const setPerPage = function(event) { + const params = new URLSearchParams(window.location.search) + params.set("per_page", this.value) + window.location.search = params +} + +Rails.delegate(document, ".pagination-per-page", "change", setPerPage) diff --git a/app/javascript/active_admin/utils/dom.js b/app/javascript/active_admin/utils/dom.js new file mode 100644 index 00000000000..20f2d051edb --- /dev/null +++ b/app/javascript/active_admin/utils/dom.js @@ -0,0 +1,17 @@ +const nextSibling = function next(element, selector) { + let sibling = element.nextElementSibling; + + if (!selector) { + return sibling; + } + + while (sibling) { + if (sibling && sibling.matches(selector)) { + return sibling; + } + + sibling = sibling.nextElementSibling; + } +} + +export { nextSibling } diff --git a/app/views/active_admin/_flash_messages.html.erb b/app/views/active_admin/_flash_messages.html.erb new file mode 100644 index 00000000000..2e67f504610 --- /dev/null +++ b/app/views/active_admin/_flash_messages.html.erb @@ -0,0 +1,22 @@ +<% if flash_messages.present? %> +
+ <% flash_messages.each do |type, message| %> + <% if type == "error" %> +
+ + <%= message %> +
+ <% elsif type == "alert" %> +
+ + <%= message %> +
+ <% elsif type == "notice" %> +
+ + <%= message %> +
+ <% end %> + <% end %> +
+<% end %> diff --git a/app/views/active_admin/_html_head.html.erb b/app/views/active_admin/_html_head.html.erb new file mode 100644 index 00000000000..afbfd32ae28 --- /dev/null +++ b/app/views/active_admin/_html_head.html.erb @@ -0,0 +1,13 @@ +<%= stylesheet_link_tag "active_admin" %> + +<%= csrf_meta_tags %> +<%= csp_meta_tag %> +<% # On page load or when changing themes, best to add inline in `head` to avoid FOUC %> +<%= javascript_tag nonce: true do %> + if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) { + document.documentElement.classList.add('dark') + } else { + document.documentElement.classList.remove('dark') + } +<% end %> +<%= javascript_importmap_tags "active_admin", importmap: ActiveAdmin.importmap %> diff --git a/app/views/active_admin/_main_navigation.html.erb b/app/views/active_admin/_main_navigation.html.erb new file mode 100644 index 00000000000..9f11ccfb919 --- /dev/null +++ b/app/views/active_admin/_main_navigation.html.erb @@ -0,0 +1,45 @@ + diff --git a/app/views/active_admin/_page_header.html.erb b/app/views/active_admin/_page_header.html.erb new file mode 100644 index 00000000000..6b17515c1a4 --- /dev/null +++ b/app/views/active_admin/_page_header.html.erb @@ -0,0 +1,27 @@ +
+
+ <% breadcrumb_links = build_breadcrumb_links(request.path, class: "text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 no-underline") %> + <% if breadcrumb_links.present? %> + + <% end %> +

<%= title %>

+
+ <% if action_items_for_action.present? %> +
+ <%= render "active_admin/shared/action_items" %> +
+ <% end %> +
diff --git a/app/views/active_admin/_sidebar.html.erb b/app/views/active_admin/_sidebar.html.erb new file mode 100644 index 00000000000..eef67b78c45 --- /dev/null +++ b/app/views/active_admin/_sidebar.html.erb @@ -0,0 +1,5 @@ +<% unless skip_sidebar? || sidebar_sections_for_action.blank? %> +
+ <%= render "active_admin/shared/sidebar_sections" %> +
+<% end %> diff --git a/app/views/active_admin/_site_footer.html.erb b/app/views/active_admin/_site_footer.html.erb new file mode 100644 index 00000000000..273a01ea2a1 --- /dev/null +++ b/app/views/active_admin/_site_footer.html.erb @@ -0,0 +1,7 @@ +
+ <%= I18n.t( + "active_admin.powered_by", + active_admin: link_to("Active Admin", "https://activeadmin.info", class: "text-gray-500 dark:text-gray-500 hover:text-gray-900 dark:hover:text-gray-400 no-underline"), + version: ActiveAdmin::VERSION + ).html_safe %> +
diff --git a/app/views/active_admin/_site_header.html.erb b/app/views/active_admin/_site_header.html.erb new file mode 100644 index 00000000000..87db7a426bd --- /dev/null +++ b/app/views/active_admin/_site_header.html.erb @@ -0,0 +1,30 @@ +
+ + +
+

+ <%= title %> +

+
+ + + + + + + +
diff --git a/app/views/active_admin/dashboard/index.html.arb b/app/views/active_admin/dashboard/index.html.arb deleted file mode 100644 index d4c18530ab4..00000000000 --- a/app/views/active_admin/dashboard/index.html.arb +++ /dev/null @@ -1 +0,0 @@ -insert_tag view_factory.dashboard_page diff --git a/app/views/active_admin/devise/confirmations/new.html.erb b/app/views/active_admin/devise/confirmations/new.html.erb new file mode 100644 index 00000000000..1d83245a901 --- /dev/null +++ b/app/views/active_admin/devise/confirmations/new.html.erb @@ -0,0 +1,17 @@ +
+

+ <%= active_admin_application.site_title(self) %> <%= set_page_title t('active_admin.devise.resend_confirmation_instructions.title') %> +

+ + <%= render partial: "active_admin/devise/shared/error_messages", resource: resource %> + <%= active_admin_form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| + f.inputs do + f.input :email + end + f.actions do + f.action :submit, label: t('active_admin.devise.resend_confirmation_instructions.submit'), button_html: { class: "w-full", value: t('active_admin.devise.resend_confirmation_instructions.submit') } + end + end %> + + <%= render partial: "active_admin/devise/shared/links" %> +
diff --git a/app/views/active_admin/devise/mailer/reset_password_instructions.html.erb b/app/views/active_admin/devise/mailer/reset_password_instructions.html.erb index ae9e888abb9..7913e88beb6 100644 --- a/app/views/active_admin/devise/mailer/reset_password_instructions.html.erb +++ b/app/views/active_admin/devise/mailer/reset_password_instructions.html.erb @@ -2,7 +2,7 @@

Someone has requested a link to change your password, and you can do this through the link below.

-

<%= link_to 'Change my password', edit_password_url(@resource, :reset_password_token => @resource.reset_password_token) %>

+

<%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>

If you didn't request this, please ignore this email.

Your password won't change until you access the link above and create a new one.

diff --git a/app/views/active_admin/devise/mailer/unlock_instructions.html.erb b/app/views/active_admin/devise/mailer/unlock_instructions.html.erb index 2263c219522..41e148bf2ac 100644 --- a/app/views/active_admin/devise/mailer/unlock_instructions.html.erb +++ b/app/views/active_admin/devise/mailer/unlock_instructions.html.erb @@ -1,7 +1,7 @@

Hello <%= @resource.email %>!

-

Your account has been locked due to an excessive amount of unsuccessful sign in attempts.

+

Your account has been locked due to an excessive number of unsuccessful sign in attempts.

Click the link below to unlock your account:

-

<%= link_to 'Unlock my account', unlock_url(@resource, :unlock_token => @resource.unlock_token) %>

+

<%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>

diff --git a/app/views/active_admin/devise/passwords/edit.html.erb b/app/views/active_admin/devise/passwords/edit.html.erb index b29634550d2..4703627920e 100644 --- a/app/views/active_admin/devise/passwords/edit.html.erb +++ b/app/views/active_admin/devise/passwords/edit.html.erb @@ -1,19 +1,20 @@ -
- <%= content_tag :h2, t('active_admin.devise.change_password.title') %> +
+

+ <%= active_admin_application.site_title(self) %> <%= set_page_title t('active_admin.devise.change_password.title') %> +

- <%= active_admin_form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :put }) do |f| %> - <%= devise_error_messages! %> - <% + <%= render partial: "active_admin/devise/shared/error_messages", resource: resource %> + <%= active_admin_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| f.inputs do f.input :password f.input :password_confirmation - end %> - <%= + f.input :reset_password_token, as: :hidden, input_html: { value: resource.reset_password_token } + end f.actions do - f.action :submit, :label => t('active_admin.devise.change_password.submit'), :button_html => { :value => t('active_admin.devise.change_password.submit') } - f.input :reset_password_token, :as => :hidden, :input_html => { :value => resource.reset_password_token } - end %> - <% end %> + f.action :submit, label: t('active_admin.devise.change_password.submit'), button_html: { class: "w-full", value: t('active_admin.devise.change_password.submit') } + end + end + %> <%= render 'active_admin/devise/shared/links' %>
diff --git a/app/views/active_admin/devise/passwords/new.html.erb b/app/views/active_admin/devise/passwords/new.html.erb index 6c4b1cdfe94..1c03cd706d9 100644 --- a/app/views/active_admin/devise/passwords/new.html.erb +++ b/app/views/active_admin/devise/passwords/new.html.erb @@ -1,14 +1,16 @@ -
- <%= content_tag :h2, t('active_admin.devise.reset_password.title') %> +
+

+ <%= active_admin_application.site_title(self) %> <%= set_page_title t('active_admin.devise.reset_password.title') %> +

- <%= active_admin_form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :post }) do |f| + <%= active_admin_form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| f.inputs do f.input :email end f.actions do - f.action :submit, :label => t('active_admin.devise.reset_password.submit'), :button_html => { :value => t('active_admin.devise.reset_password.submit') } + f.action :submit, label: t('active_admin.devise.reset_password.submit'), button_html: { class: "w-full", value: t('active_admin.devise.reset_password.submit') } end end %> - <%= render :partial => "active_admin/devise/shared/links" %> + <%= render partial: "active_admin/devise/shared/links" %>
diff --git a/app/views/active_admin/devise/registrations/new.html.erb b/app/views/active_admin/devise/registrations/new.html.erb new file mode 100644 index 00000000000..a6c4a55a619 --- /dev/null +++ b/app/views/active_admin/devise/registrations/new.html.erb @@ -0,0 +1,23 @@ +
+

+ <%= active_admin_application.site_title(self) %> <%= set_page_title t('active_admin.devise.sign_up.title') %> +

+ + <% scope = Devise::Mapping.find_scope!(resource_name) %> + <%= render partial: "active_admin/devise/shared/error_messages", resource: resource %> + <%= active_admin_form_for(resource, as: resource_name, url: main_app.send(:"#{scope}_registration_path"), html: { id: "registration_new" }) do |f| + f.inputs do + resource.class.authentication_keys.each_with_index { |key, index| + f.input key, label: t('active_admin.devise.'+key.to_s+'.title'), input_html: { autofocus: index.zero? } + } + f.input :password, label: t('active_admin.devise.password.title') + f.input :password_confirmation, label: t('active_admin.devise.password_confirmation.title') + end + f.actions do + f.action :submit, label: t('active_admin.devise.login.submit'), button_html: { class: "w-full", value: t('active_admin.devise.sign_up.submit') } + end + end + %> + + <%= render partial: "active_admin/devise/shared/links" %> +
diff --git a/app/views/active_admin/devise/sessions/new.html.erb b/app/views/active_admin/devise/sessions/new.html.erb index 91e780df3eb..1c1172e8621 100644 --- a/app/views/active_admin/devise/sessions/new.html.erb +++ b/app/views/active_admin/devise/sessions/new.html.erb @@ -1,18 +1,22 @@ -
-

<%= title "#{render_or_call_method_or_proc_on(self, active_admin_application.site_title)} #{t('active_admin.devise.login.title')}" %>

+
+

+ <%= site_title %> <%= set_page_title t('active_admin.devise.login.title') %> +

<% scope = Devise::Mapping.find_scope!(resource_name) %> - <%= active_admin_form_for(resource, :as => resource_name, :url => send(:"#{scope}_session_path"), :html => { :id => "session_new" }) do |f| + <%= active_admin_form_for(resource, as: resource_name, url: main_app.send(:"#{scope}_session_path")) do |f| f.inputs do - resource.class.authentication_keys.each { |key| f.input key, :input_html => {:autofocus => true}} - f.input :password - f.input :remember_me, :label => t('active_admin.devise.login.remember_me'), :as => :boolean if devise_mapping.rememberable? + resource.class.authentication_keys.each_with_index { |key, index| + f.input key, label: t("active_admin.devise.#{key}.title"), input_html: { autofocus: index.zero? } + } + f.input :password, label: t('active_admin.devise.password.title') + f.input :remember_me, label: t('active_admin.devise.login.remember_me'), as: :boolean if devise_mapping.rememberable? end f.actions do - f.action :submit, :label => t('active_admin.devise.login.submit'), :button_html => { :value => t('active_admin.devise.login.submit') } + f.action :submit, label: t('active_admin.devise.login.submit'), wrapper_html: { class: "grow" }, button_html: { class: "w-full", value: t('active_admin.devise.login.submit') } end end %> - <%= render :partial => "active_admin/devise/shared/links" %> + <%= render partial: "active_admin/devise/shared/links" %>
diff --git a/app/views/active_admin/devise/shared/_error_messages.html.erb b/app/views/active_admin/devise/shared/_error_messages.html.erb new file mode 100644 index 00000000000..ba7ab887013 --- /dev/null +++ b/app/views/active_admin/devise/shared/_error_messages.html.erb @@ -0,0 +1,15 @@ +<% if resource.errors.any? %> +
+

+ <%= I18n.t("errors.messages.not_saved", + count: resource.errors.count, + resource: resource.class.model_name.human.downcase) + %> +

+
    + <% resource.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+<% end %> diff --git a/app/views/active_admin/devise/shared/_links.erb b/app/views/active_admin/devise/shared/_links.erb index bf4cf49db6c..e820abe4389 100644 --- a/app/views/active_admin/devise/shared/_links.erb +++ b/app/views/active_admin/devise/shared/_links.erb @@ -1,27 +1,35 @@ +
<%- if controller_name != 'sessions' %> <% scope = Devise::Mapping.find_scope!(resource_name) %> - <%= link_to t('active_admin.devise.links.sign_in'), send(:"new_#{scope}_session_path") %>
+ <%= link_to t('active_admin.devise.links.sign_in'), main_app.send(:"new_#{scope}_session_path") %> +
<% end -%> <%- if devise_mapping.registerable? && controller_name != 'registrations' %> - <%= link_to t('active_admin.devise.links.sign_up'), new_registration_path(resource_name) %>
+ <%= link_to t('active_admin.devise.links.sign_up'), new_registration_path(resource_name) %> +
<% end -%> <%- if devise_mapping.recoverable? && controller_name != 'passwords' %> - <%= link_to t('active_admin.devise.links.forgot_your_password'), new_password_path(resource_name) %>
+ <%= link_to t('active_admin.devise.links.forgot_your_password'), new_password_path(resource_name) %> +
<% end -%> <%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> - <%= link_to t('active_admin.devise.links.resend_confirmation_instructions'), new_confirmation_path(resource_name) %>
+ <%= link_to t('active_admin.devise.links.resend_confirmation_instructions'), new_confirmation_path(resource_name) %> +
<% end -%> <%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> - <%= link_to t('active_admin.devise.links.resend_unlock_instructions'), new_unlock_path(resource_name) %>
+ <%= link_to t('active_admin.devise.links.resend_unlock_instructions'), new_unlock_path(resource_name) %> +
<% end -%> <%- if devise_mapping.omniauthable? %> <%- resource_class.omniauth_providers.each do |provider| %> - <%= link_to t('active_admin.devise.links.sign_in_with_omniauth_provider', :provider => provider.to_s.titleize), - omniauth_authorize_path(resource_name, provider) %>
+ <%= link_to t('active_admin.devise.links.sign_in_with_omniauth_provider', provider: provider.to_s.titleize), + omniauth_authorize_path(resource_name, provider), method: :post %> +
<% end -%> <% end -%> +
diff --git a/app/views/active_admin/devise/unlocks/new.html.erb b/app/views/active_admin/devise/unlocks/new.html.erb index 61017dbf8e4..24050a089be 100644 --- a/app/views/active_admin/devise/unlocks/new.html.erb +++ b/app/views/active_admin/devise/unlocks/new.html.erb @@ -1,14 +1,17 @@ -
-

<%= title "#{render_or_call_method_or_proc_on(self, active_admin_application.site_title)} #{t('active_admin.devise.unlock.title')}" %>

+
+

+ <%= site_title %> <%= set_page_title t('active_admin.devise.unlock.title') %> +

- <%= active_admin_form_for(resource, :as => resource_name, :url => unlock_path(resource_name), :html => { :method => :post }) do |f| + <%= render partial: "active_admin/devise/shared/error_messages", resource: resource %> + <%= active_admin_form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| f.inputs do f.input :email end f.actions do - f.action :submit, :label => t('active_admin.devise.unlock.submit'), :button_html => { :value => t('active_admin.devise.unlock.submit') } + f.action :submit, label: t('active_admin.devise.unlock.submit'), button_html: { class: "w-full", value: t('active_admin.devise.unlock.submit') } end end %> -<%= render :partial => "active_admin/devise/shared/links" %> + <%= render partial: "active_admin/devise/shared/links" %>
diff --git a/app/views/active_admin/kaminari/_gap.html.erb b/app/views/active_admin/kaminari/_gap.html.erb new file mode 100644 index 00000000000..a310332dc25 --- /dev/null +++ b/app/views/active_admin/kaminari/_gap.html.erb @@ -0,0 +1,10 @@ +<%# Non-link tag that stands for skipped pages... + - available local variables + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> + + <%= t('active_admin.pagination.truncate').html_safe %> + diff --git a/app/views/active_admin/kaminari/_next_page.html.erb b/app/views/active_admin/kaminari/_next_page.html.erb new file mode 100644 index 00000000000..97566c1890d --- /dev/null +++ b/app/views/active_admin/kaminari/_next_page.html.erb @@ -0,0 +1,16 @@ +<%# Link to the "Next" page + - available local variables + url: url to the next page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> +<% unless current_page.last? %> + <%= link_to url, rel: 'next', remote: remote, class: "flex items-center justify-center px-2.5 py-3 h-8 leading-tight text-gray-500 dark:text-gray-400 hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-gray-800 dark:hover:text-white rounded no-underline" do %> + <%= t('active_admin.pagination.next') %> + + <% end %> +<% end %> diff --git a/app/views/active_admin/kaminari/_page.html.erb b/app/views/active_admin/kaminari/_page.html.erb new file mode 100644 index 00000000000..3b48ec63cab --- /dev/null +++ b/app/views/active_admin/kaminari/_page.html.erb @@ -0,0 +1,14 @@ +<%# Link showing page number + - available local variables + page: a page object for "this" page + url: url to this page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> +<% if page.current? %> + <%= link_to page, url, { remote: remote, rel: page.rel, class: "flex items-center justify-center px-2.5 py-3 h-8 leading-tight text-white bg-blue-500 dark:text-white dark:bg-blue-600 hover:bg-blue-500 hover:text-white dark:hover:bg-blue-600 dark:hover:text-white rounded no-underline" } %> +<% else %> + <%= link_to page, url, { remote: remote, rel: page.rel, class: "flex items-center justify-center px-2.5 py-3 h-8 leading-tight text-gray-500 dark:text-gray-400 hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-gray-800 dark:hover:text-white rounded no-underline" } %> +<% end %> diff --git a/app/views/active_admin/kaminari/_paginator.html.erb b/app/views/active_admin/kaminari/_paginator.html.erb new file mode 100644 index 00000000000..ade78b2f619 --- /dev/null +++ b/app/views/active_admin/kaminari/_paginator.html.erb @@ -0,0 +1,23 @@ +<%# The container tag + - available local variables + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote + paginator: the paginator that renders the pagination tags inside +-%> +<%= paginator.render do -%> + +<% end -%> diff --git a/app/views/active_admin/kaminari/_prev_page.html.erb b/app/views/active_admin/kaminari/_prev_page.html.erb new file mode 100644 index 00000000000..fad4ddf169b --- /dev/null +++ b/app/views/active_admin/kaminari/_prev_page.html.erb @@ -0,0 +1,16 @@ +<%# Link to the "Previous" page + - available local variables + url: url to the previous page + current_page: a page object for the currently displayed page + total_pages: total number of pages + per_page: number of items to fetch per page + remote: data-remote +-%> +<% unless current_page.first? %> + <%= link_to url, rel: 'prev', remote: remote, class: "flex items-center justify-center px-2.5 py-3 h-8 leading-tight text-gray-500 dark:text-gray-400 hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-gray-800 dark:hover:text-white rounded no-underline" do %> + <%= t('active_admin.pagination.previous') %> + + <% end %> +<% end %> diff --git a/app/views/active_admin/page/index.html.arb b/app/views/active_admin/page/index.html.arb index a1c14ae8a34..19e0b9e838a 100644 --- a/app/views/active_admin/page/index.html.arb +++ b/app/views/active_admin/page/index.html.arb @@ -1 +1,5 @@ -insert_tag active_admin_application.view_factory["page"] +div class: "main-content-container" do + if page_presenter.block + instance_exec(&page_presenter.block) + end +end diff --git a/app/views/active_admin/resource/_active_filters.html.erb b/app/views/active_admin/resource/_active_filters.html.erb new file mode 100644 index 00000000000..16cc8625bf5 --- /dev/null +++ b/app/views/active_admin/resource/_active_filters.html.erb @@ -0,0 +1,32 @@ +
+

+ <% if current_scope %> + <%= I18n.t("active_admin.search_status.title_with_scope", name: scope_name(current_scope)) %> + <% else %> + <%= I18n.t("active_admin.search_status.title") %> + <% end %> +

+
    + <% if active_filters.all_blank? %> +
  • <%= I18n.t("active_admin.search_status.no_current_filters") %>
  • + <% else %> + <% active_filters.filters.each do |filter| %> + <%= content_tag :li, filter.html_options do %> + + <%= filter.label %> + <%= to_sentence(filter.values.map { |v| pretty_format(v) }) %> + + <% end %> + <% end %> + <% active_filters.scopes.each do |name, value| %> + <% filter_name = name.gsub(/_eq$/, "") %> + <% filter = active_admin_config.filters[filter_name.to_sym] %> + <% label = filter.try(:[], :label) || filter_name.titleize %> +
  • + <%= "#{label} #{Ransack::Translate.predicate('eq')}" %> + <%= value %> +
  • + <% end %> + <% end %> +
+
diff --git a/app/views/active_admin/resource/_batch_actions_dropdown.html.erb b/app/views/active_admin/resource/_batch_actions_dropdown.html.erb new file mode 100644 index 00000000000..71339414e19 --- /dev/null +++ b/app/views/active_admin/resource/_batch_actions_dropdown.html.erb @@ -0,0 +1,19 @@ +<% if batch_actions_to_display.any? %> +
+ +
    + <% batch_actions_to_display.each do |batch_action| %> +
  • + <% confirmation_text = render_or_call_method_or_proc_on(self, batch_action.confirm) %> + <% default_title = render_or_call_method_or_proc_on(self, batch_action.title) %> + <% title = I18n.t("active_admin.batch_actions.labels.#{batch_action.sym}", default: default_title) %> + <% label = I18n.t("active_admin.batch_actions.action_label", title: title) %> + <%= link_to(label, "#", batch_action.link_html_options.merge(data: { action: batch_action.sym, confirm: confirmation_text.presence, batch_action_item: "" })) %> +
  • + <% end %> +
+
+<% end %> diff --git a/app/views/active_admin/resource/_form.html.arb b/app/views/active_admin/resource/_form.html.arb new file mode 100644 index 00000000000..97f7c195f3e --- /dev/null +++ b/app/views/active_admin/resource/_form.html.arb @@ -0,0 +1,15 @@ +div class: "main-content-container" do + if page_presenter.block + options = { + url: resource.persisted? ? resource_path(resource) : collection_path, + as: active_admin_config.param_key + } + options.merge!(page_presenter.options) + + active_admin_form_for(resource, options, &page_presenter.block) + elsif page_presenter.options[:partial].present? + render page_presenter.options[:partial] + else + render "form_default" + end +end diff --git a/app/views/active_admin/resource/_form_default.html.arb b/app/views/active_admin/resource/_form_default.html.arb new file mode 100644 index 00000000000..f377cf1a38d --- /dev/null +++ b/app/views/active_admin/resource/_form_default.html.arb @@ -0,0 +1,11 @@ +options = { + url: resource.persisted? ? resource_path(resource) : collection_path, + as: active_admin_config.param_key +} +options.merge!(page_presenter.options) + +active_admin_form_for(resource, options) do |f| + f.semantic_errors # show errors on :base by default + f.inputs + f.actions +end diff --git a/app/views/active_admin/resource/_index_as_table_default.html.arb b/app/views/active_admin/resource/_index_as_table_default.html.arb new file mode 100644 index 00000000000..c13cb78a0aa --- /dev/null +++ b/app/views/active_admin/resource/_index_as_table_default.html.arb @@ -0,0 +1,8 @@ +insert_tag(ActiveAdmin::Views::IndexAsTable::IndexTableFor, collection, table_options) do |t| + selectable_column + id_column if resource_class.primary_key + active_admin_config.resource_columns.each do |attribute| + column attribute + end + actions +end diff --git a/app/views/active_admin/resource/_index_blank_slate.html.erb b/app/views/active_admin/resource/_index_blank_slate.html.erb new file mode 100644 index 00000000000..826f6add9ad --- /dev/null +++ b/app/views/active_admin/resource/_index_blank_slate.html.erb @@ -0,0 +1,14 @@ +
+

+ <%= I18n.t("active_admin.blank_slate.content", resource_name: active_admin_config.plural_resource_label) %> +

+ <% if new_action_authorized?(active_admin_config.resource_class) %> + <%= if page_presenter.options.has_key?(:blank_slate_link) + link = page_presenter.options[:blank_slate_link] + instance_exec(&link) if link.is_a?(Proc) + else + link_to(I18n.t("active_admin.blank_slate.link"), new_resource_path) + end + %> + <% end %> +
diff --git a/app/views/active_admin/resource/_index_empty_results.html.erb b/app/views/active_admin/resource/_index_empty_results.html.erb new file mode 100644 index 00000000000..425fe865d3b --- /dev/null +++ b/app/views/active_admin/resource/_index_empty_results.html.erb @@ -0,0 +1,5 @@ +
+

+ <%= I18n.t("active_admin.pagination.empty", model: active_admin_config.plural_resource_label) %> +

+
diff --git a/app/views/active_admin/resource/_index_table_actions_default.html.erb b/app/views/active_admin/resource/_index_table_actions_default.html.erb new file mode 100644 index 00000000000..109bbd32c55 --- /dev/null +++ b/app/views/active_admin/resource/_index_table_actions_default.html.erb @@ -0,0 +1,9 @@ +<% if show_action_authorized?(resource) %> + <%= link_to view_label, resource_path(resource) %> +<% end %> +<% if edit_action_authorized?(resource) %> + <%= link_to edit_label, edit_resource_path(resource) %> +<% end %> +<% if destroy_action_authorized?(resource) %> + <%= link_to delete_label, resource_path(resource), method: :delete, data: { confirm: delete_confirmation_text } %> +<% end %> diff --git a/app/views/active_admin/resource/_show_default.html.arb b/app/views/active_admin/resource/_show_default.html.arb new file mode 100644 index 00000000000..81df5c22ea6 --- /dev/null +++ b/app/views/active_admin/resource/_show_default.html.arb @@ -0,0 +1,2 @@ +attributes_table_for(resource, *active_admin_config.resource_columns) +active_admin_comments_for(resource) if active_admin_config.comments? diff --git a/app/views/active_admin/resource/edit.html.arb b/app/views/active_admin/resource/edit.html.arb index e3c95a4972a..8c79d16ebce 100644 --- a/app/views/active_admin/resource/edit.html.arb +++ b/app/views/active_admin/resource/edit.html.arb @@ -1 +1 @@ -insert_tag renderer_for(:edit) +render "form" diff --git a/app/views/active_admin/resource/index.csv.erb b/app/views/active_admin/resource/index.csv.erb deleted file mode 100644 index a76251737bc..00000000000 --- a/app/views/active_admin/resource/index.csv.erb +++ /dev/null @@ -1,23 +0,0 @@ -<%- - csv_lib = if RUBY_VERSION =~ /^1.8/ - require 'fastercsv' - FasterCSV - else - require 'csv' - CSV - end - - default = active_admin_application.csv_options - options = default.merge active_admin_config.csv_builder.options - columns = active_admin_config.csv_builder.columns - - csv_output = csv_lib.generate(options) do |csv| - csv << columns.map(&:name) - collection.each do |resource| - csv << columns.map do |column| - call_method_or_proc_on resource, column.data - end - end - end -%> -<%= csv_output.html_safe %> diff --git a/app/views/active_admin/resource/index.html.arb b/app/views/active_admin/resource/index.html.arb index cfd4e2ef8ef..27b7bf89ac6 100644 --- a/app/views/active_admin/resource/index.html.arb +++ b/app/views/active_admin/resource/index.html.arb @@ -1 +1,94 @@ -insert_tag renderer_for(:index) +def wrap_with_batch_action_form(&block) + if active_admin_config.batch_actions.any? + insert_tag(ActiveAdmin::BatchActions::BatchActionForm, &block) + batch_actions_to_display.each do |batch_action| + if batch_action.partial.present? + render(batch_action.partial) + end + end + else + block.call + end +end + +def build_collection + if collection_empty?(collection) + if params[:q] || params[:scope] + render("active_admin/resource/index_empty_results") + else + render("active_admin/resource/index_blank_slate") + end + else + render_index + end +end + +def build_table_tools + div class: "index-data-table-toolbar" do + render "batch_actions_dropdown" + build_scopes + build_index_list + end if any_table_tools? +end + +def any_table_tools? + active_admin_config.batch_actions.any? || + active_admin_config.scopes.any? || + active_admin_config.page_presenters[:index].try(:size).try(:>, 1) +end + +def build_scopes + if active_admin_config.scopes.any? + scope_options = { scope_count: page_presenter.fetch(:scope_count, true) } + insert_tag(ActiveAdmin::Views::Scopes, active_admin_config.scopes, scope_options) + end +end + +def build_index_list + indexes = active_admin_config.page_presenters[:index] + + if indexes.kind_of?(Hash) && indexes.length > 1 + index_classes = [] + active_admin_config.page_presenters[:index].each do |type, page_presenter| + index_classes << find_index_renderer_class(page_presenter[:as]) + end + + insert_tag(ActiveAdmin::Views::IndexList, index_classes) + end +end + +# Returns the actual class for rendering the main content on the index +# page. To set this, use the :as option in the page_presenter block. +def find_index_renderer_class(klass) + if klass.is_a?(Class) + klass + else + ::ActiveAdmin::Views.const_get("IndexAs" + klass.to_s.camelcase) + end +end + +def render_index + renderer_class = find_index_renderer_class(page_presenter[:as]) + + paginator = page_presenter.fetch(:paginator, true) + download_links = page_presenter.fetch(:download_links, active_admin_config.namespace.download_links) + pagination_total = page_presenter.fetch(:pagination_total, true) + per_page = page_presenter.fetch(:per_page, active_admin_config.per_page) + + paginated_collection( + collection, entry_name: active_admin_config.resource_label, + entries_name: active_admin_config.plural_resource_label(count: collection_size), + download_links: download_links, + paginator: paginator, + per_page: per_page, + pagination_total: pagination_total) do + insert_tag(renderer_class, page_presenter, collection) + end +end + +div class: "main-content-container" do + wrap_with_batch_action_form do + build_table_tools + build_collection + end +end diff --git a/app/views/active_admin/resource/new.html.arb b/app/views/active_admin/resource/new.html.arb index c8ecdb218d3..8c79d16ebce 100644 --- a/app/views/active_admin/resource/new.html.arb +++ b/app/views/active_admin/resource/new.html.arb @@ -1 +1 @@ -insert_tag renderer_for(:new) +render "form" diff --git a/app/views/active_admin/resource/show.html.arb b/app/views/active_admin/resource/show.html.arb index 54bf04808f0..7e57b9268de 100644 --- a/app/views/active_admin/resource/show.html.arb +++ b/app/views/active_admin/resource/show.html.arb @@ -1 +1,12 @@ -insert_tag renderer_for(:show) +def attributes_table(*args, &block) + attributes_table_for resource, *args, &block +end + +div class: "main-content-container" do + if page_presenter.block + # Evaluate the show config from the controller + instance_exec resource, &page_presenter.block + else + render "show_default" + end +end diff --git a/app/views/active_admin/shared/_action_items.html.erb b/app/views/active_admin/shared/_action_items.html.erb new file mode 100644 index 00000000000..8f5a2620f34 --- /dev/null +++ b/app/views/active_admin/shared/_action_items.html.erb @@ -0,0 +1,3 @@ +<% action_items_for_action.each do |action_item| %> + <%= instance_exec(&action_item.block) %> +<% end %> diff --git a/app/views/active_admin/shared/_download_format_links.html.erb b/app/views/active_admin/shared/_download_format_links.html.erb new file mode 100644 index 00000000000..84c9c19f9c8 --- /dev/null +++ b/app/views/active_admin/shared/_download_format_links.html.erb @@ -0,0 +1,7 @@ +<% params = request.query_parameters.except :format, :commit %> +
+ <%= I18n.t("active_admin.download") %> + <% formats.each do |format| %> + <%= link_to format.upcase, url_for(params: params, format: format) %> + <% end %> +
diff --git a/app/views/active_admin/shared/_resource_comments.html.erb b/app/views/active_admin/shared/_resource_comments.html.erb new file mode 100644 index 00000000000..0b3cef1f8d9 --- /dev/null +++ b/app/views/active_admin/shared/_resource_comments.html.erb @@ -0,0 +1,51 @@ +
+
+ <%= ActiveAdmin::Comment.model_name.human(count: 2.1) %> +
+ <% if authorized?(ActiveAdmin::Auth::NEW, ActiveAdmin::Comment) %> + <%= active_admin_form_for(ActiveAdmin::Comment.new, url: comment_form_url, html: { class: "mb-12 max-w-[700px]", novalidate: false }) do |f| + f.inputs do + f.input :resource_type, as: :hidden, input_html: { value: ActiveAdmin::Comment.resource_type(resource) } + f.input :resource_id, as: :hidden, input_html: { value: resource.id } + f.input :body, label: false, input_html: { size: "80x4", required: true } + end + f.actions do + f.action :submit, label: I18n.t("active_admin.comments.add") + end + end + %> + <% end %> +
+ <%= I18n.t "active_admin.comments.title_content", count: comments.total_count %> +
+ <% if comments.any? %> + <% comments.each do |comment| %> +
+
+ + <%= comment.author ? auto_link(comment.author) : I18n.t("active_admin.comments.author_missing") %> + + + <%= pretty_format comment.created_at %> + +
+
+ <%= simple_format(comment.body) %> +
+ <% if authorized?(ActiveAdmin::Auth::DESTROY, comment) %> + <%= link_to I18n.t("active_admin.comments.delete"), url_for_comments(comment.id, return_to: "back"), method: :delete, data: { confirm: I18n.t("active_admin.comments.delete_confirmation") } %> + <% end %> +
+ <% end %> +
+
+ <%= page_entries_info(comments).html_safe %> +
+ <%= paginate(comments, views_prefix: :active_admin, outer_window: 1, window: 2) %> +
+ <% else %> +
+ <%= I18n.t("active_admin.comments.no_comments_yet") %> +
+ <% end %> +
diff --git a/app/views/active_admin/shared/_sidebar_section.html.arb b/app/views/active_admin/shared/_sidebar_section.html.arb new file mode 100644 index 00000000000..ba44b5786f8 --- /dev/null +++ b/app/views/active_admin/shared/_sidebar_section.html.arb @@ -0,0 +1,6 @@ +if section.block + result = instance_exec(§ion.block) + text_node result unless result.is_a?(Arbre::Element) +else + render(section.partial_name) +end diff --git a/app/views/active_admin/shared/_sidebar_sections.html.erb b/app/views/active_admin/shared/_sidebar_sections.html.erb new file mode 100644 index 00000000000..253037d07eb --- /dev/null +++ b/app/views/active_admin/shared/_sidebar_sections.html.erb @@ -0,0 +1,5 @@ +<% sidebar_sections_for_action.each do |section| %> + <%= content_tag :div, id: section.id, class: section.custom_class do %> + <%= render "active_admin/shared/sidebar_section", section: section %> + <% end %> +<% end %> diff --git a/app/views/layouts/active_admin.html.arb b/app/views/layouts/active_admin.html.arb deleted file mode 100644 index 906ecd3317a..00000000000 --- a/app/views/layouts/active_admin.html.arb +++ /dev/null @@ -1 +0,0 @@ -insert_tag view_factory.layout diff --git a/app/views/layouts/active_admin.html.erb b/app/views/layouts/active_admin.html.erb new file mode 100644 index 00000000000..37d318e36e0 --- /dev/null +++ b/app/views/layouts/active_admin.html.erb @@ -0,0 +1,20 @@ + + + + <%= html_head_site_title %> + <%= render "active_admin/html_head" %> + + + <%= render "active_admin/site_header", title: site_title %> +
+ <%= render "active_admin/main_navigation" %> + <%= render "active_admin/page_header", title: @page_title || page_title %> + <%= render "active_admin/flash_messages" %> +
+ <%= yield %> + <%= render "active_admin/sidebar" %> +
+ <%= render "active_admin/site_footer" %> +
+ + diff --git a/app/views/layouts/active_admin_logged_out.html.erb b/app/views/layouts/active_admin_logged_out.html.erb index 0ea032854d6..f1211e928bf 100644 --- a/app/views/layouts/active_admin_logged_out.html.erb +++ b/app/views/layouts/active_admin_logged_out.html.erb @@ -1,35 +1,13 @@ - - + + - - - <%= [@page_title, render_or_call_method_or_proc_on(self, ActiveAdmin.application.site_title)].compact.join(" | ") %> - - <% ActiveAdmin.application.stylesheets.each do |style, options| %> - <%= stylesheet_link_tag style, options %> - <% end %> - <% ActiveAdmin.application.javascripts.each do |path| %> - <%= javascript_include_tag path %> - <% end %> - - <%= csrf_meta_tag %> + <%= html_head_site_title %> + <%= render "active_admin/html_head" %> - -
- -
- <% if active_admin_flash_messages.any? %> - <% active_admin_flash_messages.each do |type, message| %> - <%= content_tag :div, message, :class => "flash flash_#{type}" %> - <% end %> - <% end %> -
- <%= yield %> -
+ +
+ <%= render "active_admin/flash_messages" %> + <%= yield %>
- -
diff --git a/bin/bundle b/bin/bundle new file mode 100755 index 00000000000..9004a197a1f --- /dev/null +++ b/bin/bundle @@ -0,0 +1,7 @@ +#!/bin/bash + +( set -x; bundle $@ ) + +for gemfile in gemfiles/*/Gemfile; do + ( set -x; BUNDLE_GEMFILE="$gemfile" bundle $@ ) +done diff --git a/bin/cucumber b/bin/cucumber new file mode 100755 index 00000000000..af310ef9179 --- /dev/null +++ b/bin/cucumber @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" + +load Gem.bin_path("cucumber", "cucumber") diff --git a/bin/i18n-tasks b/bin/i18n-tasks new file mode 100755 index 00000000000..997e803cf83 --- /dev/null +++ b/bin/i18n-tasks @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" + +load Gem.bin_path("i18n-tasks", "i18n-tasks") diff --git a/bin/parallel_cucumber b/bin/parallel_cucumber new file mode 100755 index 00000000000..352dbffbdc5 --- /dev/null +++ b/bin/parallel_cucumber @@ -0,0 +1,10 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" + +["--serialize-stdout", "--combine-stderr", "--verbose"].each do |flag| + ARGV << flag unless ARGV.include?(flag) +end + +load Gem.bin_path("parallel_tests", "parallel_cucumber") diff --git a/bin/parallel_rspec b/bin/parallel_rspec new file mode 100755 index 00000000000..96e708eb81e --- /dev/null +++ b/bin/parallel_rspec @@ -0,0 +1,10 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" + +%w[--serialize-stdout --combine-stderr --verbose].each do |flag| + ARGV << flag unless ARGV.include?(flag) +end + +load Gem.bin_path("parallel_tests", "parallel_rspec") diff --git a/bin/prep-release b/bin/prep-release new file mode 100755 index 00000000000..aff6972e2b3 --- /dev/null +++ b/bin/prep-release @@ -0,0 +1,34 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Provide version in Ruby format. NPM format will be handled automatically. +# +# > bin/prep-release 1.2.3 +# > bin/prep-release 2.0.0.beta1 + +version = ARGV[0] + +if version.nil? || !version.match?(/\d+\.\d+\.\d+\.?[\w\d]*/) + puts "Error: Missing or invalid version." + puts "Usage: bin/prep-release [version]" + exit +end + +def bump_version_file(version) + file = "lib/active_admin/version.rb" + new_content = File.read(file).gsub!(/VERSION = ".*"/, "VERSION = \"#{version}\"") + File.open(file, "w") { |f| f.puts new_content } +end + +def bump_npm_package(version) + # See https://github.com/rails/rails/blob/0d0c30e534af7f80ec8b18eb946aaa613ca30444/tasks/release.rb#L26 + npmified_version = version.gsub(/\./).with_index { |s, i| i == 2 ? "-" : s } + system "npm", "version", npmified_version, "--no-git-tag-version", exception: true +end + +bump_version_file(version) +system "bin/bundle install" +bump_npm_package(version) +system "npm ci" +system "bin/rake", "dependencies:vendor" +system "npm run build" diff --git a/bin/rake b/bin/rake new file mode 100755 index 00000000000..ffd35240102 --- /dev/null +++ b/bin/rake @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" + +load Gem.bin_path("rake", "rake") diff --git a/bin/rspec b/bin/rspec new file mode 100755 index 00000000000..c2ff6abc5e0 --- /dev/null +++ b/bin/rspec @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" + +load Gem.bin_path("rspec-core", "rspec") diff --git a/bin/rubocop b/bin/rubocop new file mode 100755 index 00000000000..330eb080c4b --- /dev/null +++ b/bin/rubocop @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" + +load Gem.bin_path("rubocop", "rubocop") diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000000..fe9d8319788 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,9 @@ +coverage: + status: + project: + default: + threshold: 0.1% +ignore: + - spec/**/* + - tmp/**/* + - vendor/**/* diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml new file mode 100644 index 00000000000..b1079837e44 --- /dev/null +++ b/config/i18n-tasks.yml @@ -0,0 +1,41 @@ +# The "main" locale +base_locale: en + +# Read and write translations +data: + yaml: + write: + # Do not wrap lines at 80 characters + line_width: -1 + +# Find translate calls +search: + # Paths or `File.find` patterns to search in + paths: + - app + - lib + + # Files or `File.fnmatch` patterns to exclude from search + exclude: + - app/assets/images + - lib/generators + - tasks/tmp + + # Guess usages such as t("categories.#{category}.title") + strict: false + +ignore_inconsistent_interpolations: + - active_admin.new_model + - active_admin.edit_model + - active_admin.delete_model + - active_admin.details + - active_admin.has_many_new + - active_admin.pagination.empty + - active_admin.pagination.one + - active_admin.pagination.one_page + - active_admin.pagination.multiple + - active_admin.pagination.multiple_without_total + - active_admin.blank_slate.content + +ignore_missing: + - errors.messages.not_saved # Devise diff --git a/config/importmap.rb b/config/importmap.rb new file mode 100644 index 00000000000..b381620f92b --- /dev/null +++ b/config/importmap.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true +pin "flowbite", preload: true # downloaded from https://cdn.jsdelivr.net/npm/flowbite@3.1.2/dist/flowbite.min.js +pin "@rails/ujs", to: "rails_ujs_esm.js", preload: true # downloaded from https://cdn.jsdelivr.net/npm/@rails/ujs@7.1.501/+esm +pin "active_admin", to: "active_admin.js", preload: true +pin_all_from File.expand_path("../app/javascript/active_admin", __dir__), under: "active_admin", preload: true diff --git a/config/locales/ar.yml b/config/locales/ar.yml new file mode 100644 index 00000000000..fa25d1e5164 --- /dev/null +++ b/config/locales/ar.yml @@ -0,0 +1,148 @@ +--- +ar: + active_admin: + access_denied: + message: غير مصرح لك تنفيذ هذا الإجراء. + any: أي + batch_actions: + action_label: اُختير %{title} + button_label: إجراء جماعي + default_confirmation: هل أنت متأكّد؟ + delete_confirmation: هل أنت متأكّد من حذف هذه %{plural_model}؟ + labels: + destroy: حذف + selection_toggle_explanation: "(تبديل التحديد)" + successfully_destroyed: + one: حُذف بنجاح %{model} + other: حُذف بنجاح %{count} %{plural_model} + blank_slate: + content: لا يوجد %{resource_name} + link: إنشاء + cancel: إلغاء + comments: + add: إضافة تعليق + author: مؤلّف + author_missing: المؤلف مجهول + author_type: نوع الؤلّف + body: المحتوى + created_at: أُنشئ + delete: حذف تعليق + delete_confirmation: هل أنت متأكّد من حذف التعليق؟ + errors: + empty_text: لم يُحفظ التعليق، النص فارغ. + no_comments_yet: لا يوجد تعليقات. + resource: مدخل + resource_type: نوع المصدر + title_content: التعليقات (%{count}) + create_another: انشاء %{model} آخر + dashboard: لوحة التحكم + delete: حذف + delete_confirmation: هل تريد تأكيد الحذف؟ + delete_model: حذف %{model} + details: تفاصيل %{model} + devise: + change_password: + submit: تغير كلمة المرور + title: تغير كلمة المرور + email: + title: البريد الإلكترونيّ + links: + forgot_your_password: هل نسيت كلمة المرور؟ + resend_confirmation_instructions: إعادة إرسال تعليمات تأكيد البريد الإلكتروني + resend_unlock_instructions: إعادة إرسال تعليمات تنشيط الحساب + sign_in: تسجيل الدخول + sign_in_with_omniauth_provider: تسجيل الدخول بـ %{provider} + sign_up: التسجيل + login: + remember_me: تذكرني + submit: تسجيل الدخول + title: تسجيل الدخول + password: + title: كلمة المرور + password_confirmation: + title: تأكيد كلمة المرور + resend_confirmation_instructions: + submit: إعادة ارسال تعليمات تأكيد البريد الإلكتروني + title: إعادة ارسال تعليمات تأكيد البريد الإلكتروني + reset_password: + submit: استرجاع كلمة المرور + title: هل نسيت كلمة المرور؟ + sign_up: + submit: تسجيل + title: التسجيل + subdomain: + title: النطاق الفرعي + unlock: + submit: إعادة إرسال تعليمات تنشيط الحساب + title: إعادة إرسال تعليمات تنشيط الحساب + username: + title: اسم المستخدم + download: تحميل + edit: تعديل + edit_model: تعديل %{model} + empty: فارغ + filters: + buttons: + clear: إلغاء الفرز + filter: فرز + predicates: + from: من + to: إلى + has_many_delete: حذف + has_many_new: إضافة %{model} جديد + has_many_remove: إزالة + index_list: + table: جدول + logout: تسجيل الخروج + move: نقل + new_model: "%{model} جديد" + next: التالي + pagination: + empty: لا يوجد %{model} + entry: + one: مدخل + other: مدخلات + multiple: عرض %{from}-%{to} من %{total} + multiple_without_total: عرض %{from}-%{to} + next: التالي + one: عرض 1 من 1 + one_page: عرض كل %{n} + per_page: 'لكل صفحة ' + previous: السابق + truncate: "…" + powered_by: بواسطة %{active_admin} %{version} + previous: السابق + scopes: + all: الكل + search_status: + no_current_filters: بدون فرز + title: الفرز الحالي + title_with_scope: الفرز الحالي لـ %{name} + sidebars: + filters: المُرشحات + search_status: حالة البحث + status_tag: + 'no': لا + unset: غير محدد + 'yes': نعم + toggle_dark_mode: تبديل الوضع الليلي + toggle_main_navigation_menu: عرض القائمة الرئيسية + toggle_section: عرض القسم + toggle_user_menu: عرض قائمة المستخدم + view: عرض + activerecord: + attributes: + active_admin/comment: + author_type: نوع الكاتب + body: المحتوى + created_at: وقت الإنشاء + namespace: النطاق + resource_type: نوع المصدر + updated_at: وقت التعديل + models: + active_admin/comment: + one: تعليق + other: تعليقات + comment: + one: تعليق + other: تعليقات diff --git a/config/locales/az.yml b/config/locales/az.yml new file mode 100644 index 00000000000..a85b3400ddf --- /dev/null +++ b/config/locales/az.yml @@ -0,0 +1,116 @@ +--- +az: + active_admin: + access_denied: + message: Bunu etmək üçün daxil olmalısınız. + any: İstənilən + batch_actions: + action_label: "%{title} seçilmiş" + button_label: Qrup əməliyyatları + default_confirmation: Siz bunu etməyinizə əminsiniz? + delete_confirmation: Siz %{plural_model} silməyə əminsiniz? + labels: + destroy: Sil + selection_toggle_explanation: "(Hamısını seç / Seçilmişləri sıfırla)" + successfully_destroyed: + few: 'Uğurla silindi: %{count} %{plural_model}' + many: 'Uğurla silindi: %{count} %{plural_model}' + one: 'Uğurla silindi: 1 %{model}' + other: 'Uğurla silindi: %{count} %{plural_model}' + blank_slate: + content: "%{resource_name} hələ yoxdur." + link: Yarat + cancel: İmtina + comments: + add: Şərh əlavə et + author: Müəllif + author_missing: Naməlum + author_type: Müəllifin tipi + body: Mətn + created_at: Yaranma tarixi + delete: Şərhi sil + delete_confirmation: Siz bu şərhi silmək istədiyinizdən əminsiniz? + errors: + empty_text: Şərh yadda saxlanılmadı, mətn boş ola bilməz. + no_comments_yet: Hələ şərhlər yoxdur. + resource: Resurs + resource_type: Resursun tipi + title_content: Şərhlər (%{count}) + dashboard: İdarəetmə paneli + delete: Sil + delete_confirmation: Siz bunu silmək istədiyinizdən əminsiniz? + delete_model: "%{model} sil" + details: "%{model} haqqında" + devise: + change_password: + submit: Şifrəni dəyiş + title: Şifrənin dəyişdirilməsi + email: + title: E-poçt + links: + forgot_your_password: Şifrəni unutmusunuz? + resend_confirmation_instructions: Aktivləşdirmə ismarışını yenidən göndərilməsi + resend_unlock_instructions: Blokdan çıxarma üzrə təlimatı yenidən göndərilməsi + sign_in: Giriş + sign_in_with_omniauth_provider: "%{provider} vasitəsilə daxil ol" + sign_up: Qeydiyyat + login: + remember_me: Məni yadda saxla + submit: Daxil ol + title: Giriş + password: + title: Şifrə + resend_confirmation_instructions: + submit: Aktivləşdirmə ismarışını yenidən göndərmək + title: Aktivləşdirmə ismarışını yenidən göndərmək + reset_password: + submit: Şifrəni sıfırla + title: Şifrəni unutmusunuz? + sign_up: + submit: Qeydiyyatdan keç + title: Qeydiyyat + subdomain: + title: Subdomen + unlock: + submit: Blokdan çıxarma üzrə təlimatı yenidən göndərmək + title: Blokdan çıxarma üzrə təlimatı yenidən göndərmək + username: + title: İstifadəçi adı + download: 'Yüklənmə:' + edit: Dəyiş + edit_model: "%{model} dəyiş" + empty: Boş + filters: + buttons: + clear: Təmizlə + filter: Filtrlə + has_many_delete: Sil + has_many_new: "%{model} əlavə et" + has_many_remove: Yığışdır + index_list: + table: Cədvəl + logout: Çıxış + new_model: "%{model} yarat" + next: İrəli + pagination: + empty: "%{model} tapılmadı" + entry: + few: yazı + many: yazı + one: yazı + other: yazı + multiple: 'Nəticə: %{model} %{from} - %{to} %{total}' + multiple_without_total: 'Nəticə: %{model} %{from} - %{to}' + one: 'Nəticə: 1 %{model}' + one_page: 'Nəticə: %{n} %{model}' + powered_by: Работает на %{active_admin} %{version} + previous: Geri + search_status: + no_current_filters: Heç biri + sidebars: + filters: Filterlə + search_status: Axtarışın statusu + status_tag: + 'no': Xeyr + 'yes': Bəli + view: Aç diff --git a/config/locales/bg.yml b/config/locales/bg.yml index 7b3820d17c5..36dd5679e70 100644 --- a/config/locales/bg.yml +++ b/config/locales/bg.yml @@ -1,100 +1,104 @@ +--- bg: active_admin: - dashboard: Табло - dashboard_welcome: - welcome: "Добре дошли в Active Admin. Това е таблото по подразбиране." - call_to_action: "За да добавите секции, редактирайте 'app/admin/dashboard.rb'" - view: "Преглед" - edit: "Редакция" - delete: "Изтриване" - delete_confirmation: "Сигурни ли сте, че искате да изтриете това?" - new_model: "Създаване на %{model}" - create_model: "Създаване на %{model}" - edit_model: "Редакция на %{model}" - update_model: "Обновяване на %{model}" - delete_model: "Изтриване на %{model}" - details: "%{model} детайли" - cancel: "Отказ" - empty: "Празно" - previous: "Предишно" - next: "Следващо" - download: "Изтегляне:" - has_many_new: "Добавяне на %{model}" - has_many_delete: "Изтриване" - has_many_remove: "Премахване" - filters: - buttons: - filter: "Филтриране" - clear: "Изчистване" - predicates: - contains: "съдържа" - equals: "равно на" - starts_with: "Започва с" - ends_with: "Завършва с" - greater_than: "по-голямо от" - less_than: "по-малко от" - main_content: "Добавете %{model}#main_content за да видите съдържание." - logout: "Изход" - powered_by: "Задвижва се от %{active_admin} %{version}" - sidebars: - filters: "Филтри" - pagination: - empty: "Не са намерени %{model}" - one: "Показване на 1 %{model}" - one_page: "Показване на всички %{n} %{model}" - multiple: "Показване %{model} %{from} - %{to} от общо %{total}" - entry: - one: "запис" - other: "записи" - any: "Без значение" - blank_slate: - content: "Все още няма добавени %{resource_name}." - link: "Създаване" + access_denied: + message: Нямате права да извършите това действие. + any: Без значение batch_actions: - button_label: "Масови действия" - delete_confirmation: "Сигурни ли сте, че искате да изтриете тези %{plural_model}? Това действие не може да бъде върнато назад." - succesfully_destroyed: - one: "Успешно изтриване на 1 %{model}" - other: "Успешно изтриване на %{count} %{plural_model}" - selection_toggle_explanation: "(Инвертиране на маркирането)" - link: "Създаване" action_label: "%{title} избран" + button_label: Масови действия + default_confirmation: Наистина ли искате да направите това? + delete_confirmation: Сигурни ли сте, че искате да изтриете тези %{plural_model}? labels: - destroy: "Изтриване" + destroy: Изтриване + selection_toggle_explanation: "(Инвертиране на маркирането)" + successfully_destroyed: + one: Успешно изтриване на 1 %{model} + other: Успешно изтриване на %{count} %{plural_model} + blank_slate: + content: Все още няма добавени %{resource_name}. + link: Създаване + cancel: Отказ comments: - resource_type: "Тип ресурс" - author_type: "Тип автор" - body: "Текст" - author: "Автор" - title: "Коментар" - add: "Добавяне на коментар" - resource: "Ресурс" - no_comments_yet: "Все още няма коментари." - title_content: "Коментари (%{count})" + add: Добавяне на коментар + author: Автор + author_missing: Анонимен + author_type: Тип автор + body: Текст errors: - empty_text: "Коментарът с празен текст не беше запазен." + empty_text: Коментарът с празен текст не беше запазен. + no_comments_yet: Все още няма коментари. + resource: Ресурс + resource_type: Тип ресурс + title_content: Коментари (%{count}) + dashboard: Табло + delete: Изтриване + delete_confirmation: Сигурни ли сте, че искате да изтриете това? + delete_model: Изтриване на %{model} + details: "%{model} детайли" devise: + change_password: + submit: Промяна на паролата + title: Промяна на паролата + email: + title: Поща + links: + forgot_your_password: Забравена парола? + sign_in: Вход + sign_in_with_omniauth_provider: Влез с %{provider} login: - title: "Вход" - remember_me: "Запомни ме" - submit: "Вход" + remember_me: Запомни ме + submit: Вход + title: Вход + password: + title: Парола + resend_confirmation_instructions: + submit: Изпрати отново инструкциите за потвърждаване + title: Изпрати отново инструкциите за потвърждаване reset_password: - title: "Забравена парола?" - submit: "Изпращане на нова парола" - change_password: - title: "Промяна на моята парола" - submit: "Промяна на паролата" + submit: Изпращане на нова парола + title: Забравена парола? + sign_up: + submit: Регистрация + title: Регистрация + subdomain: + title: Поддомейн unlock: - title: "Изпрати отново инструкциите за отключване" - submit: "Изпрати инструкциите повторно" - links: - sign_in: "Вход" - forgot_your_password: "Забравена парола?" - sign_in_with_omniauth_provider: "Вход с %{provider}" - access_denied: - message: "Нямате права да извършите това действие." + submit: Изпрати отново инструкциите за отключване + title: Изпрати отново инструкциите за отключване + username: + title: Потребителско име + download: 'Изтегляне:' + edit: Редакция + edit_model: Редакция на %{model} + empty: Празно + filters: + buttons: + clear: Изчистване + filter: Филтриране + has_many_delete: Изтриване + has_many_new: Добавяне на %{model} + has_many_remove: Премахване index_list: - table: "Таблица" - block: "Списък" - grid: "Грид" - blog: "Блог" + table: Таблица + logout: Изход + new_model: Създаване на %{model} + next: Следващо + pagination: + empty: Не са намерени %{model} + entry: + one: запис + other: записи + multiple: Показване %{model} %{from} - %{to} от общо %{total} + multiple_without_total: Показване %{model} %{from} - %{to} + one: Показване на 1 %{model} + one_page: Показване на всички %{n} %{model} + powered_by: Задвижва се от %{active_admin} %{version} + previous: Предишно + sidebars: + filters: Филтри + status_tag: + 'no': не + unset: не + 'yes': Да + view: Преглед diff --git a/config/locales/bs.yml b/config/locales/bs.yml new file mode 100644 index 00000000000..2204cf36b16 --- /dev/null +++ b/config/locales/bs.yml @@ -0,0 +1,108 @@ +--- +bs: + active_admin: + access_denied: + message: Nemaš dopuštenja. + any: Bilo koji + batch_actions: + action_label: "%{title} označene" + button_label: Grupne akcije + default_confirmation: Jeste li sigurni da želite to učiniti? + delete_confirmation: Jeste li sigurni da želite obrisati %{plural_model}? + labels: + destroy: Obriši + selection_toggle_explanation: "(Izmijeni odabir)" + successfully_destroyed: + few: Uspješno su obrisana %{count} %{plural_model} + many: Uspješno je obrisano %{count} %{plural_model} + one: Uspješno je obrisan 1 %{model} + other: Uspješno je obrisano %{count} %{plural_model} + blank_slate: + content: Još uvijek ne postoji niti jedan zapis tipa %{resource_name}. + link: Izradi jedan + cancel: Odustani + comments: + add: Dodaj komentar + author: Autor + author_missing: Anoniman + author_type: Tip autora + body: Sadržaj + errors: + empty_text: Komentar nije spremljen, sadržaj je prazan. + no_comments_yet: Još nema komentara. + resource: Objekt + resource_type: Tip objekta + title_content: Komentari (%{count}) + dashboard: Upravljačka ploča + delete: Obriši + delete_confirmation: Jeste li sigurni da želite ovo obrisati? + delete_model: Obriši %{model} + details: "%{model} detalji" + devise: + change_password: + submit: Izmijeni lozinku + title: Izmjena lozinke + email: + title: Email + links: + forgot_your_password: Zaboravljena lozinka? + sign_in: Prijavi se + sign_in_with_omniauth_provider: Prijavite se za %{provider} + login: + remember_me: Zapamti me + submit: Prijavi se + title: Prijava + password: + title: Lozinka + resend_confirmation_instructions: + submit: Pošalji + title: Ponovno slanje uputstva za potvrdu + reset_password: + submit: Resetuj lozinku + title: Zaboravljena lozinka? + sign_up: + submit: Registruj + title: Registracija + subdomain: + title: Poddomena + unlock: + submit: Pošalji + title: Ponovno slanje uputstva za otključavanje + username: + title: Korisničko ime + download: 'Spremi na računalo:' + edit: Uredi + edit_model: Uredi %{model} + empty: Prazno + filters: + buttons: + clear: Ukloni filtere + filter: Filtriraj + has_many_delete: Obriši + has_many_new: Dodaj novi %{model} + has_many_remove: Ukloniti + index_list: + table: Tabela + logout: Odjavi se + new_model: Novi %{model} + next: Sljedeći + pagination: + empty: Nije pronađen niti jedan %{model}. + entry: + few: zapisa + many: zapisa + one: zapis + other: zapisa + multiple: Prikazani %{model} %{from} - %{to} od ukupno %{total} + multiple_without_total: Prikazani %{model} %{from} - %{to} + one: Prikazan 1 %{model} + one_page: Prikazano svih %{n} %{model} + powered_by: Powered by %{active_admin} %{version} + previous: Prethodni + sidebars: + filters: Filtriranje + status_tag: + 'no': Nema + unset: Nema + 'yes': Da + view: Pregledaj diff --git a/config/locales/ca.yml b/config/locales/ca.yml index 29f50aa265a..f3d14a694e7 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -1,97 +1,148 @@ -#encoding: utf-8 +--- ca: active_admin: - dashboard: Tauler - dashboard_welcome: - welcome: "Benvingut a Active Admin. Aquest és el tauler per defecte." - call_to_action: "Mira l'arxiu 'app/admin/dashboard.rb' per afegir seccions al tauler" - view: "Mostra" - edit: "Edita" - delete: "Elimina" - delete_confirmation: "Segur que vols eliminar-ho?" - new_model: "Crear %{model}" - edit_model: "Editar %{model}" - delete_model: "eliminar %{model}" - details: "Detalls de %{model}" - cancel: "Cancel·lar" - empty: "Buit" - previous: "Anterior" - next: "Següent" - download: "Descarregar:" - has_many_new: "Afegir %{model}" - has_many_delete: "Eliminar" - has_many_remove: "Treure" - filters: - buttons: - filter: "Filtrar" - clear: "Treure filtres" - predicates: - contains: "Conté" - equals: "Igual a" - starts_with: "Comença amb" - ends_with: "Acaba amb" - greater_than: "Més gran que" - less_than: "Més petit que" - main_content: "Implementa %{model}#main_content per mostrar contingut." - logout: "Desconnecta't" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Filtres" - pagination: - empty: "No hi ha %{model}" - one: "S'està mostrant 1 %{model}" - one_page: "S'estan mostrant tots %{n} %{model}" - multiple: "S'estan mostrant %{model} %{from} - %{to} de %{total} en total" - entry: - one: "entrada" - other: "entrades" - any: "Qualsevol" - blank_slate: - content: "Encara no hi ha cap %{resource_name}." - link: "Crea'n un/a" + access_denied: + message: No esteu autoritzats a realitzar aquesta acció + any: Qualsevol batch_actions: - button_label: "les accions per lots" - delete_confirmation: "¿Està segur que desitja eliminar aquests %{plural_model}? Vostè no serà capaç de desfer això." - succesfully_destroyed: - one: "Va destruir amb èxit 1 %{model}" - other: "Va destruir amb èxit %{count} %{plural_model}" - selection_toggle_explanation: "(Selecció de Canviar)" - link: "crear una" action_label: "%{title} seleccionat" + button_label: Accions per lots + default_confirmation: Segur que voleu fer-ho? + delete_confirmation: Segurs que voleu eliminar aquests %{plural_model}? labels: - destroy: "esborrar" + destroy: Esborrar + selection_toggle_explanation: "(Invertir la selecció)" + successfully_destroyed: + one: 1 %{model} eliminat + other: "%{count} %{plural_model} eliminats" + blank_slate: + content: Encara no hi ha cap %{resource_name}. + link: Crea'n un/a + cancel: Cancel·lar comments: - body: "Cos" - author: "autor" - title: "comentari" - add: "Afegeix comentari" - resource: "Recurs" - no_comments_yet: "No hi ha comentaris" - title_content: "comentaris (%{count})" + add: Afegeix comentari + author: Autor + author_missing: Anònim + author_type: Tipus d'author + body: Missatge + created_at: Creat el + delete: Elimina comentari + delete_confirmation: Esteu segurs que voleu eliminar aquest comentari? errors: - empty_text: "El comentari no es va salvar, el text estava buida." + empty_text: El comentari no s'ha desat, no hi havia text. + no_comments_yet: Sense comentaris + resource: Recurs + resource_type: Tipus de recurs + title_content: Tots els comentaris (%{count}) + create_another: Crear un altre %{model} + dashboard: Tauler d'activitat + delete: Elimina + delete_confirmation: Segur que voleu eliminar-ho? + delete_model: Eliminar %{model} + details: Detalls de %{model} devise: + change_password: + submit: Canvia'm la contrasenya + title: Canvieu la contrasenya + email: + title: Email + links: + forgot_your_password: Heu perdut la contrasenya? + resend_confirmation_instructions: Reenviar les instruccions de confirmació + resend_unlock_instructions: Reenviar les instruccions de desbloqueig + sign_in: Sign in + sign_in_with_omniauth_provider: Identificació via %{provider} + sign_up: Sign up login: - title: "iniciar sessió" - remember_me: "Recordar" - submit: "iniciar sessió" + remember_me: Recorda'm + submit: Identifiqueu-vos + title: Identifiqueu-vos + password: + title: Contrasenya + password_confirmation: + title: Confirmeu la contrasenya + resend_confirmation_instructions: + submit: Reenviar instruccions de confirmació + title: Reenviar instruccions de confirmació reset_password: - title: "Heu perdut la contrasenya?" - submit: "Restablir la contrasenya" - change_password: - title: "Canvieu la contrasenya" - submit: "Canviar la contrasenya" + submit: Restablir la contrasenya + title: Heu oblidat la contrasenya? + sign_up: + submit: Doneu-vos d'alta + title: Doneu-vos d'alta + subdomain: + title: Subdomini unlock: - title: "Reenvia instruccions per a desbloquejar" - submit: "Reenvia instruccions per a desbloquejar" - links: - sign_in: "Registrar" - forgot_your_password: "Heu perdut la contrasenya?" - sign_in_with_omniauth_provider: "Connecta't amb %{provider}" - access_denied: - message: "No esta autoritzat a realitzar aquesta acció." + submit: Reenvia instruccions per a desbloquejar + title: Reenvia instruccions per a desbloquejar + username: + title: Usuari + download: 'Descarregar:' + edit: Edita + edit_model: Editar %{model} + empty: Buit + filters: + buttons: + clear: Elimina els filtres + filter: Filtra + predicates: + from: Des de + to: Fins + has_many_delete: Eliminar + has_many_new: Afegir un altre %{model} + has_many_remove: Treure index_list: - table: "Taula" - block: "Llista" - grid: "Graella" - blog: "Bloc" + table: Taula + logout: Tanca la sessió + move: Moure + new_model: Crear %{model} + next: Següent + pagination: + empty: No s'ha trobat cap %{model} + entry: + one: entrada + other: entrades + multiple: Se n'estan mostrant %{from}-%{to} d'un total de %{total} + multiple_without_total: Se n'estan mostrant %{from}-%{to} + next: Següent + one: S'està mostrant 1 de 1 + one_page: S'estan mostrant tots %{n} + per_page: Per pàgina + previous: Anterior + truncate: "…" + powered_by: Powered by %{active_admin} %{version} + previous: Anterior + scopes: + all: Tots + search_status: + no_current_filters: Sense filtres actius + title: Cerca activa + title_with_scope: Cerca activa per %{name} + sidebars: + filters: Filtres + search_status: Estat de la cerca + status_tag: + 'no': 'No' + unset: Desconegut + 'yes': Sí + toggle_dark_mode: Activar el modus fosc + toggle_main_navigation_menu: Activar el menú principal de navegació + toggle_section: Activar la secció + toggle_user_menu: Activar el menú d'usuari + view: Mostra + activerecord: + attributes: + active_admin/comment: + author_type: Tipus d'autor + body: Missatge + created_at: Creat el + namespace: Espai de noms + resource_type: Tipus de recurs + updated_at: Actualitzat el + models: + active_admin/comment: + one: Comentari + other: Comentaris + comment: + one: Comentari + other: Comentaris diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 244f6ead851..7c1d90c2dea 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -1,91 +1,94 @@ +--- cs: active_admin: - dashboard: Úvod - dashboard_welcome: - welcome: "Vítejte v Active Admin. Toto je nástěnka." - call_to_action: "Pro přidání sekcí nástěnky se podívejte do 'app/admin/dashboard.rb'" - view: "Zobrazit" - edit: "Upravit" - delete: "Smazat" - delete_confirmation: "Jste si jistí, že chcete tuto položku smazat?" - new_model: "Vytvořit" - create_model: "Vytvořit %{model}" - edit_model: "Upravit" - update_model: "Upravit %{model}" - delete_model: "Smazat" - details: "Detaily" - cancel: "Zrušit" - empty: "Prázdné" - previous: "Předešlé" - next: "Následující" - download: "Stáhnout:" - has_many_new: "Přidat nový" - has_many_delete: "Odstranit" - has_many_remove: "Odstranit" - filters: - buttons: - filter: "Filtrovat" - clear: "Vyčistit filtry" - predicates: - contains: "Obsahuje" - equals: "Přesně" - starts_with: "Začíná se" - ends_with: "Končí" - greater_than: "Větší než" - less_than: "Menší než" - main_content: "Prosím implementujte %{model}#main_content pro zobrazení obsahu." - logout: "Odhlášení" - powered_by: "%{active_admin} %{version}" - sidebars: - filters: "Filtry" - pagination: - empty: "Nenalezen." - one: "Zobrazena 1 položka" - one_page: "Počet zobrazených položek %{n}" - multiple: "%{from} - %{to} z %{total}" - entry: - one: "položka" - few: "položky" - other: "položky" - any: "Kterákoliv" - blank_slate: - content: "Zatím zde není žádný obsah." - link: "Vytvořit" + access_denied: + message: Nemáte oprávnění k provedení této akce. + any: Kterákoliv batch_actions: - button_label: "Hromadné akce" - delete_confirmation: "Jste si jisti, že chcete odstranit tyto %{plural_model}?" - succesfully_destroyed: - one: "Úspěšně smazán %{model}" - few: "Úspěšně smazány %{count} %{plural_model}" - other: "Úspěšně smazáno %{count} %{plural_model}" - selection_toggle_explanation: "(Změnit výběr)" - link: "Vytvořit" action_label: "%{title}" + button_label: Hromadné akce + default_confirmation: Jste si jisti, že chcete provést? + delete_confirmation: Jste si jisti, že chcete smazat tyto %{plural_model}? labels: - destroy: "Vymazat" + destroy: Vymazat + selection_toggle_explanation: "(Změnit výběr)" + successfully_destroyed: + few: Úspěšně smazány %{count} %{plural_model} + one: Úspěšně smazán %{model} + other: Úspěšně smazáno %{count} %{plural_model} + zero: Nebyl smazán žádný %{model} + blank_slate: + content: Zatím zde není žádný obsah. + link: Vytvořit + cancel: Zrušit comments: - body: "Tělo" - author: "Autor" - title: "Komentář" - add: "Přidat komentář" - resource: "Zdroj" - no_comments_yet: "Žádný komentář" - title_content: "Komentáře administrátorů (%{count})" + add: Přidat komentář + author: Autor + author_missing: Anonymní + author_type: Typ autora + body: Tělo errors: - empty_text: "Komentář nebyl uložen, je prázdný." + empty_text: Komentář nebyl uložen, je prázdný. + no_comments_yet: Žádný komentář + resource: Zdroj + resource_type: Typ zdroje + title_content: Komentáře administrátorů (%{count}) + dashboard: Úvod + delete: Smazat + delete_confirmation: Jste si jistí, že chcete tuto položku smazat? + delete_model: Smazat + details: Detaily devise: - login: - title: "Přihlášení" - remember_me: "Zapamatovat si mě" - submit: "Přihlásit" - reset_password: - title: "Zapomněli jste heslo?" - submit: "Obnovit heslo" change_password: - title: "Změnit heslo" - submit: "Změnit své heslo" + submit: Změnit své heslo + title: Změnit heslo links: - sign_in: "Přihlásit se" - forgot_your_password: "Zapomněli jste heslo?" - sign_in_with_omniauth_provider: "Přihlásit se %{provider}" - + forgot_your_password: Zapomněli jste heslo? + sign_in: Přihlásit se + sign_in_with_omniauth_provider: Přihlásit se přes %{provider} + sign_up: Registrovat se + login: + remember_me: Zapamatovat si mě + submit: Přihlásit + title: Přihlášení + reset_password: + submit: Obnovit heslo + title: Zapomněli jste heslo? + unlock: + submit: Zaslat instrukce k odemčení účtu + title: Zaslání instrukcí k odemčení účtu + download: 'Stáhnout:' + edit: Upravit + edit_model: Upravit + empty: Prázdné + filters: + buttons: + clear: Vyčistit filtry + filter: Filtrovat + has_many_delete: Smazat + has_many_new: Přidat nový + has_many_remove: Odstranit + index_list: + table: Tabulka + logout: Odhlásit + new_model: Vytvořit + next: Následující + pagination: + empty: Nenalezen. + entry: + few: položky + one: položka + other: položky + multiple: "%{from} - %{to} z %{total}" + multiple_without_total: "%{from} - %{to}" + one: Zobrazena 1 položka + one_page: Počet zobrazených položek %{n} + powered_by: "%{active_admin} %{version}" + previous: Předchozí + sidebars: + filters: Filtry + status_tag: + 'no': Ne + unset: Ne + 'yes': Ano + view: Zobrazit diff --git a/config/locales/da.yml b/config/locales/da.yml index caa6e1b5b44..fab88913131 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -1,101 +1,115 @@ +--- da: active_admin: - dashboard: "Kontrolpanel" - dashboard_welcome: - welcome: "Velkommen til Active Admin. Dette er standardoversigtssiden." - call_to_action: "Rediger 'app/admin/dashboard.rb' for at tilføje nye elementer til oversigtssiden." - view: "Vis" - edit: "Rediger" - delete: "Slet" - delete_confirmation: "Er du sikker på at du ønsker at slette?" - new_model: "Ny(t) %{model}" - create_model: "Opret %{model}" - edit_model: "Rediger %{model}" - update_model: "Opdater %{model}" - delete_model: "Slet %{model}" - details: "%{model} detaljer" - cancel: "Fortryd" - empty: "Tom" - previous: "Forrige" - next: "Næste" - download: "Download:" - has_many_new: "Tilføj ny(t) %{model}" - has_many_delete: "Slet" - has_many_remove: "Fjern" - filters: - buttons: - filter: "Filtrer" - clear: "Ryd filtre" - predicates: - contains: "Indeholder" - equals: "lig" - starts_with: "Begynder med" - ends_with: "Slutter med" - greater_than: "større end" - less_than: "mindre end" - main_content: "Implementer venligst %{model}#main_content for at vise noget indhold." - logout: "Log ud" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Filtre" - pagination: - empty: "Ingen %{model} fundet" - one: "Viser 1 %{model}" - one_page: "Viser alle %{n} %{model}" - multiple: "Viser %{model} %{from} - %{to} af %{total} i alt" - entry: - one: "post" - other: "poster" - any: "Alle" - blank_slate: - content: "Der er ingen %{resource_name} endnu." - link: "Opret" + access_denied: + message: Du har ikke rettigheder til at udføre denne handling. + any: Alle batch_actions: - button_label: "Batch Handlinger" - delete_confirmation: "Er du sikker på du vil slette disse %{plural_model}? Du vil ikke være i stand til at fortryde dette." - succesfully_destroyed: - one: "Vellykket ødelagt 1 %{model}" - other: "Vellykket ødelagt %{count} %{plural_model}" - selection_toggle_explanation: "(Skift Selection)" - link: "Opret en" action_label: "%{title} Valgte" + button_label: Batch Handlinger + default_confirmation: Er du sikker på du vil gøre dette? + delete_confirmation: Er du sikker på du vil slette disse %{plural_model}? labels: - destroy: "Slet" + destroy: Slet + selection_toggle_explanation: "(Skift valg)" + successfully_destroyed: + one: Vellykket ødelagt 1 %{model} + other: Vellykket ødelagt %{count} %{plural_model} + blank_slate: + content: Der er ingen %{resource_name} endnu. + link: Opret + cancel: Fortryd comments: - resource_type: "resource type" - author_type: "forfatter type" - body: "krop" - author: "forfatter" - title: "Kommentar" - add: "Tilføj Kommentar" - resource: "Resource" - no_comments_yet: "Ingen kommentarer endnu." - title_content: "Kommentarer (%{count})" + add: Tilføj Kommentar + author: Forfatter + author_missing: Anonym + author_type: Forfatter type + body: Krop + created_at: Oprettet + delete: Slet kommentar + delete_confirmation: Er du sikker på du vil slette disse kommentarer? errors: - empty_text: "Kommentar blev ikke gemt, tekst var tom." + empty_text: Kommentar blev ikke gemt, tekst var tom. + no_comments_yet: Ingen kommentarer endnu. + resource: Resource + resource_type: Resource type + title_content: Kommentarer (%{count}) + create_another: Opret endnu en %{model} + dashboard: Kontrolpanel + delete: Slet + delete_confirmation: Er du sikker på, at du ønsker at slette? + delete_model: Slet %{model} + details: "%{model} detaljer" devise: + change_password: + submit: Skift min adgangskode + title: Skift din adgangskode + email: + title: Email + links: + forgot_your_password: Glemt din adgangskode? + resend_confirmation_instructions: Send oplåsningsinstruktioner igen + resend_unlock_instructions: Send oplåsningsinstruktioner igen + sign_in: Log ind + sign_in_with_omniauth_provider: Log ind med %{provider} + sign_up: Opret bruger login: - title: "Login" - remember_me: "Husk mig" - submit: "Login" + remember_me: Husk mig + submit: Login + title: Login + password: + title: Kodeord + resend_confirmation_instructions: + submit: Send bekræftigelsesinstruktioner igen + title: Send bekræftigelsesinstruktioner igen reset_password: - title: "Glemt din adgangskode?" - submit: "Nulstille min adgangskode" - change_password: - title: "Skift din adgangskode" - submit: "Skift min adgangskode" + submit: Nulstille min adgangskode + title: Glemt din adgangskode? + sign_up: + submit: Opret bruger + title: Opret bruger + subdomain: + title: Underdomæne unlock: - title: "Send oplåsnings instruktioner igen" - submit: "Send oplåsnings instruktioner igen" - links: - sign_in: "Log ind" - forgot_your_password: "Glemt din adgangskode?" - sign_in_with_omniauth_provider: "Log ind med %{provider}" - access_denied: - message: "Du har ikke rettigheder til at udføre denne handling." + submit: Send oplåsningsinstruktioner igen + title: Send oplåsningsinstruktioner igen + username: + title: Brugernavn + download: 'Download:' + edit: Rediger + edit_model: Rediger %{model} + empty: Tom + filters: + buttons: + clear: Ryd filtre + filter: Filtrer + has_many_delete: Slet + has_many_new: Tilføj ny(t) %{model} + has_many_remove: Fjern index_list: - table: "Tabel" - block: "Liste" - grid: "Gitter" - blog: "Blog" - + table: Tabel + logout: Log ud + new_model: Ny(t) %{model} + next: Næste + pagination: + empty: Ingen %{model} fundet + entry: + one: post + other: poster + multiple: Viser %{model} %{from} - %{to} af %{total} i alt + multiple_without_total: Viser %{model} %{from} - %{to} + one: Viser 1 %{model} + one_page: Viser alle %{n} %{model} + per_page: 'Per side: ' + powered_by: Powered by %{active_admin} %{version} + previous: Forrige + search_status: + no_current_filters: Ingen + sidebars: + filters: Filtre + search_status: Søgestatus + status_tag: + 'no': Nej + unset: Nej + 'yes': Ja + view: Vis diff --git a/config/locales/de-CH.yml b/config/locales/de-CH.yml deleted file mode 100644 index 2330e9be3d9..00000000000 --- a/config/locales/de-CH.yml +++ /dev/null @@ -1,116 +0,0 @@ -"de-CH": - active_admin: - dashboard: Übersicht - dashboard_welcome: - welcome: "Willkommen in Active Admin. Dies ist die Standard-Übersichtsseite." - call_to_action: "Siehe 'app/admin/dashboards.rb', um Übersichts-Bereiche hinzuzufügen." - view: "Anzeigen" - edit: "Bearbeiten" - delete: "Löschen" - delete_confirmation: "Wollen Sie dieses Element wirklich löschen?" - new_model: "%{model} erstellen" - create_model: "%{model} erstellen" - edit_model: "%{model} bearbeiten" - update_model: "%{model} bearbeiten" - delete_model: "%{model} löschen" - details: "%{model} Details" - cancel: "Abbrechen" - empty: "Leer" - previous: "Zurück" - next: "Weiter" - download: "Herunterladen:" - has_many_new: "%{model} hinzufügen" - has_many_delete: "Löschen" - has_many_remove: "Entfernen" - filters: - buttons: - filter: "Filtern" - clear: "Filter entfernen" - predicates: - contains: "Enthält" - equals: "Gleich" - starts_with: "Beginnt mit" - ends_with: "Endet mit" - greater_than: "Grösser als" - less_than: "Kleiner als" - main_content: "Bitte implementieren Sie %{model}#main_content, um Inhalte anzuzeigen." - logout: "Abmelden" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Filter" - pagination: - empty: "Keine %{model} gefunden" - one: "Zeige 1 %{model}" - one_page: "Zeige alle %{n} %{model}" - multiple: "Zeige %{model} %{from} – %{to} von %{total}" - entry: - one: "Eintrag" - other: "Einträge" - any: "Alle" - blank_slate: - content: "Es gibt noch keine %{resource_name}." - link: "Erstellen" - batch_actions: - button_label: "Stapelverarbeitung" - delete_confirmation: "Sind Sie sicher dass sie diese %{plural_model} löschen wollen? Dies kann nicht rückgängig gemacht werden" - succesfully_destroyed: - one: "Erfolgreich 1 %{model} gelöscht" - other: "Erfolgreich %{count} %{plural_model} gelöscht" - selection_toggle_explanation: "(Auswahl umschalten)" - link: "erstellen" - action_label: "%{title} ausgewählte" - labels: - destroy: "Lösche" - comments: - body: "Inhalt" - author: "Autor" - title: "Kommentar" - resource: "Resource" - add: "Kommentar hinzufügen" - no_comments_yet: "Es gibt noch keine Kommentare." - title_content: "Kommentare (%{count})" - errors: - empty_text: "Der Kommentar wurde nicht gespeichert, da der Text fehlt." - devise: - login: - title: "Login" - remember_me: "erinnere dich an mich" - submit: "Login" - reset_password: - title: "Passwort vergessen?" - submit: "Mein Passwort zurücksetzen" - change_password: - title: "Ändern Sie Ihr Passwort" - submit: "Mein Passwort ändern" - links: - sign_in: "Anmeldung" - forgot_your_password: "Passwort vergessen?" - sign_in_with_omniauth_provider: "Anmeldung mit %{provider}" - activerecord: - attributes: - admin_user: - id: ID - email: E-Mail - encrypted_Password: Verschlüsseltes Passwort - reset_password_token: Passwort-Reset-Token - reset_password_sent_at: Passwort-Reset-Anweisungen gesendet am - remember_created_at: Passwort merken seit - sign_in_count: Anzahl Logins - current_sign_in_at: Aktuell angemeldet seit - last_sign_in_at: Letzter Login am - current_sign_in_ip: Aktuelle IP-Adresse - last_sign_in_ip: Letzte IP-Adresse - created_at: Erstellt am - updated_at: Aktualisiert am - models: - admin_user: - one: Administrator - other: Administratoren - devise: - failure: - admin_user: - invalid: Benutzername oder Kennwort nicht korrekt. - unauthenticated: Bitte melden Sie sich als Administrator an. - sessions: - admin_user: - signed_in: Erfolgreich als Administrator angemeldet. diff --git a/config/locales/de.yml b/config/locales/de.yml index 69dea0dd3cd..91f37cf8ce7 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1,118 +1,143 @@ +--- de: active_admin: + access_denied: + message: Sie haben nicht die Berechtigung um diese Aktion auszuführen. + any: Alle + batch_actions: + action_label: Ausgewählte %{title} + button_label: Stapelverarbeitung + default_confirmation: Sind Sie sicher? + delete_confirmation: Sind Sie sicher dass Sie diese %{plural_model} löschen wollen? + labels: + destroy: löschen + selection_toggle_explanation: "(Auswahl umschalten)" + successfully_destroyed: + one: Erfolgreich 1 %{model} gelöscht + other: Erfolgreich %{count} %{plural_model} gelöscht + blank_slate: + content: Es gibt noch keine %{resource_name}. + link: Erstellen + cancel: Abbrechen + comments: + add: Kommentar hinzufügen + author: Autor + author_missing: Unbekannt + author_type: Autor-Typ + body: Inhalt + created_at: Erstellt + delete: Löschen + delete_confirmation: Sind Sie sicher dass Sie diesen Kommentar löschen wollen? + errors: + empty_text: Der Kommentar wurde nicht gespeichert, da der Text fehlt. + no_comments_yet: Es gibt noch keine Kommentare. + resource: Res­sour­ce + resource_type: Res­sour­cen-Typ + title_content: Kommentare (%{count}) + create_another: Mehr %{model} erstellen dashboard: Übersicht - dashboard_welcome: - welcome: "Willkommen in Active Admin. Dies ist die Standard-Übersichtsseite." - call_to_action: "Siehe 'app/admin/dashboard.rb', um Übersichts-Bereiche hinzuzufügen." - view: "Anzeigen" - edit: "Bearbeiten" - delete: "Löschen" - delete_confirmation: "Wollen Sie dieses Element wirklich löschen?" - new_model: "%{model} erstellen" - create_model: "%{model} erstellen" - edit_model: "%{model} bearbeiten" - update_model: "%{model} bearbeiten" + delete: Löschen + delete_confirmation: Wollen Sie dieses Element wirklich löschen? delete_model: "%{model} löschen" details: "%{model} Details" - cancel: "Abbrechen" - empty: "Leer" - previous: "Zurück" - next: "Weiter" - download: "Herunterladen:" - has_many_new: "%{model} hinzufügen" - has_many_delete: "Löschen" - has_many_remove: "Entfernen" + devise: + change_password: + submit: Mein Passwort ändern + title: Ändern Sie Ihr Passwort + email: + title: E-Mail-Adresse + links: + forgot_your_password: Passwort vergessen? + resend_confirmation_instructions: Bestätigungsanweisung erneut senden + resend_unlock_instructions: Entsperrungsanweisung erneut senden + sign_in: Anmeldung + sign_in_with_omniauth_provider: Anmeldung mit %{provider} + sign_up: Registrieren + login: + remember_me: Angemeldet bleiben + submit: Login + title: Login + password: + title: Passwort + password_confirmation: + title: Passwort Bestätigung + resend_confirmation_instructions: + submit: Anleitung zur Bestätigung noch mal schicken + title: Anleitung zur Bestätigung noch mal schicken + reset_password: + submit: Mein Passwort zurücksetzen + title: Passwort vergessen? + sign_up: + submit: Registrieren + title: Registrieren + subdomain: + title: Subdomain + unlock: + submit: Entsperrungsanweisung erneut senden + title: Entsperrungsanweisung erneut senden + username: + title: Benutzername + download: 'Herunterladen:' + edit: Bearbeiten + edit_model: "%{model} bearbeiten" + empty: Leer filters: buttons: - filter: "Filtern" - clear: "Filter entfernen" + clear: Filter entfernen + filter: Filtern predicates: - contains: "Enthält" - equals: "Gleich" - starts_with: "Beginnt mit" - ends_with: "Endet mit" - greater_than: "Größer als" - less_than: "Kleiner als" - main_content: "Bitte implementieren Sie %{model}#main_content, um Inhalte anzuzeigen." - logout: "Abmelden" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Filter" + from: Von + to: Bis + has_many_delete: Löschen + has_many_new: "%{model} hinzufügen" + has_many_remove: Entfernen + index_list: + table: Tabelle + logout: Abmelden + move: Verschieben + new_model: "%{model} erstellen" + next: Weiter pagination: - empty: "Keine %{model} gefunden" - one: "Zeige 1 %{model}" - one_page: "Zeige alle %{n} %{model}" - multiple: "Zeige %{model} %{from} – %{to} von %{total}" + empty: Keine %{model} gefunden entry: - one: "Eintrag" - other: "Einträge" - any: "Alle" - blank_slate: - content: "Es gibt noch keine %{resource_name}." - link: "Erstellen" - batch_actions: - button_label: "Stapelverarbeitung" - delete_confirmation: "Sind Sie sicher dass sie diese %{plural_model} löschen wollen? Dies kann nicht rückgängig gemacht werden" - succesfully_destroyed: - one: "Erfolgreich 1 %{model} gelöscht" - other: "Erfolgreich %{count} %{plural_model} gelöscht" - selection_toggle_explanation: "(Auswahl umschalten)" - link: "erstellen" - action_label: "%{title} ausgewählte" - labels: - destroy: "Lösche" - comments: - resource_type: "Res­sour­cen-Typ" - author_type: "Autor-Typ" - body: "Inhalt" - author: "Autor" - title: "Kommentar" - resource: "Res­sour­ce" - add: "Kommentar hinzufügen" - no_comments_yet: "Es gibt noch keine Kommentare." - title_content: "Kommentare (%{count})" - errors: - empty_text: "Der Kommentar wurde nicht gespeichert, da der Text fehlt." - devise: - login: - title: "Login" - remember_me: "erinnere dich an mich" - submit: "Login" - reset_password: - title: "Passwort vergessen?" - submit: "Mein Passwort zurücksetzen" - change_password: - title: "Ändern Sie Ihr Passwort" - submit: "Mein Passwort ändern" - links: - sign_in: "Anmeldung" - forgot_your_password: "Passwort vergessen?" - sign_in_with_omniauth_provider: "Anmeldung mit %{provider}" + one: Eintrag + other: Einträge + multiple: "%{model} %{from} – %{to} von %{total}" + multiple_without_total: "%{model} %{from} – %{to}" + next: Nächste + one: "1 %{model}" + one_page: "Alle %{n} %{model}" + per_page: 'Pro Seite: ' + previous: Vorherige + powered_by: Powered by %{active_admin} %{version} + previous: Zurück + scopes: + all: Alle + search_status: + no_current_filters: Keine + title: Aktive Filter + title_with_scope: Aktive Filter in %{name} + sidebars: + filters: Filter + search_status: Aktive Filter + status_tag: + 'no': Nein + unset: Nein + 'yes': Ja + view: Anzeigen activerecord: attributes: - admin_user: - id: ID - email: E-Mail - encrypted_Password: Verschlüsseltes Passwort - reset_password_token: Passwort-Reset-Token - reset_password_sent_at: Passwort-Reset-Anweisungen gesendet am - remember_created_at: Passwort merken seit - sign_in_count: Anzahl Logins - current_sign_in_at: Aktuell angemeldet seit - last_sign_in_at: Letzter Login am - current_sign_in_ip: Aktuelle IP-Adresse - last_sign_in_ip: Letzte IP-Adresse - created_at: Erstellt am - updated_at: Aktualisiert am + active_admin/comment: + author_type: Autortyp + body: Inhalt + created_at: Erstellt + namespace: Namensraum + resource_type: Ressourcentyp + updated_at: Aktualisiert models: - admin_user: - one: Administrator - other: Administratoren - devise: - failure: - admin_user: - invalid: Benutzername oder Kennwort nicht korrekt. - unauthenticated: Bitte melden Sie sich als Administrator an. - sessions: - admin_user: - signed_in: Erfolgreich als Administrator angemeldet. + active_admin/comment: + one: Kommentar + other: Kommentare + comment: + one: Kommentar + other: Kommentare diff --git a/config/locales/el.yml b/config/locales/el.yml new file mode 100644 index 00000000000..29eaa60a4c3 --- /dev/null +++ b/config/locales/el.yml @@ -0,0 +1,107 @@ +--- +el: + active_admin: + access_denied: + message: Δεν έχετε πρόσβαση για αυτή την ενέργεια. + any: Όλες οι εγγραφές + batch_actions: + action_label: "%{title} επιλεγμένων" + button_label: Μαζικές Ενέργειες + default_confirmation: Είστε σίγουρος πως θέλετε να το κάνετε αυτό; + delete_confirmation: Είστε σίγουρος πως θέλετε να διαγράψετε αυτά τα %{plural_model}? + labels: + destroy: Διαγραφή + selection_toggle_explanation: "(Αντιστροφή επιλογών)" + successfully_destroyed: + one: Διαγράφηκε επιτυχώς 1 %{model} + other: Διαγράφηκαν επιτυχώς %{count} %{plural_model} + blank_slate: + content: Δεν υπάρχουν %{resource_name} ακόμα. + link: Δημιουργήστε μία εγγραφή + cancel: Ακύρωση + comments: + add: Προσθήκη Σχολίου + author: Συγγραφέας + author_missing: Ανώνυμος + author_type: Τύπος Συγγραφέα + body: Κείμενο + errors: + empty_text: Το σχόλιο δε σώθηκε, το κείμενο ήταν κενό. + no_comments_yet: Δεν υπάρχει κανένα σχόλιο. + resource: Εγγραφή + resource_type: Τύπος Εγγραφής + title_content: Σχόλια (%{count}) + dashboard: Σελίδα διαχείρισης + delete: Διαγραφή + delete_confirmation: Είστε σίγουρος πως θέλετε να το διαγράψετε; + delete_model: Διαγραφή %{model} + details: Λεπτομέρειες %{model} + devise: + change_password: + submit: Αλλαγή του κωδικού + title: Αλλάξτε τον κωδικό σας + email: + title: Email + links: + forgot_your_password: Ξεχάσατε τον κωδικό σας; + resend_confirmation_instructions: Αποστολή οδηγιών επιβεβαίωσης + resend_unlock_instructions: Αποστολή οδηγιών ξεκλειδώματος + sign_in: Σύνδεση + sign_in_with_omniauth_provider: Σύνδεση με %{provider} + sign_up: Εγγραφή + login: + remember_me: Να με θυμάσαι + submit: Σύνδεση + title: Σύνδεση + password: + title: Κωδικός + resend_confirmation_instructions: + submit: Αποστολή οδηγιών επιβεβαίωσης + title: Αποστολή οδηγιών επιβεβαίωσης + reset_password: + submit: Επαναφορά κωδικού + title: Ξεχάσατε τον κωδικό σας; + sign_up: + submit: Εγγραφή + title: Εγγραφή + subdomain: + title: Subdomain + unlock: + submit: Αποστολή οδηγιών ξεκλειδώματος + title: Αποστολή οδηγιών ξεκλειδώματος + username: + title: Όνομα χρήστη + download: 'Κατέβασμα:' + edit: Επεξεργασία + edit_model: Επεξεργασία %{model} + empty: Άδειο + filters: + buttons: + clear: Καθαρισμός Φίλτρων + filter: Φίλτρα + has_many_delete: Διαγραφή + has_many_new: Προσθήκη Νέου %{model} + has_many_remove: Αφαίρεση + index_list: + table: Πίνακας + logout: Αποσύνδεση + new_model: Δημιουργία %{model} + next: Επόμενη + pagination: + empty: Δε βρέθηκαν %{model} + entry: + one: εγγραφή + other: εγγραφές + multiple: Εμφανίζονται %{model} %{from} - %{to} από %{total} συνολικά + multiple_without_total: Εμφανίζονται %{model} %{from} - %{to} + one: Εμφάνιζεται 1 %{model} + one_page: Εμφανίζονται όλες οι %{n} εγγραφές %{model} + powered_by: Powered by %{active_admin} %{version} + previous: Προηγούμενη + sidebars: + filters: Φίλτρα + status_tag: + 'no': Όχι + unset: Όχι + 'yes': Ναι + view: Προβολή diff --git a/config/locales/en-CA.yml b/config/locales/en-CA.yml new file mode 100644 index 00000000000..249328af814 --- /dev/null +++ b/config/locales/en-CA.yml @@ -0,0 +1,117 @@ +--- +en-CA: + active_admin: + access_denied: + message: You are not authorized to perform this action. + any: Any + batch_actions: + action_label: "%{title} Selected" + button_label: Batch Actions + default_confirmation: Are you sure you want to do this? + delete_confirmation: Are you sure you want to delete these %{plural_model}? + labels: + destroy: Delete + selection_toggle_explanation: "(Toggle Selection)" + successfully_destroyed: + one: Successfully deleted 1 %{model} + other: Successfully deleted %{count} %{plural_model} + blank_slate: + content: There are no %{resource_name} yet. + link: Create one + cancel: Cancel + comments: + add: Add Comment + author: Author + author_missing: Anonymous + author_type: Author Type + body: Body + created_at: Created + delete: Delete Comment + delete_confirmation: Are you sure you want to delete these comments? + errors: + empty_text: Comment wasn't saved, text was empty. + no_comments_yet: No comments yet. + resource: Resource + resource_type: Resource Type + title_content: Comments (%{count}) + create_another: Create another %{model} + dashboard: Dashboard + delete: Delete + delete_confirmation: Are you sure you want to delete this? + delete_model: Delete %{model} + details: "%{model} Details" + devise: + change_password: + submit: Change my password + title: Change your password + email: + title: Email + links: + forgot_your_password: Forgot your password? + resend_confirmation_instructions: Resend confirmation instructions + resend_unlock_instructions: Resend unlock instructions + sign_in: Sign in + sign_in_with_omniauth_provider: Sign in with %{provider} + sign_up: Sign up + login: + remember_me: Remember me + submit: Login + title: Login + password: + title: Password + password_confirmation: + title: Confirm Password + resend_confirmation_instructions: + submit: Resend confirmation instructions + title: Resend confirmation instructions + reset_password: + submit: Reset My Password + title: Forgot your password? + sign_up: + submit: Sign up + title: Sign up + subdomain: + title: Subdomain + unlock: + submit: Resend unlock instructions + title: Resend unlock instructions + username: + title: Username + download: 'Download:' + edit: Edit + edit_model: Edit %{model} + empty: Empty + filters: + buttons: + clear: Clear Filters + filter: Filter + has_many_delete: Delete + has_many_new: Add New %{model} + has_many_remove: Remove + index_list: + table: Table + logout: Logout + new_model: New %{model} + next: Next + pagination: + empty: No %{model} found + entry: + one: entry + other: entries + multiple: Displaying %{model} %{from} - %{to} of %{total} in total + multiple_without_total: Displaying %{model} %{from} - %{to} + one: Displaying 1 %{model} + one_page: Displaying all %{n} %{model} + per_page: 'Per page: ' + powered_by: Powered by %{active_admin} %{version} + previous: Previous + search_status: + no_current_filters: None + sidebars: + filters: Filters + search_status: Search Status + status_tag: + 'no': 'No' + unset: 'No' + 'yes': 'Yes' + view: View diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml index 3e8f500c9ea..1650178ffdc 100644 --- a/config/locales/en-GB.yml +++ b/config/locales/en-GB.yml @@ -1,89 +1,117 @@ -"en-GB": +--- +en-GB: active_admin: - dashboard: Dashboard - dashboard_welcome: - welcome: "Welcome to Active Admin. This is the default dashboard page." - call_to_action: "To add dashboard sections, checkout 'app/admin/dashboards.rb'" - view: "View" - edit: "Edit" - delete: "Delete" - delete_confirmation: "Are you sure you want to delete this?" - new_model: "New %{model}" - create_model: "New %{model}" - edit_model: "Edit %{model}" - update_model: "Edit %{model}" - delete_model: "Delete %{model}" - details: "%{model} Details" - cancel: "Cancel" - empty: "Empty" - previous: "Previous" - next: "Next" - download: "Download:" - has_many_new: "Add New %{model}" - has_many_delete: "Delete" - has_many_remove: "Remove" - filters: - buttons: - filter: "Filter" - clear: "Clear Filters" - predicates: - contains: "Contains" - equals: "Equals" - starts_with: "Starts with" - ends_with: "Ends with" - greater_than: "Greater than" - less_than: "Less than" - main_content: "Please implement %{model}#main_content to display content." - logout: "Logout" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Filters" - pagination: - empty: "No %{model} found" - one: "Displaying 1 %{model}" - one_page: "Displaying all %{n} %{model}" - multiple: "Displaying %{model} %{from} - %{to} of %{total} in total" - entry: - one: "entry" - other: "entries" - any: "Any" - blank_slate: - content: "There are no %{resource_name} yet." - link: "Create one" + access_denied: + message: You are not authorised to perform this action. + any: Any batch_actions: - button_label: "Batch Actions" - delete_confirmation: "Are you sure you want to delete these %{plural_model}? You won't be able to undo this." - succesfully_destroyed: - one: "Successfully destroyed 1 %{model}" - other: "Successfully destroyed %{count} %{plural_model}" - selection_toggle_explanation: "(Toggle Selection)" - link: "Create one" action_label: "%{title} Selected" + button_label: Batch Actions + default_confirmation: Are you sure you want to do this? + delete_confirmation: Are you sure you want to delete these %{plural_model}? labels: - destroy: "Delete" + destroy: Delete + selection_toggle_explanation: "(Toggle Selection)" + successfully_destroyed: + one: Successfully deleted 1 %{model} + other: Successfully deleted %{count} %{plural_model} + blank_slate: + content: There are no %{resource_name} yet. + link: Create one + cancel: Cancel comments: - body: "Body" - author: "Author" - title: "Comment" - add: "Add Comment" - resource: "Resource" - no_comments_yet: "No comments yet." - title_content: "Comments (%{count})" + add: Add Comment + author: Author + author_missing: Anonymous + author_type: Author Type + body: Body + created_at: Created + delete: Delete Comment + delete_confirmation: Are you sure you want to delete these comments? errors: - empty_text: "Comment wasn't saved, text was empty." + empty_text: Comment wasn't saved, text was empty. + no_comments_yet: No comments yet. + resource: Resource + resource_type: Resource Type + title_content: Comments (%{count}) + create_another: Create another %{model} + dashboard: Dashboard + delete: Delete + delete_confirmation: Are you sure you want to delete this? + delete_model: Delete %{model} + details: "%{model} Details" devise: - login: - title: "Login" - remember_me: "Remember me" - submit: "Login" - reset_password: - title: "Forgot your password?" - submit: "Reset My Password" change_password: - title: "Change your password" - submit: "Change my password" + submit: Change my password + title: Change your password + email: + title: Email links: - sign_in: "Sign in" - forgot_your_password: "Forgot your password?" - sign_in_with_omniauth_provider: "Sign in with %{provider}" - + forgot_your_password: Forgot your password? + resend_confirmation_instructions: Resend confirmation instructions + resend_unlock_instructions: Resend unlock instructions + sign_in: Sign in + sign_in_with_omniauth_provider: Sign in with %{provider} + sign_up: Sign up + login: + remember_me: Remember me + submit: Login + title: Login + password: + title: Password + password_confirmation: + title: Confirm Password + resend_confirmation_instructions: + submit: Resend confirmation instructions + title: Resend confirmation instructions + reset_password: + submit: Reset My Password + title: Forgot your password? + sign_up: + submit: Sign up + title: Sign up + subdomain: + title: Subdomain + unlock: + submit: Resend unlock instructions + title: Resend unlock instructions + username: + title: Username + download: 'Download:' + edit: Edit + edit_model: Edit %{model} + empty: Empty + filters: + buttons: + clear: Clear Filters + filter: Filter + has_many_delete: Delete + has_many_new: Add New %{model} + has_many_remove: Remove + index_list: + table: Table + logout: Logout + new_model: New %{model} + next: Next + pagination: + empty: No %{model} found + entry: + one: entry + other: entries + multiple: Displaying %{model} %{from} - %{to} of %{total} in total + multiple_without_total: Displaying %{model} %{from} - %{to} + one: Displaying 1 %{model} + one_page: Displaying all %{n} %{model} + per_page: 'Per page: ' + powered_by: Powered by %{active_admin} %{version} + previous: Previous + search_status: + no_current_filters: None + sidebars: + filters: Filters + search_status: Search Status + status_tag: + 'no': 'No' + unset: 'No' + 'yes': 'Yes' + view: View diff --git a/config/locales/en.yml b/config/locales/en.yml index 12373978ea9..96da53870ce 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,100 +1,148 @@ +--- en: active_admin: - dashboard: Dashboard - dashboard_welcome: - welcome: "Welcome to Active Admin. This is the default dashboard page." - call_to_action: "To add dashboard sections, checkout 'app/admin/dashboard.rb'" - view: "View" - edit: "Edit" - delete: "Delete" - delete_confirmation: "Are you sure you want to delete this?" - new_model: "New %{model}" - create_model: "New %{model}" - edit_model: "Edit %{model}" - update_model: "Edit %{model}" - delete_model: "Delete %{model}" - details: "%{model} Details" - cancel: "Cancel" - empty: "Empty" - previous: "Previous" - next: "Next" - download: "Download:" - has_many_new: "Add New %{model}" - has_many_delete: "Delete" - has_many_remove: "Remove" - filters: - buttons: - filter: "Filter" - clear: "Clear Filters" - predicates: - contains: "Contains" - equals: "Equals" - starts_with: "Starts with" - ends_with: "Ends with" - greater_than: "Greater than" - less_than: "Less than" - main_content: "Please implement %{model}#main_content to display content." - logout: "Logout" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Filters" - pagination: - empty: "No %{model} found" - one: "Displaying 1 %{model}" - one_page: "Displaying all %{n} %{model}" - multiple: "Displaying %{model} %{from} - %{to} of %{total} in total" - entry: - one: "entry" - other: "entries" - any: "Any" - blank_slate: - content: "There are no %{resource_name} yet." - link: "Create one" + access_denied: + message: You are not authorized to perform this action. + any: Any batch_actions: - button_label: "Batch Actions" - delete_confirmation: "Are you sure you want to delete these %{plural_model}? You won't be able to undo this." - succesfully_destroyed: - one: "Successfully destroyed 1 %{model}" - other: "Successfully destroyed %{count} %{plural_model}" - selection_toggle_explanation: "(Toggle Selection)" - link: "Create one" action_label: "%{title} Selected" + button_label: Batch Actions + default_confirmation: Are you sure you want to do this? + delete_confirmation: Are you sure you want to delete these %{plural_model}? labels: - destroy: "Delete" + destroy: Delete + selection_toggle_explanation: "(Toggle Selection)" + successfully_destroyed: + one: Successfully deleted 1 %{model} + other: Successfully deleted %{count} %{plural_model} + blank_slate: + content: There are no %{resource_name} yet. + link: Create one + cancel: Cancel comments: - resource_type: "Resource Type" - author_type: "Author Type" - body: "Body" - author: "Author" - title: "Comment" - add: "Add Comment" - resource: "Resource" - no_comments_yet: "No comments yet." - title_content: "Comments (%{count})" + add: Add Comment + author: Author + author_missing: Anonymous + author_type: Author Type + body: Body + created_at: Created + delete: Delete Comment + delete_confirmation: Are you sure you want to delete this comment? errors: - empty_text: "Comment wasn't saved, text was empty." + empty_text: Comment wasn't saved, text was empty. + no_comments_yet: No comments yet. + resource: Resource + resource_type: Resource Type + title_content: All Comments (%{count}) + create_another: Create another %{model} + dashboard: Dashboard + delete: Delete + delete_confirmation: Are you sure you want to delete this? + delete_model: Delete %{model} + details: "%{model} Details" devise: + change_password: + submit: Change my password + title: Change your password + email: + title: Email + links: + forgot_your_password: Forgot your password? + resend_confirmation_instructions: Resend confirmation instructions + resend_unlock_instructions: Resend unlock instructions + sign_in: Sign in + sign_in_with_omniauth_provider: Sign in with %{provider} + sign_up: Sign up login: - title: "Login" - remember_me: "Remember me" - submit: "Login" + remember_me: Remember me + submit: Sign In + title: Sign In + password: + title: Password + password_confirmation: + title: Confirm Password + resend_confirmation_instructions: + submit: Resend confirmation instructions + title: Resend confirmation instructions reset_password: - title: "Forgot your password?" - submit: "Reset My Password" - change_password: - title: "Change your password" - submit: "Change my password" + submit: Reset My Password + title: Forgot your password? + sign_up: + submit: Sign up + title: Sign up + subdomain: + title: Subdomain unlock: - title: "Resend unlock instructions" - submit: "Resend unlock instructions" - links: - sign_in: "Sign in" - forgot_your_password: "Forgot your password?" - sign_in_with_omniauth_provider: "Sign in with %{provider}" - access_denied: - message: "You are not authorized to perform this action." + submit: Resend unlock instructions + title: Resend unlock instructions + username: + title: Username + download: 'Download:' + edit: Edit + edit_model: Edit %{model} + empty: Empty + filters: + buttons: + clear: Clear Filters + filter: Filter + predicates: + from: From + to: To + has_many_delete: Delete + has_many_new: Add New %{model} + has_many_remove: Remove index_list: - table: "Table" - block: "List" - grid: "Grid" - blog: "Blog" + table: Table + logout: Sign out + move: Move + new_model: New %{model} + next: Next + pagination: + empty: No %{model} found + entry: + one: entry + other: entries + multiple: Showing %{from}-%{to} of %{total} + multiple_without_total: Showing %{from}-%{to} + next: Next + one: Showing 1 of 1 + one_page: Showing all %{n} + per_page: 'Per page ' + previous: Previous + truncate: "…" + powered_by: Powered by %{active_admin} %{version} + previous: Previous + scopes: + all: All + search_status: + no_current_filters: No filters applied + title: Active Search + title_with_scope: Active Search for %{name} + sidebars: + filters: Filters + search_status: Search Status + status_tag: + 'no': 'No' + unset: Unknown + 'yes': 'Yes' + toggle_dark_mode: Toggle dark mode + toggle_main_navigation_menu: Toggle main navigation menu + toggle_section: Toggle section + toggle_user_menu: Toggle user menu + view: View + activerecord: + attributes: + active_admin/comment: + author_type: Author type + body: Body + created_at: Created + namespace: Namespace + resource_type: Resource type + updated_at: Updated + models: + active_admin/comment: + one: Comment + other: Comments + comment: + one: Comment + other: Comments diff --git a/config/locales/eo.yml b/config/locales/eo.yml new file mode 100644 index 00000000000..d9df1356668 --- /dev/null +++ b/config/locales/eo.yml @@ -0,0 +1,121 @@ +--- +eo: + active_admin: + access_denied: + message: Vi ne rajtas fari tiun agon. + any: Ĉiuj + batch_actions: + action_label: "%{title} elektita" + button_label: Amasagoj + default_confirmation: Ĉu vi certas, ke vi volas fari tion? + delete_confirmation: Ĉu vi certas, ke vi volas forigi tiujn %{plural_model}? + labels: + destroy: Forigi + selection_toggle_explanation: "(Baskuligi elekton)" + successfully_destroyed: + one: 1 %{model} sukcese forigita + other: "%{count} %{plural_model} sukcese forigitaj" + blank_slate: + content: Ankoraŭ ne estas %{resource_name}. + link: Krei novan + cancel: Nuligi + comments: + add: Aldoni komenton + author: Aŭtoro + author_missing: Anonimulo + author_type: Aŭtorotipo + body: Enhavo + created_at: Kreita + delete: Forigi komenton + delete_confirmation: Ĉu vi certas, ke vi volas forigi tiun komenton? + errors: + empty_text: La komento ne estis konservita, la teksto estis malplena. + no_comments_yet: Ankoraŭ neniu komento. + resource: Resurso + resource_type: Resursotipo + title_content: Komentoj (%{count}) + create_another: Krei alian %{model} + dashboard: Panelo + delete: Forigi + delete_confirmation: Ĉu vi certas, ke vi volas forigi tion? + delete_model: Forigi %{model} + details: Detaloj de %{model} + devise: + change_password: + submit: Ŝanĝi mian pasvorton + title: Ŝanĝi vian pasvorton + email: + title: Retpoŝtadreso + links: + forgot_your_password: Ĉu vi forgesis vian pasvorton? + resend_confirmation_instructions: Resendi klarigojn por konfirmi + resend_unlock_instructions: Resendi klarigojn por malŝlosi + sign_in: Ensaluti + sign_in_with_omniauth_provider: Ensaluti per %{provider} + sign_up: Registriĝi + login: + remember_me: Memori min + submit: Ensaluti + title: Ensaluti + password: + title: Pasvorto + password_confirmation: + title: Konfirmi pasvorton + resend_confirmation_instructions: + submit: Resendi klarigojn por konfirmi + title: Resendi klarigojn por konfirmi + reset_password: + submit: Restarigi mian pasvorton + title: Ĉu vi forgesis vian pasvorton? + sign_up: + submit: Registriĝi + title: Registriĝi + subdomain: + title: Subdomajno + unlock: + submit: Resendi klarigojn por malŝlosi + title: Resendi klarigojn por malŝlosi + username: + title: Uzantnomo + download: 'Elŝuti:' + edit: Redakti + edit_model: Redakti %{model} + empty: Malplena + filters: + buttons: + clear: Viŝi filtrilojn + filter: Filtri + predicates: + from: De + to: Al + has_many_delete: Forigi + has_many_new: Aldoni novan %{model} + has_many_remove: Forigi + index_list: + table: Tabelo + logout: Elsaluti + move: Movi + new_model: Nova %{model} + next: Sekva + pagination: + empty: Neniu %{model} trovita + entry: + one: ero + other: eroj + multiple: Montras %{model} %{from} - %{to} de %{total} entute + multiple_without_total: Montras %{model} %{from} - %{to} + one: Montras 1 %{model} + one_page: Montras ĉiujn %{n} %{model} + per_page: 'Paĝe: ' + powered_by: Povigita de %{active_admin} %{version} + previous: Antaŭa + search_status: + no_current_filters: Neniu + sidebars: + filters: Filtriloj + search_status: Serĉstato + status_tag: + 'no': Ne + unset: Ne + 'yes': Jes + view: Vidi diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml new file mode 100644 index 00000000000..9cf3d0acbd6 --- /dev/null +++ b/config/locales/es-MX.yml @@ -0,0 +1,82 @@ +--- +es-MX: + active_admin: + any: Cualquiera + batch_actions: + action_label: "%{title} seleccionados" + button_label: Acciones en masa + default_confirmation: "¿Seguro que quieres hacer esto?" + delete_confirmation: 'Eliminar %{plural_model}: ¿Está seguro?' + labels: + destroy: Borrar + selection_toggle_explanation: "(Cambiar selección)" + successfully_destroyed: + one: Se ha destruido 1 %{model} con éxito + other: Se han destruido %{count} %{plural_model} con éxito + blank_slate: + content: No hay %{resource_name} aún. + link: Añadir + cancel: Cancelar + comments: + add: Comentar + author: Autor + body: Cuerpo + errors: + empty_text: El comentario no fue guardado, el texto estaba vacío. + no_comments_yet: Aún sin comentarios. + resource: Recurso + title_content: Comentarios (%{count}) + dashboard: Inicio + delete: Eliminar + delete_confirmation: "¿Está seguro de que quiere eliminar esto?" + delete_model: Eliminar %{model} + details: Detalles de %{model} + devise: + change_password: + submit: Cambiar mi contraseña + title: Cambie su contraseña + links: + forgot_your_password: "¿Olvidó su contraseña?" + sign_in: Iniciar Sesión + sign_in_with_omniauth_provider: Conéctate con %{provider} + sign_up: Registrarse + login: + remember_me: Recordarme + submit: Iniciar Sesión + title: Iniciar Sesión + reset_password: + submit: Restablecer mi contraseña + title: "¿Olvidó su contraseña?" + download: 'Descargar:' + edit: Editar + edit_model: Editar %{model} + empty: Vacío + filters: + buttons: + clear: Quitar Filtros + filter: Filtrar + has_many_delete: Eliminar + has_many_new: Añadir %{model} + has_many_remove: Quitar + index_list: + table: Tabla + logout: Salir + new_model: Añadir %{model} + next: Siguiente + pagination: + empty: No se han encontrado %{model} + entry: + one: + other: + multiple: Mostrando %{model} %{from} - %{to} de un total de %{total} + one: Mostrando 1 %{model} + one_page: Mostrando un total de %{n} %{model} + powered_by: Powered by %{active_admin} %{version} + previous: Anterior + sidebars: + filters: Filtros + status_tag: + 'no': 'No' + unset: 'No' + 'yes': Sí + view: Ver diff --git a/config/locales/es.yml b/config/locales/es.yml index ca0d57801ac..4bdb605c202 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1,105 +1,145 @@ +--- es: active_admin: - dashboard: Inicio - dashboard_welcome: - welcome: "Bienvenido a Active Admin. Esta es la página de inicio predeterminada." - call_to_action: "Para agregar secciones edite 'app/admin/dashboard.rb'" - view: "Ver" - edit: "Editar" - delete: "Eliminar" - delete_confirmation: "¿Está seguro de que quiere eliminar esto?" - new_model: "Añadir %{model}" - create_model: "Añadir %{model}" - edit_model: "Editar %{model}" - update_model: "Editar %{model}" - delete_model: "Eliminar %{model}" - details: "Detalles de %{model}" - cancel: "Cancelar" - empty: "Vacío" - previous: "Anterior" - next: "Siguiente" - download: "Descargar:" - has_many_new: "Agregar Añadir %{model}" - has_many_delete: "Eliminar" - has_many_remove: "Quitar" - filters: - buttons: - filter: "Filtrar" - clear: "Quitar Filtros" - predicates: - contains: "Contiene" - equals: "Igual a" - starts_with: "Empieza con" - ends_with: "Termina con" - greater_than: "Mayor que" - less_than: "Menor que" - main_content: "Por favor implemente %{model}#main_content para mostrar contenido." - logout: "Salir" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Filtros" - pagination: - empty: "No se han encontrado %{model}" - one: "Mostrando 1 %{model}" - one_page: "Mostrando un total de %{n} %{model}" - multiple: "Mostrando %{model} %{from} - %{to} de un total de %{total}" - blank_slate: - content: "No hay %{resource_name} aún." - link: "Añadir" - entry: - one: "registro" - other: "registros" - any: "Cualquiera" + access_denied: + message: No está autorizado/a a realizar esta acción. + any: Cualquiera batch_actions: - button_label: "Acciones en masa" - delete_confirmation: "Eliminar %{plural_model}: ¿Está seguro? No podrá deshacer esta acción." - succesfully_destroyed: - one: "Se ha destruido 1 %{model} con éxito" - other: "Se han destruido %{count} %{plural_model} con éxito" - selection_toggle_explanation: "(Cambiar selección)" - link: "Añadir" - action_label: "%{title} seleccionado" + action_label: "%{title} seleccionados" + button_label: Acciones en masa + default_confirmation: "¿Seguro que quieres hacer esto?" + delete_confirmation: Se eliminarán %{plural_model}. ¿Desea continuar? labels: - destroy: "Borrar" + destroy: Borrar + selection_toggle_explanation: "(Cambiar selección)" + successfully_destroyed: + one: Se ha destruido 1 %{model} con éxito + other: Se han destruido %{count} %{plural_model} con éxito + blank_slate: + content: No hay %{resource_name} aún. + link: Añadir + cancel: Cancelar comments: - body: "Cuerpo" - author: "Autor" - title: "Comentario" - add: "Comentar" - resource: "Recurso" - no_comments_yet: "Aún sin comentarios." - title_content: "Comentarios (%{count})" + add: Comentar + author: Autor + author_missing: Anónimo + author_type: Tipo de autor + body: Cuerpo + created_at: Fecha de creación + delete: Borrar Comentario + delete_confirmation: "¿Confirma que desea borrar este comentario?" errors: - empty_text: "El comentario no fue guardado, el texto estaba vacío." + empty_text: El comentario no fue guardado, el texto estaba vacío. + no_comments_yet: No hay comentarios aún. + resource: Recurso + resource_type: Tipo de recurso + title_content: Comentarios (%{count}) + create_another: Crear otro %{model} + dashboard: Inicio + delete: Eliminar + delete_confirmation: "¿Confirma que desea borrar este elemento?" + delete_model: Eliminar %{model} + details: Detalles de %{model} devise: - login: - title: "iniciar sesión" - remember_me: "Recordarme" - submit: "iniciar sesión" - reset_password: - title: "¿Olvidó su contraseña?" - submit: "Restablecer mi contraseña" change_password: - title: "Cambie su contraseña" - submit: "Cambiar mi contraseña" + submit: Cambiar mi contraseña + title: Cambie su contraseña + email: + title: Email links: - sign_in: "registrarse" forgot_your_password: "¿Olvidó su contraseña?" - sign_in_with_omniauth_provider: "Conéctate con %{provider}" - views: - pagination: - truncate: "..." - first: "Inicio" - previous: "Anterior" - next: "Siguiente" - last: "Último" - formtastic: - yes: "Sí" - no: "No" - create: "Guardar %{model}" - update: "Guardar %{model}" - submit: "Aceptar" - cancel: "Cancelar" - reset: "Restablecer %{model}" - required: "requerido" - + resend_confirmation_instructions: Reenviar instrucciones de confirmación + resend_unlock_instructions: Reenviar instrucciones de desbloqueo + sign_in: Iniciar Sesión + sign_in_with_omniauth_provider: Conéctate con %{provider} + sign_up: Registrarse + login: + remember_me: Recordarme + submit: Iniciar Sesión + title: Iniciar Sesión + password: + title: Contraseña + password_confirmation: + title: Confirmar Contraseña + resend_confirmation_instructions: + submit: Reenviar instrucciones de confirmación + title: Reenviar instrucciones de confirmación + reset_password: + submit: Restablecer mi contraseña + title: "¿Olvidó su contraseña?" + sign_up: + submit: Registrarse + title: Registrarse + subdomain: + title: Subdominio + unlock: + submit: Reenviar instrucciones de desbloqueo + title: Reenviar instrucciones de desbloqueo + username: + title: Nombre de usuario + download: 'Descargar:' + edit: Editar + edit_model: Editar %{model} + empty: Vacío + filters: + buttons: + clear: Quitar Filtros + filter: Filtrar + predicates: + from: Desde + to: Hasta + has_many_delete: Eliminar + has_many_new: Añadir %{model} + has_many_remove: Quitar + index_list: + table: Tabla + logout: Salir + move: Mover + new_model: Añadir %{model} + next: Siguiente + pagination: + empty: No se han encontrado %{model} + entry: + one: registro + other: registros + multiple: Mostrando %{model} %{from} - %{to} de un total de %{total} + multiple_without_total: Mostrando %{model} %{from} - %{to} + next: Siguiente + one: Mostrando 1 %{model} + one_page: Mostrando un total de %{n} %{model} + per_page: 'Por página: ' + previous: Anterior + powered_by: Funciona con %{active_admin} %{version} + previous: Anterior + scopes: + all: Todos + search_status: + no_current_filters: Ninguno + sidebars: + filters: Filtros + search_status: Estado de la búsqueda + status_tag: + 'no': 'No' + unset: 'No' + 'yes': Sí + toggle_dark_mode: Alternar modo oscuro + toggle_main_navigation_menu: Alternar el menú de navegación principal + toggle_section: Alternar sección + toggle_user_menu: Alternar menú de usuario + view: Ver + activerecord: + attributes: + active_admin/comment: + author_type: Tipo de autor + body: Cuerpo + created_at: Fecha de creación + namespace: Espacio de nombres + resource_type: Tipo de recurso + updated_at: Fecha de actualización + models: + active_admin/comment: + one: Comentario + other: Comentarios + comment: + one: Comentario + other: Comentarios diff --git a/config/locales/es_MX.yml b/config/locales/es_MX.yml deleted file mode 100644 index 771978a5e9b..00000000000 --- a/config/locales/es_MX.yml +++ /dev/null @@ -1,104 +0,0 @@ -es_MX: - active_admin: - dashboard: Inicio - dashboard_welcome: - welcome: "Bienvenido a Active Admin. Esta es la página de inicio predeterminada." - call_to_action: "Para agregar secciones edite 'app/admin/dashboard.rb'" - view: "Ver" - edit: "Editar" - delete: "Eliminar" - delete_confirmation: "¿Está seguro de que quiere eliminar esto?" - new_model: "Añadir %{model}" - create_model: "Añadir %{model}" - edit_model: "Editar %{model}" - update_model: "Editar %{model}" - delete_model: "Eliminar %{model}" - details: "Detalles de %{model}" - cancel: "Cancelar" - empty: "Vacío" - previous: "Anterior" - next: "Siguiente" - download: "Descargar:" - has_many_new: "Agregar Añadir %{model}" - has_many_delete: "Eliminar" - has_many_remove: "Quitar" - filters: - buttons: - filter: "Filtrar" - clear: "Quitar Filtros" - predicates: - contains: "Contiene" - equals: "Igual a" - starts_with: "Empieza con" - ends_with: "Termina con" - greater_than: "Mayor que" - less_than: "Menor que" - main_content: "Por favor implemente %{model}#main_content para mostrar contenido." - logout: "Salir" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Filtros" - pagination: - empty: "No se han encontrado %{model}" - one: "Mostrando 1 %{model}" - one_page: "Mostrando un total de %{n} %{model}" - multiple: "Mostrando %{model} %{from} - %{to} de un total de %{total}" - blank_slate: - content: "No hay %{resource_name} aún." - link: "Añadir" - entry: - one: "registro" - other: "registros" - any: "Cualquiera" - batch_actions: - button_label: "Acciones en masa" - delete_confirmation: "Eliminar %{plural_model}: ¿Está seguro? No podrá deshacer esta acción." - succesfully_destroyed: - one: "Se ha destruido 1 %{model} con éxito" - other: "Se han destruido %{count} %{plural_model} con éxito" - selection_toggle_explanation: "(Cambiar selección)" - link: "Añadir" - action_label: "%{title} seleccionado" - labels: - destroy: "Borrar" - comments: - body: "Cuerpo" - author: "Autor" - title: "Comentario" - add: "Comentar" - resource: "Recurso" - no_comments_yet: "Aún sin comentarios." - title_content: "Comentarios (%{count})" - errors: - empty_text: "El comentario no fue guardado, el texto estaba vacío." - devise: - login: - title: "iniciar sesión" - remember_me: "Recordarme" - submit: "iniciar sesión" - reset_password: - title: "¿Olvidó su contraseña?" - submit: "Restablecer mi contraseña" - change_password: - title: "Cambie su contraseña" - submit: "Cambiar mi contraseña" - links: - sign_in: "registrarse" - forgot_your_password: "¿Olvidó su contraseña?" - sign_in_with_omniauth_provider: "Conéctate con %{provider}" - views: - pagination: - truncate: "..." - first: "Inicio" - previous: "Anterior" - next: "Siguiente" - last: "Último" - formtastic: - yes: "Sí" - no: "No" - create: "Guardar %{model}" - update: "Guardar %{model}" - submit: "Aceptar" - cancel: "Cancelar" - reset: "Restablecer %{model}" - required: "requerido" diff --git a/config/locales/fa.yml b/config/locales/fa.yml new file mode 100644 index 00000000000..cbbf6cbec30 --- /dev/null +++ b/config/locales/fa.yml @@ -0,0 +1,104 @@ +--- +fa: + active_admin: + access_denied: + message: شما دسترسی لازم برای انجام این عملیات را ندارید. + any: هرکدام + batch_actions: + action_label: "%{title} انتخاب شده است" + button_label: عملیات‌های دسته‌ای + default_confirmation: آیا برای اجرای این عملیات اطمینان دارید؟ + delete_confirmation: آیا برای حذف همه رکوردهای %{plural_model} اطمینان دارید؟ + labels: + destroy: حذف + selection_toggle_explanation: "(انتخاب‌ها برعکس شوند)" + successfully_destroyed: + one: 1 %{model} با موفقیت حذف شد + other: "%{count} %{plural_model} با موفقت حذف شدند." + blank_slate: + content: هنوز هیچ رکوردی از %{resource_name} درج نشده. + link: درج اولین رکورد + cancel: لغو + comments: + add: افزودن کامنت + author: ایجاد کننده + author_missing: بی‌نام + author_type: نوع ایجاد کننده + body: بدنه + errors: + empty_text: کامنت درج نشد، متن کامنت خالی بود. + no_comments_yet: هنوز هیچ کامنتی نوشته نشده. + resource: رکورد + resource_type: نوع رکورد + title_content: کامنت‌ها (%{count}) + dashboard: داشبرد + delete: حذف + delete_confirmation: آیا برای حذف این آیتم اطمینان دارید؟ + delete_model: حذف %{model} + details: جزئیات %{model} + devise: + change_password: + submit: تغییر کلمه عبور + title: تغییر کلمه عبور + email: + title: ایمیل + links: + forgot_your_password: کلمه عبور را فراموش کرده‌اید؟ + sign_in: ورود + sign_in_with_omniauth_provider: ورود با حساب %{provider} + login: + remember_me: مرا به خاطر بسپار + submit: ورود + title: ورود + password: + title: کلمه‌عبور + resend_confirmation_instructions: + submit: ارسال مجدد تاییدیه ایمیل + title: ارسال مجدد تاییدیه ایمیل + reset_password: + submit: دریافت کلمه عبور جدید + title: کلمه عبور را فراموش کرده‌اید؟ + sign_up: + submit: ثبت‌نام + title: ثبت‌نام + subdomain: + title: Subdomain + unlock: + submit: ارسال مجدد دستورالعمل بازگشایی حساب کاربری + title: ارسال مجدد دستورالعمل بازگشایی حساب کاربری + username: + title: نام کاربری + download: 'دریافت:' + edit: ویرایش + edit_model: ویرایش %{model} + empty: خالی + filters: + buttons: + clear: پاک کردن فیلتر + filter: فیلتر + has_many_delete: حذف + has_many_new: اضافه کردن %{model} جدید + has_many_remove: حذف + index_list: + table: جدول + logout: خروج + new_model: "%{model} جدید" + next: بعدی + pagination: + empty: هیچ رکورد %{model} یافت نشد + entry: + one: آیتم + other: آیتم‌ها + multiple: نمایش %{model} %{from} - %{to} از کل %{total} رکورد + multiple_without_total: نمایش %{model} %{from} - %{to} + one: نمایش 1 %{model} + one_page: نمایش همه %{n} %{model} + powered_by: قدرت گرفته از %{active_admin} %{version} + previous: قبلی + sidebars: + filters: فیلتر‌ها + status_tag: + 'no': بدون + unset: بدون + 'yes': بله + view: نمایش diff --git a/config/locales/fi.yml b/config/locales/fi.yml new file mode 100644 index 00000000000..0daaeef830f --- /dev/null +++ b/config/locales/fi.yml @@ -0,0 +1,97 @@ +--- +fi: + active_admin: + access_denied: + message: Sinulla ei ole oikeuksia suorittaa yrittämääsi toimintoa. + any: mikä vain + batch_actions: + action_label: "%{title} Valittu" + button_label: Toimet + default_confirmation: Oletko varma, että haluat tehdä tämän? + delete_confirmation: Oletko varma, että haluat poistaa nämä %{plural_model}:t? + labels: + destroy: Poista + selection_toggle_explanation: "(Vaihda valintaa)" + successfully_destroyed: + one: 1 %{model} poistettu + other: "%{count} %{plural_model}:a poistettu" + blank_slate: + content: Järjestelmässä ei ole yhtään %{resource_name}:ia vielä. + link: Luo ensimmäinen + cancel: Peruuta + comments: + add: Lisää kommentti + author: Luoja + author_type: Luoja-tyyppi + body: Runko + errors: + empty_text: Kommenttia ei pystytty tallentamaan, et kirjoittanut kommenttitekstiä. + no_comments_yet: Ei kommentteja. + resource: Resurssi + resource_type: Resurssityyppi + title_content: Kommentteja (%{count}) + dashboard: Etusivu + delete: Poista + delete_confirmation: Oletko varma, että haluat poistaa tämän? + delete_model: Poista %{model} + details: "%{model} Tiedot" + devise: + change_password: + submit: Vaihda salasana + title: Vaihda salasana + email: + title: Sähköposti + links: + forgot_your_password: Unohtunut salasana? + sign_in: Kirjaudu sisään + sign_in_with_omniauth_provider: Kirjaudu sisään %{provider}:ia käyttäen + login: + remember_me: Muista minut + submit: Kirjaudu sisään + title: Sisäänkirjautuminen + password: + title: Salasana + reset_password: + submit: Resetoi salasana + title: Unohtunut salasana? + subdomain: + title: Subdomain + unlock: + submit: Lähetä ohjeet lukituksen poistoon + title: Lähetä ohjeet lukituksen poistoon + username: + title: Käyttäjänimi + download: 'Lataa:' + edit: Muokkaa + edit_model: Muokkaa %{model} + empty: Tyhjä + filters: + buttons: + clear: Tyhjennä valinnat + filter: Hae + has_many_delete: Poista + has_many_new: Lisää uusi %{model} + has_many_remove: Poista + index_list: + table: Taulukko + logout: Kirjaudu ulos + new_model: Uusi %{model} + next: Seuraava + pagination: + empty: "%{model}:ia ei löytynyt" + entry: + one: syöte + other: syötteet + multiple: Näytetään %{model} %{from} - %{to} (yhteensä %{total}) + multiple_without_total: Näytetään %{model} %{from} - %{to} + one: Näytetään 1 %{model} + one_page: Näytetään kaikki %{n} %{model}:it + powered_by: Käyttää %{active_admin} %{version}:ia + previous: Edellinen + sidebars: + filters: Haku + status_tag: + 'no': Ei + unset: Ei + 'yes': Kyllä + view: Katso diff --git a/config/locales/fr.yml b/config/locales/fr.yml index bf5f9d6c04a..0a1feb204ec 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1,88 +1,148 @@ +--- fr: active_admin: - dashboard: "Tableau de Bord" - dashboard_welcome: - welcome: "Bienvenue dans Active Admin. Ceci est la page par défaut." - call_to_action: "Pour ajouter des sections au tableau de bord, consultez 'app/admin/dashboard.rb'" - view: "Voir" - edit: "Modifier" - delete: "Supprimer" - delete_confirmation: "Êtes-vous certain de vouloir supprimer ceci ?" - new_model: "Nouveau %{model}" - create_model: "Nouveau %{model}" - edit_model: "Modifier %{model}" - update_model: "Modifier %{model}" - delete_model: "Supprimer %{model}" - details: "Détails de %{model}" - cancel: "Annuler" - empty: "Vide" - previous: "Précédent" - next: "Suivant" - download: "Télécharger:" - has_many_new: "Ajouter un nouveau %{model}" - has_many_delete: "Supprimer" - has_many_remove: "Enlever" - filters: - buttons: - filter: "Filtrer" - clear: "Supprimer les filtres" - predicates: - contains: "Contient" - equals: "Egal à" - starts_with: "Commence par" - ends_with: "Se termine par" - greater_than: "Plus grand que" - less_than: "Plus petit que" - main_content: "Veuillez implémenter %{model}#main_content pour afficher le contenu." - logout: "Déconnexion" - powered_by: "Propulsé par %{active_admin} %{version}" - sidebars: - filters: "Filtres" - pagination: - empty: "Aucun %{model} trouvé" - one: "Affichage de 1 %{model}" - one_page: "Affichage de tous les %{n} %{model}" - multiple: "Affichage de %{model} %{from} - %{to} sur un total de %{total}" - entry: - one: "entrée" - other: "entrées" - any: "N'importe lequel" - blank_slate: - content: "Il n'y a pas encore de %{resource_name}." - link: "Créez en un" + access_denied: + message: Vous n'êtes pas autorisé à exécuter cette action + any: N'importe lequel batch_actions: - button_label: "Actions groupées" - delete_confirmation: "Êtes-vous sur de vouloir supprimer ces %{plural_model}? Cette action est irréversible." - succesfully_destroyed: - one: "1 %{model} supprimé" - other: "%{count} %{plural_model} supprimés" - selection_toggle_explanation: "(Inverser la sélection)" - link: "Créer un" action_label: "%{title} les éléments sélectionnés" + button_label: Actions groupées + default_confirmation: Voulez-vous vraiment faire cela ? + delete_confirmation: Voulez-vous vraiment supprimer ces %{plural_model} ? labels: - destroy: "Supprimer" + destroy: Supprimer + selection_toggle_explanation: "(Inverser la sélection)" + successfully_destroyed: + one: 1 %{model} supprimé(e) + other: "%{count} %{plural_model} supprimé(e)s" + blank_slate: + content: Il n'y a pas encore de %{resource_name}. + link: Créez en un + cancel: Annuler comments: - body: "Corps" - author: "Auteur" - title: "Commentaire" - add: "Ajouter un commentaire" - resource: "Ressource" - no_comments_yet: "Aucun commentaire actuellement" - title_content: "Commentaires (%{count})" + add: Ajouter un commentaire + author: Auteur + author_missing: Anonyme + author_type: Profil de l'auteur + body: Corps + created_at: Créé le + delete: Supprimer ce commentaire + delete_confirmation: Voulez-vous vraiment supprimer ce commentaire ? errors: - empty_text: "Le commentaire n'a pas été enregistré puisque le texte était vide." + empty_text: Le commentaire n'a pas été enregistré puisque le texte était vide. + no_comments_yet: Aucun commentaire actuellement + resource: Ressource + resource_type: Type de ressource + title_content: Tous les commentaires (%{count}) + create_another: Créer autre %{model} + dashboard: Tableau de bord + delete: Supprimer + delete_confirmation: Voulez-vous vraiment supprimer ceci ? + delete_model: Supprimer %{model} + details: Détails de %{model} devise: - login: - title: "login" - remember_me: "Se souvenir de moi" - submit: "login" - reset_password: - title: "Vous avez oublié votre mot de passe?" - submit: "Réinitialiser mon mot de passe" change_password: - title: "Changez votre mot de passe" - submit: "Changer mon mot de passe" + submit: Changer mon mot de passe + title: Changez votre mot de passe + email: + title: Email links: - sign_in: "Connectez-vous" - forgot_your_password: "Vous avez oublié votre mot de passe?" - sign_in_with_omniauth_provider: "Connectez-vous avec %{provider}" + forgot_your_password: Vous avez oublié votre mot de passe ? + resend_confirmation_instructions: Renvoyer les instructions de confirmation + resend_unlock_instructions: Renvoyer les informations de déverrouillage + sign_in: Connectez-vous + sign_in_with_omniauth_provider: Connectez-vous avec %{provider} + sign_up: Inscrivez-vous + login: + remember_me: Garder ma session ouverte + submit: Se connecter + title: Connexion + password: + title: Mot de passe + password_confirmation: + title: Confirmer le mot de passe + resend_confirmation_instructions: + submit: Renvoyer les instructions de confirmation + title: Renvoyer les instructions de confirmation + reset_password: + submit: Réinitialiser mon mot de passe + title: Vous avez oublié votre mot de passe ? + sign_up: + submit: S'inscrire + title: S'inscrire + subdomain: + title: Sous-domaine + unlock: + submit: Renvoyer les informations de déverrouillage + title: Renvoyer les informations de déverrouillage + username: + title: Nom d'utilisateur + download: 'Télécharger :' + edit: Modifier + edit_model: Modifier %{model} + empty: Vide + filters: + buttons: + clear: Supprimer les filtres + filter: Filtrer + predicates: + from: De + to: À + has_many_delete: Supprimer + has_many_new: Ajouter un(e) %{model} + has_many_remove: Enlever + index_list: + table: Tableau + logout: Déconnexion + move: Déplacer + new_model: Créer %{model} + next: Suivant + pagination: + empty: Aucun(e) %{model} trouvé(e) + entry: + one: entrée + other: entrées + multiple: Affichage de %{from}-%{to} sur %{total} + multiple_without_total: Affichage de %{from}-%{to} + next: Suivant + one: Affichage de 1 sur 1 + one_page: Affichage de tous les %{n} + per_page: 'Par page ' + previous: Précédent + truncate: "…" + powered_by: Propulsé par %{active_admin} %{version} + previous: Précédent + scopes: + all: Tout + search_status: + no_current_filters: Aucun filtre appliqué + title: Recherche active + title_with_scope: Recherche active pour %{name} + sidebars: + filters: Filtres + search_status: Statut de la recherche + status_tag: + 'no': Non + unset: Inconnu + 'yes': Oui + toggle_dark_mode: Bascule de mode sombre + toggle_main_navigation_menu: Bascule de menu principal + toggle_section: Bascule de section + toggle_user_menu: Bascule de menu utilisateur + view: Voir + activerecord: + attributes: + active_admin/comment: + author_type: Type d'auteur + body: Corps + created_at: Créé le + namespace: Espace de nom + resource_type: Type de ressource + updated_at: Mis à jour le + models: + active_admin/comment: + one: Commentaire + other: Commentaires + comment: + one: Commentaire + other: Commentaires diff --git a/config/locales/he.yml b/config/locales/he.yml index 36c3dd5539a..fbbca6d504b 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -1,90 +1,117 @@ +--- he: active_admin: - dashboard: פנל ניהול - dashboard_welcome: - welcome: "ברוכים הבאים לאקטיב אדמין. זהו פנל הניהול" - call_to_action: "כדי להוסיף אזורים בפנל הניהול, אנא בדוק את, 'app/admin/dashboard.rb'" - view: "צפייה" - edit: "עריכה" - delete: "מחיקה" - delete_confirmation: "האם אתה בטוח שאתה רוצה למחוק את זה?" - new_model: "%{model} חדש" - create_model: "יצירת %{model} חדש" - edit_model: "ערוך %{model}" - update_model: "עדכון %{model}" - delete_model: "מחיקת %{model}" - details: "פרטים על %{model}" - cancel: "ביטול" - empty: "ריק" - previous: "הקודם" - next: "הבא" - download: "הורד:" - has_many_new: "הוספת %{model} חדש" - has_many_delete: "מחיקה" - has_many_remove: "להסיר" - filters: - buttons: - filter: "סינון" - clear: "איפוס שדות" - predicates: - contains: "מכיל" - equals: "שווה ל" - starts_with: "מתחיל עם" - ends_with: "מסתיים ב" - greater_than: "גדול מ" - less_than: "פחות מ" - main_content: "Please implement %{model}#main_content to display content." - logout: "התנתקות" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "סינון" - pagination: - empty: "אין %{model} בנמצא" - one: "מציג 1 %{model}" - one_page: "הצגת כל %{n} %{model}" - multiple: "מציג %{model} %{from} - %{to} מתוך %{total} בסך הכל" - entry: - one: "רשומה בודדה" - other: "רשומות" - any: "Any" - blank_slate: - content: "כרגע אין עוד אף %{resource_name}." - link: "צור אחד" + access_denied: + message: אינך רשאי לבצע פעולה זו. + any: Any batch_actions: - button_label: "פעולות מרובות" - delete_confirmation: "האם הנך בטוח שאתה רוצה למרוח את %{plural_model}? לא תוכל לבטל את המחיקה." - succesfully_destroyed: - one: "1 %{model} נמחק בהצלחה" - few: "%{count} %{plural_model} נמחק בהצלחה" - many: "%{count} %{plural_model} נמחק בהצלחה" - other: "%{count} %{plural_model} נמחק בהצלחה" - selection_toggle_explanation: "(שינוי בחירה)" - link: "צור" action_label: "%{title} נבחר" + button_label: פעולות מרובות + default_confirmation: אתה בטוח שאתה רוצה לעשות את זה? + delete_confirmation: האם הנך בטוח שאתה רוצה למרוח את %{plural_model}? labels: - destroy: "מחק" + destroy: מחק + selection_toggle_explanation: "(שינוי בחירה)" + successfully_destroyed: + one: 1 %{model} נמחק בהצלחה + other: "%{count} %{plural_model} נמחק בהצלחה" + blank_slate: + content: כרגע אין עוד אף %{resource_name}. + link: צור אחד + cancel: ביטול comments: - body: "תוכן" - author: 'נוצר ע"י' - title: "תגובה" - add: "הוסף תגובה" - resource: "Resource" - no_comments_yet: "אין עדיין תגובות." - title_content: "תגובות (%{count})" + add: הוסף תגובה + author: נוצר ע"י + author_missing: אנונימי + author_type: סוג מחבר + body: תוכן + created_at: נוצר + delete: מחק תגובה + delete_confirmation: האם אתה בטוח שברצונך למחוק תגובה זאת? errors: - empty_text: "התגובה לא נשמרה, שדה התוכן ריק." + empty_text: התגובה לא נשמרה, שדה התוכן ריק. + no_comments_yet: אין עדיין תגובות. + resource: רשומה + resource_type: סוג רישום + title_content: תגובות (%{count}) + create_another: צור עוד %{model} + dashboard: פנל ניהול + delete: מחיקה + delete_confirmation: האם אתה בטוח שאתה רוצה למחוק את זה? + delete_model: מחיקת %{model} + details: פרטים על %{model} devise: - login: - title: "כניסה" - remember_me: "זכור אותי" - submit: "הכנס" - reset_password: - title: "שכחת סיסמא?" - submit: "אפס את הסיסמא שלי" change_password: - title: "שנה את הסיסמא שלך" - submit: "שנה את הסיסמא שלי" + submit: שנה את הסיסמא שלי + title: שנה את הסיסמא שלך + email: + title: כתובת דוא״ל links: - sign_in: "כניסה" - forgot_your_password: "שכחת את הסיסמא שלך?" + forgot_your_password: שכחת את הסיסמא שלך? + resend_confirmation_instructions: שלח שוב הוראות אישור + resend_unlock_instructions: שלח שוב הוראות שיחרור + sign_in: כניסה sign_in_with_omniauth_provider: "%{provider} היכנס עם" + sign_up: הרשמה + login: + remember_me: זכור אותי + submit: הכנס + title: כניסה + password: + title: סיסמא + password_confirmation: + title: אישור סיסמא + resend_confirmation_instructions: + submit: שלח שוב הוראות אישור + title: שלח שוב הוראות אישור + reset_password: + submit: אפס את הסיסמא שלי + title: שכחת סיסמא? + sign_up: + submit: הרשמה + title: הרשמה + subdomain: + title: תת-דומיין + unlock: + submit: שלח שוב הוראות שיחרור + title: שלח שוב הוראות שיחרור + username: + title: שם משתמש + download: 'הורד:' + edit: עריכה + edit_model: ערוך %{model} + empty: ריק + filters: + buttons: + clear: איפוס שדות + filter: סינון + has_many_delete: מחיקה + has_many_new: הוספת %{model} חדש + has_many_remove: להסיר + index_list: + table: טבלה + logout: התנתקות + new_model: "%{model} חדש" + next: הבא + pagination: + empty: אין %{model} בנמצא + entry: + one: רשומה בודדה + other: רשומות + multiple: מציג %{model} %{from} - %{to} מתוך %{total} בסך הכל + multiple_without_total: מציג %{model} %{from} - %{to} + one: מציג 1 %{model} + one_page: הצגת כל %{n} %{model} + per_page: 'בדף: ' + powered_by: ממונע בעזרת %{active_admin} %{version} + previous: הקודם + search_status: + no_current_filters: ללא + sidebars: + filters: סינון + search_status: מצב החיפוש + status_tag: + 'no': לא + unset: לא + 'yes': כן + view: צפייה diff --git a/config/locales/hr.yml b/config/locales/hr.yml index 8cf4cfce257..11756d35244 100644 --- a/config/locales/hr.yml +++ b/config/locales/hr.yml @@ -1,92 +1,108 @@ +--- hr: active_admin: - dashboard: "Upravljačka ploča" - dashboard_welcome: - welcome: "Dobrodošli u Active Admin. Ovo je početna upravljačka ploča." - call_to_action: "Da bi ste dodali nove odjeljke na upravljačku ploču, pogledajte 'app/admin/dashboard.rb'" - view: "Pregledaj" - edit: "Uredi" - delete: "Obriši" - delete_confirmation: "Jeste li sigurni da želite ovo obrisati?" - new_model: "Novi %{model}" - create_model: "Izradi %{model}" - edit_model: "Uredi %{model}" - update_model: "Spremi %{model}" - delete_model: "Obriši %{model}" - details: "%{model} detalji" - cancel: "Odustani" - empty: "Prazno" - previous: "Prijašnji" - next: "Slijedeći" - download: "Spremi na računalo:" - has_many_new: "Dodaj novi %{model}" - has_many_delete: "Obriši" - has_many_remove: "Ukloniti" - filters: - buttons: - filter: "Filtriraj" - clear: "Očisti filtere" - predicates: - contains: "Sadrži" - equals: "Jednako" - starts_with: "počinje s" - ends_with: "Završava sa" - greater_than: "Veće Od" - less_than: "Manje Od" - main_content: "Molim Vas, implementirajte %{model}#main_content da bi ste prikazali sadržaj." - logout: "Odjavi se" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Filtriranje" - pagination: - empty: "Nije pronađen niti jedan %{model}." - one: "Prikazan 1 %{model}" - one_page: "Prikazano svih %{n} %{model}" - multiple: "Prikazani %{model} %{from} - %{to} od ukupno %{total}" - entry: - one: "zapis" - few: "zapisa" - many: "zapisa" - other: "zapisa" - any: "Bilo koji" - blank_slate: - content: "Još uvijek ne postoji niti jedan zapis tipa %{resource_name}." - link: "Izradi jedan" + access_denied: + message: Nemaš dopuštenja. + any: Bilo koji batch_actions: - button_label: "Grupne akcije" - delete_confirmation: "Jeste li sigurni da želite obrisati %{plural_model}? Obrisano nije moguće poništiti." - succesfully_destroyed: - one: "Uspješno je obrisan 1 %{model}" - few: "Uspješno su obrisana %{count} %{plural_model}" - many: "Uspješno je obrisano %{count} %{plural_model}" - other: "Uspješno je obrisano %{count} %{plural_model}" - selection_toggle_explanation: "(Izmjeni odabir)" - link: "Izradi jedan" action_label: "%{title} označene" + button_label: Grupne akcije + default_confirmation: Jeste li sigurni da želite to učiniti? + delete_confirmation: Jeste li sigurni da želite obrisati %{plural_model}? labels: - destroy: "Obriši" + destroy: Obriši + selection_toggle_explanation: "(Izmijeni odabir)" + successfully_destroyed: + few: Uspješno su obrisana %{count} %{plural_model} + many: Uspješno je obrisano %{count} %{plural_model} + one: Uspješno je obrisan 1 %{model} + other: Uspješno je obrisano %{count} %{plural_model} + blank_slate: + content: Još uvijek ne postoji niti jedan zapis tipa %{resource_name}. + link: Izradi jedan + cancel: Odustani comments: - body: "Sadržaj" - author: "Autor" - title: "Komentar" - add: "Dodaj komentar" - resource: "Resource" - no_comments_yet: "Još nema komentara." - title_content: "Komentari (%{count})" + add: Dodaj komentar + author: Autor + author_missing: Anoniman + author_type: Tip autora + body: Sadržaj errors: - empty_text: "Komentar nije spremljen, sadržaj je prazan." + empty_text: Komentar nije spremljen, sadržaj je prazan. + no_comments_yet: Još nema komentara. + resource: Objekt + resource_type: Tip objekta + title_content: Komentari (%{count}) + dashboard: Upravljačka ploča + delete: Obriši + delete_confirmation: Jeste li sigurni da želite ovo obrisati? + delete_model: Obriši %{model} + details: "%{model} detalji" devise: - login: - title: "Prijava" - remember_me: "Zapamti me" - submit: "Prijavi se" - reset_password: - title: "Zaboravljena lozinka?" - submit: "Resetiraj lozinku" change_password: - title: "Izmjena lozinke" - submit: "Izmjeni lozinku" + submit: Izmijeni lozinku + title: Izmjena lozinke + email: + title: Email links: - sign_in: "Prijavi se" - forgot_your_password: "Zaboravljena lozinka?" - sign_in_with_omniauth_provider: "Prijavite se za %{provider}" + forgot_your_password: Zaboravljena lozinka? + sign_in: Prijavi se + sign_in_with_omniauth_provider: Prijavite se za %{provider} + login: + remember_me: Zapamti me + submit: Prijavi se + title: Prijava + password: + title: Lozinka + resend_confirmation_instructions: + submit: Pošalji + title: Ponovno slanje uputstva za potvrdu + reset_password: + submit: Resetiraj lozinku + title: Zaboravljena lozinka? + sign_up: + submit: Registruj + title: Registracija + subdomain: + title: Poddomena + unlock: + submit: Pošalji + title: Ponovno slanje uputstva za otključavanje + username: + title: Korisničko ime + download: 'Spremi na računalo:' + edit: Uredi + edit_model: Uredi %{model} + empty: Prazno + filters: + buttons: + clear: Očisti filtere + filter: Filtriraj + has_many_delete: Obriši + has_many_new: Dodaj novi %{model} + has_many_remove: Ukloniti + index_list: + table: Tabela + logout: Odjavi se + new_model: Novi %{model} + next: Sljedeći + pagination: + empty: Nije pronađen niti jedan %{model}. + entry: + few: zapisa + many: zapisa + one: zapis + other: zapisa + multiple: Prikazani %{model} %{from} - %{to} od ukupno %{total} + multiple_without_total: Prikazani %{model} %{from} - %{to} + one: Prikazan 1 %{model} + one_page: Prikazano svih %{n} %{model} + powered_by: Powered by %{active_admin} %{version} + previous: Prijašnji + sidebars: + filters: Filtriranje + status_tag: + 'no': Nema + unset: Nema + 'yes': Da + view: Pregledaj diff --git a/config/locales/hu.yml b/config/locales/hu.yml index f791ffb17d8..aac702853af 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -1,91 +1,89 @@ +--- hu: active_admin: - dashboard: Vezérlőpult - dashboard_welcome: - welcome: "Üdvözöljük az Active Admin felületén. Ez a vezérlőpult kezdőlapja" - call_to_action: "Elemek hozzáadásához nézze meg a 'app/admin/dashboard.rb' fájlt" - view: "Megtekintés" - edit: "Szerkesztés" - delete: "Törlés" - delete_confirmation: "Biztosan törli ezt az elemet?" - new_model: "Új %{model}" - create_model: "%{model} létrehozása" - edit_model: "%{model} módosítása" - update_model: "%{model} módosítása" - delete_model: "%{model} törlése" - details: "%{model} részletei" - cancel: "Mégsem" - empty: "Üres" - previous: "Előző" - next: "Következő" - download: "Letöltés:" - has_many_new: "Új %{model} hozzáadása" - has_many_delete: "Törlés" - has_many_remove: "Eltávolít" - filters: - buttons: - filter: "Szűrés" - clear: "Feltételek törlése" - predicates: - contains: "Tartalmazza" - equals: "Pontosan" - starts_with: "kezdődik" - ends_with: "végződik" - greater_than: "Nagyobb, mint" - less_than: "Kisebb, mint" - main_content: "Kérem, implementálja a %{model}#main_content metódust a tartalom megjelenítéséhez." - logout: "Kilépés" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Szűrők" - pagination: - empty: "Nincs több %{model}" - one: "Egy %{model} megjelenítése" - one_page: "Az összes (%{n} db) %{model} megjelenítése" - multiple: "%{model} listájának megjelenítése, %{from} - %{to}/%{total} " - entry: - one: "elem" - other: "elem" - any: "Összes" - blank_slate: - content: "Még nincs létrehozva %{resource_name}." - link: "Létrehozás most" + any: Összes batch_actions: - button_label: "Tömeges műveletek" - delete_confirmation: "Biztosan törli ezeket a %{plural_model}? Később nem lehet visszavonni." - succesfully_destroyed: - one: "1 %{model} sikeresen törölve" - other: "%{count} %{plural_model} sikeresen törölve" - selection_toggle_explanation: "(Kijelölés megfordítása)" - link: "Létrehozás" action_label: "%{title} kiválasztva" + button_label: Tömeges műveletek + default_confirmation: Biztos vagy benne, hogy a ön akar-hoz csinál ez? + delete_confirmation: Biztosan törli ezeket a %{plural_model}? labels: - destroy: "Törlés" + destroy: Törlés + selection_toggle_explanation: "(Kijelölés megfordítása)" + successfully_destroyed: + one: 1 %{model} sikeresen törölve + other: "%{count} %{plural_model} sikeresen törölve" + blank_slate: + content: Még nincs létrehozva %{resource_name}. + link: Létrehozás most + cancel: Mégsem comments: - body: "Törzs" - author: "Szerző" - title: "Hozzászólás" - add: "Új hozzászólás" - resource: "Erőforrás" - no_comments_yet: "Nincsenek hozzászólások." - title_content: "%{count} hozzászólás" + add: Új hozzászólás + author: Szerző + body: Törzs errors: - empty_text: "A hozzászólás nem lett mentve, a törzs nem lehet üres." + empty_text: A hozzászólás nem lett mentve, a törzs nem lehet üres. + no_comments_yet: Nincsenek hozzászólások. + resource: Erőforrás + title_content: "%{count} hozzászólás" + dashboard: Vezérlőpult + delete: Törlés + delete_confirmation: Biztosan törli ezt az elemet? + delete_model: "%{model} törlése" + details: "%{model} részletei" devise: + change_password: + submit: Jelszó módosítása + title: A jelszó módosítása + links: + forgot_your_password: Elfelejtette a jelszavát? + sign_in: Bejelentkezés + sign_in_with_omniauth_provider: Jelentkezzen be a %{provider} login: - title: "Bejelentkezés" - remember_me: "Emlékezz rám" - submit: "Belépés" + remember_me: Emlékezz rám + submit: Belépés + title: Bejelentkezés + resend_confirmation_instructions: + submit: Megerősítő levél újraküldése + title: Megerősítő levél újraküldése reset_password: - title: "Elfelejtette a jelszavát?" - submit: "Visszaállítása a jelszót" - change_password: - title: "A jelszó módosítása" - submit: "Jelszó módosítása" + submit: Jelszó visszaállítása + title: Elfelejtette a jelszavát? unlock: - title: "Újraküldés unlock utasítások" - submit: "Újraküldés unlock utasítások" - links: - sign_in: "Bejelentkezés" - forgot_your_password: "Elfelejtette a jelszavát?" - sign_in_with_omniauth_provider: "Jelentkezzen be a %{provider}" + submit: Újraküldés unlock utasítások + title: Újraküldés unlock utasítások + download: 'Letöltés:' + edit: Szerkesztés + edit_model: "%{model} módosítása" + empty: Üres + filters: + buttons: + clear: Feltételek törlése + filter: Szűrés + predicates: + from: "-tól" + to: "-ig" + has_many_delete: Törlés + has_many_new: Új %{model} hozzáadása + has_many_remove: Eltávolít + logout: Kilépés + new_model: Új %{model} + next: Következő + pagination: + empty: Nincs több %{model} + entry: + one: elem + other: elem + multiple: "%{model} listájának megjelenítése, %{from} - %{to}/%{total} " + multiple_without_total: "%{model} listájának megjelenítése, %{from} - %{to} " + one: "Egy %{model} megjelenítése" + one_page: "Az összes (%{n} db) %{model} megjelenítése" + powered_by: Powered by %{active_admin} %{version} + previous: Előző + sidebars: + filters: Szűrők + status_tag: + 'no': Nem + unset: Nem + 'yes': Igen + view: Megtekintés diff --git a/config/locales/id.yml b/config/locales/id.yml new file mode 100644 index 00000000000..862a32b2e01 --- /dev/null +++ b/config/locales/id.yml @@ -0,0 +1,113 @@ +--- +id: + active_admin: + access_denied: + message: Anda tidak diperkenankan melakukan aksi tersebut. + any: Apapun + batch_actions: + action_label: "%{title} terpilih" + button_label: Tindakan Serentak + default_confirmation: Apakah anda yakin akan melakukan ini? + delete_confirmation: Apakah anda yakin akan menghapus %{plural_model}? + labels: + destroy: Hapus + selection_toggle_explanation: "(Tampilkan Pilihan)" + successfully_destroyed: + one: Berhasil menghapus %{model} + other: Berhasil menghapus %{count} %{plural_model} + blank_slate: + content: "%{resource_name} masih belum ada sama sekali." + link: Tambah data + cancel: Batal + comments: + add: Tambah Komentar + author: Penulis + author_missing: Anonim + author_type: Tipe Penulis + body: Isi + created_at: Dibuat + delete: Hapus Komentar + delete_confirmation: Apakah anda yakin akan menghapus komentar tersebut? + errors: + empty_text: Komentar tak bisa disimpan, text tidak boleh dikosongi. + no_comments_yet: Belum ada komentar sama sekali. + resource: Resource + resource_type: Jenis Resource + title_content: Komentar (%{count}) + dashboard: Dashboard + delete: Hapus + delete_confirmation: Apakah anda yakin ingin menghapus data ini? + delete_model: Hapus %{model} + details: Detail %{model} + devise: + change_password: + submit: Kirimkan instruksi pengaturan ulang password + title: " - Atur Ulang Password" + email: + title: Email + links: + forgot_your_password: Lupa password? + resend_confirmation_instructions: Kirim lagi instruksi konfirmasi akun + resend_unlock_instructions: Kirim instruksi pengaktifan kembali akun + sign_in: Masuk + sign_in_with_omniauth_provider: Daftar melalui %{provider} + sign_up: Daftar + login: + remember_me: Ingat saya + submit: Masuk + title: " - Masuk" + password: + title: Password + resend_confirmation_instructions: + submit: Kirimkan lagi instruksi konfirmasi akun + title: " - Kirim Lagi Instruksi Konfirmasi Akun" + reset_password: + submit: Atur ulang password + title: " - Form Atur Ulang Password" + sign_up: + submit: Daftar + title: " - Daftar" + subdomain: + title: Subdomain + unlock: + submit: Kirimkan instruksi pengaktifan kembali akun + title: " - Kirim Instruksi Pengaktifan Kembali Akun" + username: + title: Username + download: 'Unduh:' + edit: Ubah + edit_model: Ubah %{model} + empty: Kosong + filters: + buttons: + clear: Hapus Filters + filter: Filter + has_many_delete: Hapus + has_many_new: Tambah %{model} baru + has_many_remove: Hapus + index_list: + table: Tabel + logout: Keluar + new_model: Tambah %{model} baru + next: Berikutnya + pagination: + empty: Tidak ada %{model} yang bisa ditemukan + entry: + one: data + other: data + multiple: Menampilkan %{from} - %{to} dari %{total} keseluruhan %{model} + multiple_without_total: Menampilkan %{from} - %{to} %{model} + one: Menampilkan 1 %{model} + one_page: Menampilkan semua %{n} %{model} + powered_by: Dibuat dengan %{active_admin} %{version} + previous: Sebelumnya + search_status: + no_current_filters: Tidak ada + sidebars: + filters: Filter + search_status: Status Pencarian + status_tag: + 'no': Tidak + unset: Tidak + 'yes': Ya + view: Lihat diff --git a/config/locales/it.yml b/config/locales/it.yml index 6a8e6eb13c1..10d575f2516 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -1,88 +1,148 @@ +--- it: active_admin: - dashboard: Dashboard - dashboard_welcome: - welcome: "Benvenuti in Active Admin. Questa è la pagina dashboard di default." - call_to_action: "Per aggiungere sezioni alla dashboard controlla il file 'app/admin/dashboard.rb'" - view: "Mostra" - edit: "Modifica" - delete: "Rimuovi" - delete_confirmation: "Sei sicuro di volerlo rimuovere?" - new_model: "Aggiungi %{model}" - create_model: "Aggiungi %{model}" - edit_model: "Modifica %{model}" - update_model: "Modifica %{model}" - delete_model: "Rimuovi %{model}" - details: "Dettagli %{model}" - cancel: "Annulla" - empty: "Vuoto" - previous: "Precedente" - next: "Prossimo" - download: "Scarica:" - has_many_new: "Aggiungi nuovo/a %{model}" - has_many_delete: "Rimuovi" - has_many_remove: "Rimuovi" - filters: - buttons: - filter: "Filtra" - clear: "Rimuovi filtri" - predicates: - contains: "Contiene" - equals: "Uguale a" - starts_with: "Inizia con" - ends_with: "Finisce con" - greater_than: "Maggiore di" - less_than: "Minore di" - main_content: "Devi implemetare %{model}#main_content per mostrarne il contenuto." - logout: "Esci" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Filtri" - pagination: - empty: "Nessun %{model} trovato" - one: "Sto mostrando 1 %{model}" - one_page: "Sto mostrando %{n} %{model}. Lista completa." - multiple: "Sto mostrando %{model} %{from} - %{to} di %{total} in totale" - entry: - one: "voce" - other: "voci" - any: "Qualsiasi" - blank_slate: - content: "Non sono presenti %{resource_name}" - link: "Crea nuovo/a" + access_denied: + message: Non hai le autorizzazioni necessarie per eseguire questa azione. + any: Qualsiasi batch_actions: - button_label: "Azioni multiple" - delete_confirmation: "Sei sicuro di volere cancellare %{plural_model}? Non sarà possibile annulare questa modifica." - succesfully_destroyed: - one: "Eliminato con successo 1 %{model}" - other: "Eliminati con successo %{count} %{plural_model}" - selection_toggle_explanation: "(Toggle Selection)" - link: "Crea uno" action_label: "%{title} Selezionati" + button_label: Azioni multiple + default_confirmation: Sei sicuro di che voler proseguire? + delete_confirmation: Sei sicuro di volere cancellare %{plural_model}? labels: - destroy: "Elimina" + destroy: Elimina + selection_toggle_explanation: "(cambia selezione)" + successfully_destroyed: + one: Eliminato con successo 1 %{model} + other: Eliminati con successo %{count} %{plural_model} + blank_slate: + content: Non sono presenti %{resource_name} + link: Crea nuovo/a + cancel: Annulla comments: - body: "Corpo" - author: "Autore" - title: "Commento" - add: "Aggiungi Commento" - resource: "Risorsa" - no_comments_yet: "Nessun commento." - title_content: "Commenti (%{count})" + add: Aggiungi Commento + author: Autore + author_missing: Anonimo + author_type: Tipo di Autore + body: Corpo + created_at: Creato il + delete: Cancella Commento + delete_confirmation: Sei sicuro di voler cancellare questo commento? errors: - empty_text: "Il commento non può essere salvato, il testo è vuoto." + empty_text: Il commento non può essere salvato, il testo è vuoto. + no_comments_yet: Nessun commento. + resource: Risorsa + resource_type: Tipo di risorsa + title_content: Commenti (%{count}) + create_another: Crea un altro %{model} + dashboard: Dashboard + delete: Rimuovi + delete_confirmation: Sei sicuro di volerlo rimuovere? + delete_model: Rimuovi %{model} + details: Dettagli %{model} devise: - login: - title: "Entra" - remember_me: "Ricordami" - submit: "Entra" - reset_password: - title: "Dimenticato la password?" - submit: "Reimposta la tua password" change_password: - title: "Cambia la tua password" - submit: "Cambia la mia password" + submit: Cambia la mia password + title: Cambia la tua password + email: + title: Email links: - sign_in: "Entra" - forgot_your_password: "Dimenticato la password?" - sign_in_with_omniauth_provider: "Collegati a %{provider}" + forgot_your_password: Dimenticato la password? + resend_confirmation_instructions: Invia di nuovo le istruzioni per la conferma + resend_unlock_instructions: Invia di nuovo le istruzioni per lo sblocco + sign_in: Entra + sign_in_with_omniauth_provider: Collegati a %{provider} + sign_up: Iscriviti + login: + remember_me: Ricordami + submit: Entra + title: Entra + password: + title: Password + password_confirmation: + title: Conferma password + resend_confirmation_instructions: + submit: Invia di nuovo le istruzioni per la conferma + title: Invia di nuovo le istruzioni per la conferma + reset_password: + submit: Reimposta la tua password + title: Dimenticato la password? + sign_up: + submit: Iscriviti + title: Iscriviti + subdomain: + title: Sottodominio + unlock: + submit: Invia di nuovo le istruzioni per sbloccare + title: Invia di nuovo le istruzioni per sbloccare + username: + title: Nome Utente + download: 'Scarica:' + edit: Modifica + edit_model: Modifica %{model} + empty: Vuoto + filters: + buttons: + clear: Rimuovi filtri + filter: Filtra + predicates: + from: Da + to: A + has_many_delete: Rimuovi + has_many_new: Aggiungi nuovo/a %{model} + has_many_remove: Rimuovi + index_list: + table: Tabella + logout: Esci + move: Sposta + new_model: Aggiungi %{model} + next: Prossimo + pagination: + empty: Nessun risultato per %{model} + entry: + one: voce + other: voci + multiple: Mostrando %{from}-%{to} di %{total} + multiple_without_total: Mostrando %{from}-%{to} + next: Successiva + one: Mostrando 1 di 1 + one_page: Mostrando %{n} %{model}. Lista completa. + per_page: 'Oggetti per pagina: ' + previous: Precedente + truncate: "…" + powered_by: Powered by %{active_admin} %{version} + previous: Precedente + scopes: + all: Tutti + search_status: + no_current_filters: Nessun filtro applicato + title: Ricerca corrente + title_with_scope: Ricerca corrente per %{name} + sidebars: + filters: Filtri + search_status: Informazioni sulla ricerca + status_tag: + 'no': 'No' + unset: Vuoto + 'yes': Sì + toggle_dark_mode: Attiva/Disattiva tema scuro + toggle_main_navigation_menu: Espandi/Riduci menu di navigazione principale + toggle_section: Espandi/Riduci sezione + toggle_user_menu: Espandi/Riduci menu utente + view: Mostra + activerecord: + attributes: + active_admin/comment: + author_type: Tipo di Autore + body: Corpo + created_at: Creato il + namespace: Namespace + resource_type: Tipo di risorsa + updated_at: Aggiornato il + models: + active_admin/comment: + one: Commento + other: Commenti + comment: + one: Commento + other: Commenti diff --git a/config/locales/ja.yml b/config/locales/ja.yml index e5c7ec23545..54cfb078fde 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -1,86 +1,122 @@ +--- ja: active_admin: - dashboard: "ダッシュボード" - dashboard_welcome: - welcome: "Active Admin へようこそ。ダッシュボードの初期ページを表示しています。" - call_to_action: "ダッシュボードに項目を追加するために 'app/admin/dashboard.rb' を編集してください。" - view: "閲覧" - edit: "編集" - delete: "削除" - delete_confirmation: "本当に削除しますか?" - new_model: "%{model} を作成する" - create_model: "%{model} を作成する" - edit_model: "%{model} を編集する" - update_model: "%{model} を更新する" + access_denied: + message: アクションを実行する権限がありません + any: 任意 + batch_actions: + action_label: 選択した行を%{title} + button_label: 一括操作 + default_confirmation: 本当によろしいですか? + delete_confirmation: "%{plural_model} を削除してもよろしいですか?" + labels: + destroy: 削除する + selection_toggle_explanation: "(選択)" + successfully_destroyed: + one: 1件の %{model} を削除しました + other: "%{count}件の %{plural_model} を削除しました" + blank_slate: + content: "%{resource_name} はまだありません。" + link: 作成する + cancel: 取り消す + comments: + add: コメントを追加 + author: 作成者 + author_missing: 匿名ユーザ + author_type: 作成者種別 + body: 本文 + created_at: 作成日 + delete: コメントを削除 + delete_confirmation: 本当にコメントを削除しますか? + errors: + empty_text: テキストが空のため、コメントは保存されませんでした。 + no_comments_yet: コメントはまだありません。 + resource: リソース + resource_type: リソース種別 + title_content: コメント (%{count}) + create_another: "%{model} を続けて作成する" + dashboard: ダッシュボード + delete: 削除 + delete_confirmation: 本当に削除しますか? delete_model: "%{model} を削除する" details: "%{model} の詳細" - cancel: "取り消す" - empty: "空" - previous: "前" - next: "次" - download: "ダウンロード:" - has_many_new: "新規に %{model} を追加する" - has_many_delete: "削除する" - has_many_remove: "削除する" + devise: + change_password: + submit: パスワードを変更する + title: パスワードを変更する + email: + title: メールアドレス + links: + forgot_your_password: パスワードをお忘れですか? + resend_confirmation_instructions: ユーザ確認手順を再送する + resend_unlock_instructions: ロックの解除方法を再送する + sign_in: サインイン + sign_in_with_omniauth_provider: "%{provider}のアカウントを使ってログイン" + sign_up: ユーザ登録 + login: + remember_me: 次回から自動的にログイン + submit: ログイン + title: ログイン + password: + title: パスワード + resend_confirmation_instructions: + submit: 確認方法を再送信する + title: 確認方法を再送信する + reset_password: + submit: パスワードをリセットする + title: パスワードをお忘れですか? + sign_up: + submit: 登録 + title: 登録 + subdomain: + title: サブドメイン + unlock: + submit: ロックの解除方法を送る + title: ロックの解除方法を送る + username: + title: ユーザ名 + download: 'ダウンロード:' + edit: 編集 + edit_model: "%{model} を編集する" + empty: 空 filters: buttons: - filter: "絞り込む" - clear: "条件を削除する" + clear: 条件を削除する + filter: 絞り込む predicates: - contains: "含まれています" - equals: "等しい" - starts_with: "で始まる" - ends_with: "で終わる" - greater_than: "より大きい" - less_than: "より小さい" - main_content: "内容を表示するために %{model}#main_content を実装してください。" - logout: "ログアウト" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "検索条件" + from: 開始 + to: 終了 + has_many_delete: 削除する + has_many_new: "%{model} を追加する" + has_many_remove: 削除する + index_list: + table: テーブル + logout: ログアウト + new_model: "%{model} を作成する" + next: 次 pagination: empty: "%{model} は見つかりませんでした" + entry: + one: レコード + other: レコード + multiple: 全 %{total} 件中 %{from} - %{to} 件の %{model} を表示しています + multiple_without_total: "%{from} - %{to} 件の %{model} を表示しています" one: "1 件の %{model} を表示しています" one_page: "全 %{n} 件の %{model} を表示しています" - multiple: "全 %{total} 件中 %{from} - %{to} 件の %{model} を表示しています" - entry: レコード - any: "任意" - blank_slate: - content: "%{resource_name} はまだありません。" - link: "作成する" - batch_actions: - button_label: "一括操作" - delete_confirmation: "%{plural_model} を削除してもよろしいですか? この操作は取り消すことができません。" - succesfully_destroyed: - one: "1件の %{model} を削除しました" - other: "%{count}件の %{plural_model} を削除しました" - selection_toggle_explanation: "(選択)" - link: "作成する" - action_label: "選択した行を%{title}" - labels: - destroy: "削除する" - comments: - body: "本文" - author: "作成者" - title: "コメント" - add: "コメントを追加" - resource: "リソース" - no_comments_yet: "コメントはまだありません。" - title_content: "コメント (%{count})" - errors: - empty_text: "テキストが空のため、コメントは保存されませんでした。" - devise: - login: - title: "ログイン" - remember_me: "次回から自動的にログイン" - submit: "ログイン" - reset_password: - title: "パスワードをお忘れですか?" - submit: "パスワードをリセットする" - change_password: - title: "パスワードを変更する" - submit: "パスワードを変更する" - links: - sign_in: "サインイン" - forgot_your_password: "パスワードをお忘れですか?" - sign_in_with_omniauth_provider: "%{provider}のアカウントを使ってログイン" + per_page: '表示件数: ' + powered_by: Powered by %{active_admin} %{version} + previous: 前 + search_status: + no_current_filters: なし + sidebars: + filters: 検索条件 + search_status: 検索状態 + status_tag: + 'no': いいえ + unset: いいえ + 'yes': はい + toggle_dark_mode: ダークモードを切り替える + toggle_main_navigation_menu: メインナビゲーションメニューを切り替える + toggle_section: セクションを切り替える + toggle_user_menu: ユーザーメニューを切り替える + view: 閲覧 diff --git a/config/locales/ko.yml b/config/locales/ko.yml index 78e4f2481c4..1847883fa66 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -1,83 +1,148 @@ +--- ko: active_admin: - dashboard: "대시보드" - dashboard_welcome: - welcome: "Active Admin 에 오신 것을 환영합니다. 이것은 디폴트 대시보드 페이지 입니다." - call_to_action: "대시보드에 섹션을 추가하시려면 'app/admin/dashboard.rb' 소스를 확인해 주세요." - view: "보기" - edit: "수정" - delete: "삭제" - delete_confirmation: "정말로 삭제 하시겠습니까?" - new_model: "새 %{model}" - edit_model: "%{model} 수정" - delete_model: "%{model} 삭제" - details: "%{model} 상세보기" - cancel: "취소" - empty: "비었음" - previous: "이전" - next: "다음" - download: "다운로드:" - has_many_new: "%{model} 추가" - has_many_delete: "삭제" - has_many_remove: "삭제" - filters: - buttons: - filter: "필터" - clear: "필터 초기화" - predicates: - contains: "포함" - equals: "같다" - starts_with: "로 시작" - ends_with: "로 종료" - greater_than: "크다" - less_than: "작다" - main_content: "내용을 보시려면 %{model}#main_content 를 구현해 주시기 바랍니다." - logout: "로그아웃" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "필터 목록" - pagination: - empty: "%{model} 이/가 없습니다." - one: "1 %{model} 표시중" - one_page: "전체 %{n} %{model} 표시중" - multiple: "전체 %{total} 중 %{from} - %{to} %{model} 표시중" - any: "어떤" - blank_slate: - content: "아직 %{resource_name} 이/가 없습니다." - link: "만들기" + access_denied: + message: 이 작업을 수행할 권한이 없습니다. + any: 어떤 batch_actions: - button_label: "배치 작업" - delete_confirmation: "당신은 이들을 삭제하시겠습니까 %{plural_model}? 당신은 이것을 취소할 수 없습니다." - succesfully_destroyed: - one: "성공적으로 파괴 1 %{model}" - other: "성공적으로 파괴 %{count} %{plural_model}" - selection_toggle_explanation: "(전환 선택)" - link: "만들기 한" - action_label: "%{title} 선택된" + action_label: 선택한 항목 %{title} + button_label: 배치 작업 + default_confirmation: 확실하십니까? + delete_confirmation: "%{plural_model}을/를 삭제하시겠습니까?" labels: - destroy: "삭제" + destroy: 삭제 + selection_toggle_explanation: "(선택 항목 바꾸기)" + successfully_destroyed: + one: 성공적으로 1개 %{model}을/를 삭제하였습니다 + other: 성공적으로 %{count}개의 %{plural_model}을/를 삭제하였습니다 + blank_slate: + content: 아직 %{resource_name} 이/가 없습니다. + link: 추가하기 + cancel: 취소 comments: - body: "몸" - author: "저자" - title: "논평" - add: "코멘트를 추가" - resource: "자원" - no_comments_yet: "아직 코멘트가 없습니다." - title_content: "댓글 (%{count})" + add: 댓글 추가 + author: 글쓴이 + author_missing: 익명 + author_type: 글쓴이 유형 + body: 본문 + created_at: 작성시간 + delete: 댓글 삭제 + delete_confirmation: 정말로 이 댓글을 삭제하시겠습니까? errors: - empty_text: "댓글이 저장되지 않았습니다, ​​텍스트가 비어 있었어." + empty_text: 댓글이 저장되지 않았습니다. 내용을 입력해주세요. + no_comments_yet: 아직 댓글이 없습니다. + resource: 첨부파일 + resource_type: 첨부파일 형태 + title_content: 댓글 (%{count}개) + create_another: 다른 %{model} 생성 + dashboard: 대시보드 + delete: 삭제 + delete_confirmation: 정말로 삭제 하시겠습니까? + delete_model: "%{model} 삭제" + details: "%{model} 상세보기" devise: - login: - title: "로그인" - remember_me: "내 계정 정보 기억" - submit: "로그인" - reset_password: - title: "비밀 번호를 잊으 셨나요?" - submit: "비밀 번호를 재설정" change_password: - title: "비밀 번호를 변경" - submit: "내 비밀 번호 변경" + submit: 내 비밀번호 변경 + title: 비밀번호 변경 + email: + title: 이메일 links: - sign_in: "로그인" - forgot_your_password: "비밀 번호를 잊으 셨나요?" + forgot_your_password: 비밀번호를 잊으셨나요? + resend_confirmation_instructions: 계정 승인 요청하기 + resend_unlock_instructions: 계정 잠금 해제하기 + sign_in: 로그인 sign_in_with_omniauth_provider: "%{provider} 으로 로그인" + sign_up: 회원가입 + login: + remember_me: 내 계정 정보 기억 + submit: 로그인 + title: 로그인 + password: + title: 비밀번호 + password_confirmation: + title: 비밀번호 확인 + resend_confirmation_instructions: + submit: 계정 승인 요청하기 + title: 계정 승인 요청하기 + reset_password: + submit: 비밀번호 재설정 + title: 비밀번호를 잊으셨나요? + sign_up: + submit: 가입하기 + title: 회원가입 + subdomain: + title: 서브도메인 + unlock: + submit: 계정 잠금 해제하기 + title: 계정 잠금 해제하기 + username: + title: 아이디 + download: '다운로드:' + edit: 수정 + edit_model: "%{model} 수정" + empty: 비어있음 + filters: + buttons: + clear: 필터 초기화 + filter: 필터 + predicates: + from: 시작 + to: 끝 + has_many_delete: 삭제 + has_many_new: 새 %{model} 추가 + has_many_remove: 삭제 + index_list: + table: 테이블 + logout: 로그아웃 + move: 이동 + new_model: "%{model} 추가" + next: 다음 + pagination: + empty: "%{model} 이/가 없습니다." + entry: + one: 항목 + other: 항목들 + multiple: "%{total}개 중 %{from} - %{to} %{model} 표시중" + multiple_without_total: "%{from} - %{to} %{model} 표시중" + next: 다음 + one: "1개 %{model} 표시중" + one_page: "%{n}개 %{model} 표시중" + per_page: '페이지당 ' + previous: 이전 + truncate: "…" + powered_by: "%{active_admin} %{version} 제공" + previous: 이전 + scopes: + all: 전체 + search_status: + no_current_filters: 현재 적용된 필터가 없습니다 + title: 검색 중 + title_with_scope: "%{name} 검색 중" + sidebars: + filters: 필터 목록 + search_status: 검색 상태 + status_tag: + 'no': 없음 + unset: 알 수 없음 + 'yes': 있음 + toggle_dark_mode: 다크모드 전환 + toggle_main_navigation_menu: 메인 메뉴 전환 + toggle_section: 섹션 전환 + toggle_user_menu: 사용자 메뉴 전환 + view: 보기 + activerecord: + attributes: + active_admin/comment: + author_type: 글쓴이 유형 + body: 본문 + created_at: 작성시간 + namespace: 네임스페이스 + resource_type: 첨부파일 형태 + updated_at: 수정시간 + models: + active_admin/comment: + one: 댓글 + other: 댓글들 + comment: + one: 댓글 + other: 댓글들 diff --git a/config/locales/lt.yml b/config/locales/lt.yml index 999982a785f..c2eba31c4c5 100644 --- a/config/locales/lt.yml +++ b/config/locales/lt.yml @@ -1,89 +1,119 @@ +--- lt: active_admin: - dashboard: Valdymo skydelis - dashboard_welcome: - welcome: "Sveiki atvykę į Active Admin. Tai yra numatytasis valdymo skydelis." - call_to_action: 'Norėdami pridėti skydelyje skyrius, žiūrėkite app/admin/dashboard.rb' - view: 'Žiūrėti' - edit: 'Redaguoti' - delete: 'Šalinti' - delete_confirmation: 'Ar jūs tikrai norite tai pašalinti?' - new_model: 'Naujas %{model}' - create_model: 'Naujas %{model}' - edit_model: 'Redaguoti %{model}' - update_model: 'Redaguoti %{model}' - delete_model: 'Pašalinti %{model}' - details: '%{model} Informacija' - cancel: 'Atšaukti' - empty: 'Tuščia' - previous: 'Atgal' - next: 'Toliau' - download: 'Atsisiųsti' - has_many_new: 'Pridėti naują %{model}' - has_many_delete: 'Šalinti' - has_many_remove: 'Pašalinti' - filters: - buttons: - filter: 'Filtras' - clear: 'Išvalyti filtrus' - predicates: - contains: "Sudėtyje yra" - equals: 'lygus' - starts_with: "Prasideda nuo" - ends_with: "Baigiasi" - greater_than: 'didesnis nei' - less_than: 'mažiau nei' - main_content: 'Prašome realizuoti %{model}#main_content turiniui vaizduoti.' - logout: 'Išeiti' - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: 'Filtrai' - pagination: - empty: '%{model} nerastas' - one: 'Rodoma 1 %{model}' - one_page: 'Rodoma visi %{n} %{model}' - multiple: 'Rodomi %{model} %{iš} - %{to} iš%{total} iš viso' - entry: - one: 'įrašas' - other: 'įrašai' - any: 'Bet kokia' - blank_slate: - content: 'Nėra %{resource_name}.' - link: 'Sukurti' + access_denied: + message: Jūs nesate įgaliotas atlikti šį veiksmą. + any: Bet kokia batch_actions: - button_label: 'Veiksmai su pažymėtais' - delete_confirmation: 'Ar jūs tikrai norite pašalinti šiuos %{plural_model}? Pašalinus negalėsite atstatyti?' - succesfully_destroyed: - one: 'Sėkmingai pašalintas 1 %{model}' - few: 'Sėkmingai pašalinti %{count} %{plural_model}' - other: 'Sėkmingai pašalinti %{count} %{plural_model}' - selection_toggle_explanation: '(Žymėti)' - link: 'Sukurti' - action_label: '%{title} Pasirinkta' + action_label: "%{title} Pasirinkta" + button_label: Veiksmai su pažymėtais + default_confirmation: Ar jūs tikrai norite tai padaryti? + delete_confirmation: Ar jūs tikrai norite pašalinti šiuos %{plural_model}? labels: - destroy: 'Šalinti' + destroy: Šalinti + selection_toggle_explanation: "(Žymėti)" + successfully_destroyed: + one: Sėkmingai pašalintas 1 %{model} + other: Sėkmingai pašalinti %{count} %{plural_model} + blank_slate: + content: Nėra %{resource_name}. + link: Sukurti + cancel: Atšaukti comments: - body: 'Kūno' - author: 'Autorius' - title: 'Komentaras' - add: 'Pridėti komentarą' - resource: 'Išteklių' - no_comments_yet: 'Dar nėra komentarų.' - title_content: 'Komentarai (%{count})' + add: Pridėti komentarą + author: Autorius + author_missing: Anonimas + author_type: Autoriaus Tipas + body: Įrašas + created_at: Sukurta + delete: Trinti komentarą + delete_confirmation: Ar tikrai norite ištrinti šį komentarą? errors: - empty_text: 'komentaras neišsaugotas, tekstas buvo tuščias.' + empty_text: Komentaras neišsaugotas, tekstas buvo tuščias. + no_comments_yet: Dar nėra komentarų. + resource: Išteklių + resource_type: Resurso Tipas + title_content: Komentarai (%{count}) + dashboard: Valdymo skydelis + delete: Šalinti + delete_confirmation: Ar jūs tikrai norite tai pašalinti? + delete_model: Pašalinti %{model} + details: "%{model} Informacija" devise: - login: - title: 'Prisijungti' - remember_me: 'Prisimink mane' - submit: 'Prisijungti' - reset_password: - title: 'Pamiršote slaptažodį?' - submit: 'Vėl nustatykite savo slaptažodį' change_password: - title: 'Keisti slaptažodį' - submit: 'Keisti slaptažodį' + submit: Pakeisti mano slaptažodį + title: Slaptažodžio Keitimas + email: + title: El. paštas links: - sign_in: 'Prisijungti' - forgot_your_password: 'Pamiršote slaptažodį?' - sign_in_with_omniauth_provider: "Prisijunkite su %{provider}" + forgot_your_password: Pamiršote slaptažodį? + resend_confirmation_instructions: Persiųsti patvirtinimo instrukcijas + resend_unlock_instructions: Persiųsti pakartotinio atrakinimo instrukcijas + sign_in: Prisijungti + sign_in_with_omniauth_provider: Prisijungti su %{provider} + sign_up: Užsiregistruoti + login: + remember_me: Prisiminti Mane + submit: Prisijungti + title: Prisijungimas + password: + title: Slaptažodis + password_confirmation: + title: Pakartokite slaptažodį + resend_confirmation_instructions: + submit: Siųsti patvirtinimo instrukcijas + title: Patvirtinimo Instrukcijos + reset_password: + submit: Sukurti Naują Slaptažodį + title: Pamiršote slaptažodį? + sign_up: + submit: Užsiregistruoti + title: Registracija + subdomain: + title: Subdomenas + unlock: + submit: Pakartotinai siųsti atrakinimo instrukcijas + title: Pakartotinio Atrakinimo Instrukcijos + username: + title: Vartotojo Vardas + download: Atsisiųsti + edit: Redaguoti + edit_model: Redaguoti %{model} + empty: Tuščia + filters: + buttons: + clear: Išvalyti filtrus + filter: Filtras + predicates: + from: Nuo + to: Iki + has_many_delete: Šalinti + has_many_new: Pridėti naują %{model} + has_many_remove: Pašalinti + index_list: + table: Lentelė + logout: Išeiti + new_model: Naujas %{model} + next: Toliau + pagination: + empty: "%{model} nerastas" + entry: + one: įrašas + other: įrašai + multiple: Rodomi %{model} %{from} - %{to} iš%{total} iš viso + multiple_without_total: 'Rodomi %{model} %{from} - %{to} ' + one: Rodoma 1 %{model} + one_page: Rodoma visi %{n} %{model} + per_page: 'Puslapyje: ' + powered_by: Powered by %{active_admin} %{version} + previous: Atgal + search_status: + no_current_filters: nėra + sidebars: + filters: Filtrai + search_status: Paieškos būsena + status_tag: + 'no': Nėra + unset: Nėra + 'yes': Taip + view: Žiūrėti diff --git a/config/locales/lv.yml b/config/locales/lv.yml index ca79327a4c0..59cf0d48285 100644 --- a/config/locales/lv.yml +++ b/config/locales/lv.yml @@ -1,88 +1,80 @@ +--- lv: active_admin: - dashboard: Panelis - dashboard_welcome: - welcome: "Laipni lūgti Active Admin." - call_to_action: "Izmantojiet 'app/admin/dashboard.rb', lai pievienotu sadaļas panelim." - view: "Apskatīt" - edit: "Labot" - delete: "Dzēst" - delete_confirmation: "Vai Tu tiešām vēlies dzēst?" - new_model: "Pievienot '%{model}' ierakstu" - create_model: "Pievienot '%{model}' ierakstu" - edit_model: "Labot '%{model}' ierakstu" - update_model: "Labot '%{model}' ierakstu" - delete_model: "Dzēst '%{model}' ierakstu" - details: "Apraksts" - cancel: "Atcelt" - empty: "Tukšs" - previous: "Iepriekšējā" - next: "Nākošā" - download: "Lejuplādēt:" - has_many_new: "Pievienot jaunu '%{model}' ierakstu" - has_many_delete: "Dzēst" - has_many_remove: "Noņemt" - filters: - buttons: - filter: "Filtrēt" - clear: "Novākt filtrus" - predicates: - contains: "Satur" - equals: "Vienāds ar" - starts_with: "Sākas ar" - ends_with: "Beidzas ar" - greater_than: "Lielāks par" - less_than: "Mazāks par" - main_content: "Lūdzu implementēt %{model}#main_content, lai rādītos saturs." - logout: "Iziet" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Filtri" - pagination: - empty: "Nav ierakstu" - one: "1 ieraksts" - one_page: "%{n} ieraksti" - multiple: "%{from} - %{to} ieraksti no %{total} kopā" - entry: - one: "ieraksts" - other: "ieraksti" - any: "Jebkurš" - blank_slate: - content: "Sadaļā '%{resource_name}' nav neviena ieraksta." - link: "Izveidot jaunu" + any: Jebkurš batch_actions: - button_label: "Batch Actions" - delete_confirmation: "Are you sure you want to delete these %{plural_model}? You won't be able to undo this." - succesfully_destroyed: - one: "Successfully destroyed 1 %{model}" - other: "Successfully destroyed %{count} %{plural_model}" - selection_toggle_explanation: "(Toggle Selection)" - link: "Create one" action_label: "%{title} Selected" + button_label: Batch Actions + default_confirmation: Vai tiešām vēlaties to darīt? + delete_confirmation: Vai tiešām vēlaties dzēst šos %{plural_model}? labels: - destroy: "Delete" + destroy: Delete + selection_toggle_explanation: "(Toggle Selection)" + successfully_destroyed: + one: Successfully deleted 1 %{model} + other: Successfully deleted %{count} %{plural_model} + blank_slate: + content: Sadaļā '%{resource_name}' nav neviena ieraksta. + link: Izveidot jaunu + cancel: Atcelt comments: - body: "Saturs" - author: "Autors" - title: "Komentārs" - add: "Pievienot komentāru" - resource: "Resurss" - no_comments_yet: "Nav neviena komentāra." - title_content: "Komentāri (%{count})" + add: Pievienot komentāru + author: Autors + body: Saturs errors: - empty_text: "Komentārs netika saglabāts - nekas nav ierakstīts" + empty_text: Komentārs netika saglabāts - nekas nav ierakstīts + no_comments_yet: Nav neviena komentāra. + resource: Resurss + title_content: Komentāri (%{count}) + dashboard: Panelis + delete: Dzēst + delete_confirmation: Vai Tu tiešām vēlies dzēst? + delete_model: Dzēst '%{model}' ierakstu + details: Apraksts devise: - login: - title: "Ielogojaties" - remember_me: "atcerēties mani" - submit: "Ielogojaties" - reset_password: - title: "Aizmirsāt savu paroli?" - submit: "Atjaunotu savu paroli" change_password: - title: "Nomainīt paroli" - submit: "Nomainīt savu paroli" + submit: Nomainīt savu paroli + title: Nomainīt paroli links: - sign_in: "pierakstīties" - forgot_your_password: "Aizmirsāt savu paroli?" - sign_in_with_omniauth_provider: "Pierakstieties ar %{provider}" + forgot_your_password: Aizmirsāt savu paroli? + sign_in: pierakstīties + sign_in_with_omniauth_provider: Pierakstieties ar %{provider} + login: + remember_me: atcerēties mani + submit: Ielogojaties + title: Ielogojaties + reset_password: + submit: Atjaunotu savu paroli + title: Aizmirsāt savu paroli? + download: 'Lejuplādēt:' + edit: Labot + edit_model: Labot '%{model}' ierakstu + empty: Tukšs + filters: + buttons: + clear: Novākt filtrus + filter: Filtrēt + has_many_delete: Dzēst + has_many_new: Pievienot jaunu '%{model}' ierakstu + has_many_remove: Noņemt + logout: Iziet + new_model: Pievienot '%{model}' ierakstu + next: Nākošā + pagination: + empty: Nav ierakstu + entry: + one: ieraksts + other: ieraksti + multiple: "%{from} - %{to} ieraksti no %{total} kopā" + multiple_without_total: "%{from} - %{to}" + one: "1 ieraksts" + one_page: "%{n} ieraksti" + powered_by: Powered by %{active_admin} %{version} + previous: Iepriekšējā + sidebars: + filters: Filtri + status_tag: + 'no': Nē + unset: Nē + 'yes': Jā + view: Apskatīt diff --git a/config/locales/mk.yml b/config/locales/mk.yml new file mode 100644 index 00000000000..63c215fe057 --- /dev/null +++ b/config/locales/mk.yml @@ -0,0 +1,112 @@ +--- +mk: + active_admin: + access_denied: + message: Немате овластување да ја извршите оваа активност. + any: Било кој + batch_actions: + action_label: "%{title} го селектираното" + button_label: Групни активности + default_confirmation: Дали сте сигурни? + delete_confirmation: Дали сте сигурни дека сакате да ги избришете %{plural_model}? + labels: + destroy: Избриши + selection_toggle_explanation: "(Toggle Selection)" + successfully_destroyed: + one: Успешно е избришан 1 %{model} + other: Успешно се избришани %{count} %{plural_model} + blank_slate: + content: Не се креирани записи од типот на %{resource_name}. + link: Креирај нов + cancel: Откажи + create_another: Креирај нов %{model} + dashboard: Почетна + delete: Избриши + delete_confirmation: Дали сте сигурни дека сакате да го избришете записот? + delete_model: Избриши %{model} + details: Детали за %{model} + devise: + change_password: + submit: Промени лозинка + title: Променете ја лозинката + email: + title: Е-маил + links: + forgot_your_password: Ја заборавивте Вашата лозинка? + resend_confirmation_instructions: Повторно испрати инструкции за потврдување на профил + resend_unlock_instructions: Повторно испрати инструкции за отклучување на профил + sign_in: Најави се + sign_in_with_omniauth_provider: Најави се со %{provider} + sign_up: Креирај профил + login: + remember_me: Запомни ме + submit: Најави се + title: Најави се + password: + title: Лозинка + password_confirmation: + title: Потврди Лозинка + resend_confirmation_instructions: + submit: Инспрати инструкции + title: Повторно испраќање на инструкции за потврдување на профил + reset_password: + submit: Промени лозинка + title: Ја заборавивте Вашата лозинка? + sign_up: + submit: Креирај + title: Креирај профил + unlock: + submit: Испрати инструкции + title: Повторно испраќање на инструкции за отклучување на профил + username: + title: Корисничко име + download: 'Преземи во понудените формати:' + edit: Измени + edit_model: Измени %{model} + empty: Празно + filters: + buttons: + clear: Исчисти филтри + filter: Пребарај + predicates: + from: Од + to: До + has_many_delete: Избриши + has_many_new: Додај нов %{model} + has_many_remove: Отстрани + index_list: + table: Табела + logout: Одјави се + move: Премести + new_model: Додај нов %{model} + next: Следно + pagination: + empty: Нема пронајдени записи за %{model} + entry: + one: запис + other: записи + multiple: Прикажани %{model} %{from} - %{to} од вкупно %{total} + multiple_without_total: Прикажани %{model} %{from} - %{to} + one: Прикажан 1 %{model} + one_page: Прикажани сите %{n} %{model} + per_page: 'Прикази на записи по страна:' + powered_by: Овозможено од %{active_admin} %{version} + previous: Претходно + search_status: + no_current_filters: Моментално нема филтри + sidebars: + filters: Филтри за пребарување + search_status: Резултати од пребарување + status_tag: + 'no': Не + unset: Не + 'yes': Да + view: Прегледај + activerecord: + models: + active_admin/comment: + one: Коментар + other: Коментари + comment: + one: Коментар + other: Коментари diff --git a/config/locales/nb.yml b/config/locales/nb.yml new file mode 100644 index 00000000000..c8864992df8 --- /dev/null +++ b/config/locales/nb.yml @@ -0,0 +1,111 @@ +--- +nb: + active_admin: + access_denied: + message: Du er ikke autorisert til å utføre denne handlingen. + any: Alle + batch_actions: + action_label: "%{title} valgt" + button_label: Gruppehandlinger + delete_confirmation: Er du sikker på at du vil slette disse %{plural_model}? Dette kan ikke reverseres. + labels: + destroy: Slett + selection_toggle_explanation: "(Toggle Selection)" + successfully_destroyed: + one: Slettet én %{model} + other: Slettet %{count} %{plural_model} + blank_slate: + content: Her er det ingen %{resource_name} enda. + link: Opprett en + cancel: Avbryt + comments: + add: Legg til kommentar + author: Forfatter + author_missing: Anonym + author_type: Forfattertype + body: Brødtekst + created_at: Opprettet + delete: Slett kommentar + delete_confirmation: Er du sikker på at du ønsker å slette kommentaren? + errors: + empty_text: Kommentar ble ikke lagret, teksten var tom. + no_comments_yet: Ingen kommentarer ennå. + resource: Ressurs + resource_type: Ressurstype + title_content: Kommentarer (%{count}) + dashboard: Oversikt + delete: Slett + delete_confirmation: Er du sikker på at du vil slette denne? + delete_model: Slett %{model} + details: "%{model} Detaljer" + devise: + change_password: + submit: Endre mitt passord + title: Endre passordet + email: + title: E-post + links: + forgot_your_password: Glemt passord? + sign_in: Logg inn + sign_in_with_omniauth_provider: Logg på med %{provider} + login: + remember_me: Husk meg + submit: Logg inn + title: Innlogging + password: + title: Passord + resend_confirmation_instructions: + submit: Send bekreftelsesinformasjon på nytt + title: Send bekreftelsesinformasjon på nytt + reset_password: + submit: Tilbakestille passordet mitt + title: Glemt passord? + sign_up: + submit: Opprett + title: Opprett brukerkonto + subdomain: + title: Subdomene + unlock: + submit: Send info om gjenoppretting på nytt + title: Send info om gjenoppretting på nytt + username: + title: Brukernavn + download: 'Last ned:' + edit: Rediger + edit_model: Rediger %{model} + empty: Tom + filters: + buttons: + clear: Fjern filter + filter: Filter + predicates: + from: Fra + to: Til + has_many_delete: Slett + has_many_new: Legg til ny %{model} + has_many_remove: Fjern + index_list: + table: Tabell + logout: Logg ut + new_model: Ny %{model} + next: Neste + pagination: + empty: Fant ingen %{model} + entry: + one: innslag + other: innslag + multiple: Viser %{model} %{from} - %{to} av %{total} totalt + multiple_without_total: Viser %{model} %{from} - %{to} + one: Viser 1 %{model} + one_page: Viser alle %{n} %{model} + powered_by: Powered by %{active_admin} %{version} + previous: Forrige + search_status: + no_current_filters: Ingen + sidebars: + filters: Filtere + status_tag: + 'no': Nei + unset: Nei + 'yes': Ja + view: Vis diff --git a/config/locales/nl.yml b/config/locales/nl.yml index f1a68a35eb6..07c58f0ba80 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -1,89 +1,127 @@ +--- nl: active_admin: - dashboard: Dashboard - dashboard_welcome: - welcome: "Welkom bij Active Admin. Dit is de standaard dashboard pagina" - call_to_action: "Pas je eigen dashboard aan in het bestand 'app/admin/dashboard.rb'" - view: "Bekijk" - edit: "Wijzig" - delete: "Verwijder" - delete_confirmation: "Weet je zeker dat je dit item wilt verwijderen?" - new_model: "Nieuwe %{model}" - create_model: "Nieuwe %{model}" - edit_model: "Wijzig %{model}" - update_model: "Wijzig %{model}" - delete_model: "Verwijder %{model}" - details: "%{model} details" - cancel: "Annuleren" - empty: "Leeg" - previous: "Vorige" - next: "Volgende" - download: "Download" - has_many_new: "Voeg nieuwe %{model} toe" - has_many_delete: "Verwijderen" - has_many_remove: "Verwijderen" - filters: - buttons: - filter: "Filter" - clear: "Maak Filters Ongedaan" - predicates: - contains: "Bevat" - equals: "Gelijk aan" - starts_with: "Begint met" - ends_with: "Eindigt op" - greater_than: "Groter dan" - less_than: "Kleiner dan" - main_content: "Implementeer %{model}#main_content om de content weer te geven." - logout: "Uitloggen" - powered_by: "Mogelijk gemaakt door %{active_admin} %{version}" - sidebars: - filters: "Filters" - pagination: - empty: "Geen %{model} gevonden" - one: "Geeft 1 %{model} weer" - one_page: "Geeft %{n} %{model} weer" - multiple: "Geeft %{model} %{from} - %{to} van de %{total} weer" - entry: - one: "entry" - other: "entries" - any: "Alle" - blank_slate: - content: "Er zijn geen %{resource_name} gevonden." - link: "Maak aan" + access_denied: + message: U bent niet gemachtigd voor deze actie. + any: Alle batch_actions: - button_label: "Batch acties" - delete_confirmation: "Weet je zeker dat je deze %{plural_model} wilt verwijderen? Er is geen undo." - succesfully_destroyed: - one: "1 %{model} verwijderd." - other: "%{count} %{plural_model} verwijderd." - selection_toggle_explanation: "(Toggle selectie)" - link: "Maak aan" action_label: "%{title} geselecteerde" + button_label: Batch acties + default_confirmation: Weet u zeker dat u dit wilt doen? + delete_confirmation: Weet u zeker dat u deze %{plural_model} wilt verwijderen? labels: - destroy: "Verwijder" + destroy: Verwijder + selection_toggle_explanation: "(Toggle selectie)" + successfully_destroyed: + one: 1 %{model} verwijderd. + other: "%{count} %{plural_model} verwijderd." + blank_slate: + content: Er zijn geen %{resource_name} gevonden. + link: Maak aan + cancel: Annuleren comments: - body: "Tekst" - author: "Auteur" - title: "Reactie" - add: "Voeg commentaar toe" - resource: "Resource" - no_comments_yet: "Nog geen reacties." - title_content: "Reacties (%{count})" + add: Voeg reactie toe + author: Auteur + author_missing: Anoniem + author_type: Auteur Type + body: Tekst + created_at: Aangemaakt op + delete: Verwijder reactie + delete_confirmation: Weet u zeker dat u deze reactie wilt verwijderen? errors: - empty_text: "De reactie is niet opgeslagen, de tekst was leeg." + empty_text: De reactie is niet opgeslagen, de tekst was leeg. + no_comments_yet: Nog geen reacties. + resource: Resource + resource_type: Resource Type + title_content: Alle reacties (%{count}) + create_another: Maak nog een %{model} + dashboard: Dashboard + delete: Verwijder + delete_confirmation: Weet u zeker dat je dit item wilt verwijderen? + delete_model: Verwijder %{model} + details: "%{model} details" devise: - login: - title: "inloggen" - remember_me: "Onthoud mij" - submit: "inloggen" - reset_password: - title: "Wachtwoord vergeten?" - submit: "Reset mijn wachtwoord vergeten" change_password: - title: "Wijzig uw wachtwoord" - submit: "Mijn wachtwoord wijzigen" + submit: Mijn wachtwoord wijzigen + title: Wijzig uw wachtwoord + email: + title: Email links: - sign_in: "Meld u aan" - forgot_your_password: "Wachtwoord vergeten?" - sign_in_with_omniauth_provider: "Log in met %{provider}" - + forgot_your_password: Wachtwoord vergeten? + resend_confirmation_instructions: Bevestigingsinstructies opnieuw versturen + resend_unlock_instructions: Ontgrendelinstructies opnieuw versturen + sign_in: Meld u aan + sign_in_with_omniauth_provider: Log in met %{provider} + sign_up: Registreren + login: + remember_me: Onthoud mij + submit: Inloggen + title: Inloggen + password: + title: Wachtwoord + password_confirmation: + title: Bevestig password + resend_confirmation_instructions: + submit: Verstuur bevestigingsinstructies opnieuw + title: Verstuur bevestigingsinstructies opnieuw + reset_password: + submit: Reset mijn wachtwoord + title: Wachtwoord vergeten? + sign_up: + submit: Registreren + title: Registreren + subdomain: + title: Subdomein + unlock: + submit: Verstuur ontgrendelinstructies opnieuw + title: Verstuur ontgrendelinstructies opnieuw + username: + title: Gebruikersnaam + download: Download + edit: Wijzig + edit_model: Wijzig %{model} + empty: Leeg + filters: + buttons: + clear: Maak Filters Ongedaan + filter: Filter + predicates: + from: Van + to: Tot + has_many_delete: Verwijderen + has_many_new: Voeg nieuwe %{model} toe + has_many_remove: Verwijderen + index_list: + table: Tabel + logout: Uitloggen + move: Verplaats + new_model: Nieuwe %{model} + next: Volgende + pagination: + empty: Geen %{model} gevonden + entry: + one: entry + other: entries + multiple: Toont %{from}-%{to} van %{total} + multiple_without_total: Toont %{from}-%{to} + next: Volgende + one: Toont 1 van 1 + one_page: Toont alle %{n} + per_page: 'Per pagina: ' + previous: Vorige + powered_by: Mogelijk gemaakt door %{active_admin} %{version} + previous: Vorige + scopes: + all: Alle + search_status: + no_current_filters: Geen + title: Huidige filter + title_with_scope: Huidige filter voor %{name} + sidebars: + filters: Filters + search_status: Zoek status + status_tag: + 'no': Geen + unset: Onbekend + 'yes': Ja + view: Bekijk diff --git a/config/locales/no-NB.yml b/config/locales/no-NB.yml deleted file mode 100644 index 70198adaa54..00000000000 --- a/config/locales/no-NB.yml +++ /dev/null @@ -1,84 +0,0 @@ -"no-NB": - active_admin: - dashboard: Oversikt - dashboard_welcome: - welcome: "Velkommen til Active Admin. Dette er standardoversiktssiden." - call_to_action: "Rediger 'app/admin/dashboard.rb' for at legge til elementer i oversikten." - view: "Vis" - edit: "Rediger" - delete: "Slett" - delete_confirmation: "Er du sikker på at du vil slette denne?" - new_model: "Ny %{model}" - edit_model: "Rediger %{model}" - delete_model: "Slett %{model}" - details: "%{model} Detaljer" - cancel: "Avbryt" - empty: "Tom" - previous: "Forrige" - next: "Neste" - download: "Last ned:" - has_many_new: "Legg til ny %{model}" - has_many_delete: "Slett" - has_many_remove: "Fjern" - filters: - buttons: - filter: "Filter" - clear: "Fjern filter" - predicates: - contains: "Inneholder" - equals: "Er lik" - starts_with: "Starter med" - ends_with: "Slutter med" - greater_than: "Større enn" - less_than: "Mindre enn" - main_content: "Please implement %{model}#main_content to display content." - logout: "Logg ut" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Filtere" - pagination: - empty: "Fannt ingen %{model}" - one: "Viser 1 %{model}" - one_page: "Viser alle %{n} %{model}" - multiple: "Viser %{model} %{from} - %{to} av %{total} totalt" - any: "Noen" - blank_slate: - content: "Her er det ingen %{resource_name} enda." - link: "Opprett en" - batch_actions: - button_label: "Batch Actions" - delete_confirmation: "Are you sure you want to delete these %{plural_model}? You won't be able to undo this." - succesfully_destroyed: - one: "Successfully destroyed 1 %{model}" - other: "Successfully destroyed %{count} %{plural_model}" - selection_toggle_explanation: "(Toggle Selection)" - link: "Create one" - action_label: "%{title} Selected" - labels: - destroy: "Delete" - comments: - body: "Body" - author: "Author" - title: "Comment" - add: "Add Comment" - resource: "Resource" - no_comments_yet: "No comments yet." - title_content: "Comments (%{count})" - errors: - empty_text: "Comment wasn't saved, text was empty." - devise: - login: - title: "Logg inn" - remember_me: "Husk meg" - submit: "Logg inn" - reset_password: - title: "Glemt passord?" - submit: "Tilbakestille passordet mitt" - change_password: - title: "Endre passordet" - submit: "Endre mitt passord" - links: - sign_in: "Logg inn" - forgot_your_password: "Glemt passord?" - sign_in_with_omniauth_provider: "Logg på med %{provider}" - diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 17a0c01496b..57ae345ab30 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -1,92 +1,151 @@ +--- pl: active_admin: - dashboard: Pulpit - dashboard_welcome: - welcome: "Witaj w Active Adminie. To jest domyślny pulpit." - call_to_action: "Aby dodać sekcje do pulpitu, sprawdź 'app/admin/dashboard.rb'" - view: "Podgląd" - edit: "Edytuj" - delete: "Usuń" - delete_confirmation: "Jesteś pewien, że chcesz to usunąć?" - new_model: "Nowy %{model}" - create_model: "Nowy %{model}" - edit_model: "Edytuj %{model}" - update_model: "Edytuj %{model}" - delete_model: "Usuń %{model}" - details: "Detale %{model}" - cancel: "Anuluj" - empty: "Pusty" - previous: "Poprzednia" - next: "Następna" - download: "Pobierz:" - has_many_new: "Dodaj nowy %{model}" - has_many_delete: "Usuń" - has_many_remove: "Usuń" - filters: - buttons: - filter: "Filtruj" - clear: "Wyczyść Filtry" - predicates: - contains: "Zawiera" - equals: "Równe" - starts_with: "Zaczyna się" - ends_with: "Kończy się" - greater_than: "Większe niż" - less_than: "Mniejsze niż" - main_content: "Zaimplementuj %{model}#main_content aby wyświetlić treść." - logout: "Wyloguj" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Filtry" - pagination: - empty: "Nie znaleziono %{model}" - one: "Wyświetlanie 1 %{model}" - one_page: "Wyświetlanie wszystkich %{n} %{model}" - multiple: "Wyświetlanie %{model} %{from} - %{to} z %{total}" - any: "Jakikolwiek" - blank_slate: - content: "Nie ma jeszcze zasobu %{resource_name}." - link: "Utwórz go" + access_denied: + message: Nie masz uprawnień wystarczających do wykonania tej akcji. + any: Jakikolwiek batch_actions: - button_label: "Akcje na partiach" - delete_confirmation: "Czy na pewno chcesz usunąć te %{plural_model}? Nie będziesz miał możliwości cofnięcia tego." - succesfully_destroyed: - one: "Poprawnie usunięto 1 %{model}" - other: "Poprawnie usunięto %{count} %{plural_model}" - many: "Poprawnie usunięto %{count} %{plural_model}" - few: "Poprawnie usunięto %{count} %{plural_model}" - selection_toggle_explanation: "(Przełącz zaznaczenie)" - link: "Utwórz jeden" action_label: "%{title} zaznaczone" + button_label: Akcje na partiach + default_confirmation: Czy na pewno chcesz to zrobić? + delete_confirmation: Czy na pewno chcesz usunąć te %{plural_model}? labels: - destroy: "Usuń" + destroy: Usuń + selection_toggle_explanation: "(Przełącz zaznaczenie)" + successfully_destroyed: + few: Poprawnie usunięto %{count} %{plural_model} + many: Poprawnie usunięto %{count} %{plural_model} + one: Poprawnie usunięto 1 %{model} + other: Poprawnie usunięto %{count} %{plural_model} + blank_slate: + content: Nie ma jeszcze zasobu %{resource_name}. + link: Utwórz go + cancel: Anuluj comments: - body: "Treść" - author: "Autor" - title: "Komentarz" - add: "Dodaj komentarz" - resource: "Zasób" - no_comments_yet: "Nie ma jeszcze komentarzy." - title_content: "Komentarze (%{count})" + add: Dodaj komentarz + author: Autor + author_missing: Anonim + author_type: Typ autora + body: Treść + created_at: Utworzony + delete: Usuń komentarz + delete_confirmation: Czy na pewno chcesz usunąć ten komentarz? errors: - empty_text: "Komentarz nie został zapisany, zawartość była pusta." + empty_text: Komentarz nie został zapisany, zawartość była pusta. + no_comments_yet: Nie ma jeszcze komentarzy. + resource: Zasób + resource_type: Typ zasobu + title_content: Komentarze (%{count}) + create_another: Utwórz kolejny %{model} + dashboard: Pulpit + delete: Usuń + delete_confirmation: Jesteś pewien, że chcesz to usunąć? + delete_model: Usuń %{model} + details: Szczegóły %{model} devise: - login: - title: "Logowanie" - remember_me: "Zapamiętaj mnie" - submit: "Zaloguj się" - reset_password: - title: "Nie pamiętasz hasła?" - submit: "Zresetować hasło" change_password: - title: "Zmień hasło" - submit: "Zmień hasło" + submit: Zmień hasło + title: Zmień hasło + email: + title: Email links: - sign_in: "Zaloguj się" - forgot_your_password: "Nie pamiętasz hasła?" - sign_in_with_omniauth_provider: "Zaloguj się z %{provider}" - formtastic: - actions: - update: "Aktualizuj %{model}" - create: "Utwórz %{model}" - + forgot_your_password: Nie pamiętasz hasła? + resend_confirmation_instructions: Ponownie wyślij instrukcje aktywacji + resend_unlock_instructions: Ponownie wyślij instrukcję odblokowania konta + sign_in: Zaloguj się + sign_in_with_omniauth_provider: Zaloguj się z %{provider} + sign_up: Zarejestruj się + login: + remember_me: Zapamiętaj mnie + submit: Zaloguj się + title: Logowanie + password: + title: Hasło + password_confirmation: + title: Powtórz hasło + resend_confirmation_instructions: + submit: Ponownie wyślij instrukcje aktywacji + title: Ponownie wyślij instrukcje aktywacji + reset_password: + submit: Zresetować hasło + title: Nie pamiętasz hasła? + sign_up: + submit: Zarejestruj się + title: Rejestracja + subdomain: + title: Subdomena + unlock: + submit: Ponownie wyślij instrukcję odblokowania konta + title: Ponownie wyślij instrukcję odblokowania konta + username: + title: Nazwa użytkownika + download: 'Pobierz:' + edit: Edytuj + edit_model: Edytuj %{model} + empty: Pusty + filters: + buttons: + clear: Wyczyść Filtry + filter: Filtruj + predicates: + from: Od + to: Do + has_many_delete: Usuń + has_many_new: Dodaj nowy %{model} + has_many_remove: Usuń + index_list: + table: Tabela + logout: Wyloguj + move: Przenieś + new_model: Nowy %{model} + next: Następna + pagination: + empty: Nie znaleziono %{model} + entry: + one: wpis + other: wpisów + multiple: Wyświetlanie %{model} %{from} - %{to} z %{total} + multiple_without_total: Wyświetlanie %{model} %{from} - %{to} + one: Wyświetlanie 1 %{model} + one_page: Wyświetlanie wszystkich %{n} %{model} + per_page: 'Na stronę: ' + powered_by: Powered by %{active_admin} %{version} + previous: Poprzednia + scopes: + all: Wszystko + search_status: + no_current_filters: Brak + title: Wyszukiwanie + title_with_scope: Wyszukiwanie %{name} + sidebars: + filters: Filtry + search_status: Status wyszukiwania + status_tag: + 'no': Nie + unset: Nie + 'yes': Tak + toggle_dark_mode: Przełącz tryb ciemny + toggle_main_navigation_menu: Przełącz główną nawigację + toggle_section: Przełącz sekcję + toggle_user_menu: Przełącz menu użytkownika + view: Podgląd + activerecord: + attributes: + active_admin/comment: + author_type: Typ autora + body: Treść + created_at: Utworzony + namespace: Namespace + resource_type: Typ zasobu + updated_at: Zaktualizowany + models: + active_admin/comment: + few: Komentarze + many: Komentarzy + one: Komentarz + other: Komentarze + comment: + few: Komentarze + many: Komentarzy + one: Komentarz + other: Komentarze diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 6fe4519427d..2fbd0b02b4e 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -1,88 +1,148 @@ -"pt-BR": +--- +pt-BR: active_admin: - dashboard: "Painel Administrativo" - dashboard_welcome: - welcome: "Bem vindo ao Active Admin. Esta é a página de painéis padrão." - call_to_action: "Para adicionar seções ao painel, verifique 'app/admin/dashboard.rb'" - view: "Visualizar" - edit: "Editar" - delete: "Remover" - delete_confirmation: "Você tem certeza que deseja remover este item?" - new_model: "Novo(a) %{model}" - create_model: "Novo(a) %{model}" - edit_model: "Editar %{model}" - update_model: "Atualizar %{model}" - delete_model: "Remover %{model}" - details: "Detalhes do(a) %{model}" - cancel: "Cancelar" - empty: "Vazio" - previous: "Anterior" - next: "Próximo" - download: "Baixar:" - has_many_new: "Adicionar Novo(a) %{model}" - has_many_delete: "Remover" - has_many_remove: "Remover" - filters: - buttons: - filter: "Filtrar" - clear: "Limpar Filtros" - predicates: - contains: "Contém" - equals: "Igual A" - starts_with: "Começa com" - ends_with: "Termina com" - greater_than: "Maior Que" - less_than: "Menor Que" - main_content: "Por favor implemente %{model}#main_content para exibir conteúdo." - logout: "Sair" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Filtros" - pagination: - empty: "Nenhum(a) %{model} encontrado(a)" - one: "Exibindo 1 %{model}" - one_page: "Exibindo todos(as) os(as) %{n} %{model}" - multiple: "Exibindo %{model} %{from} - %{to} de um total de %{total}" - entry: - one: "registro" - other: "registros" - any: "Qualquer" - blank_slate: - content: "Não existem %{resource_name} ainda." - link: "Novo" + access_denied: + message: Você não tem permissão para realizar o solicitado + any: Qualquer batch_actions: - button_label: "Ações em lote" - delete_confirmation: "Tem certeza que deseja excluir estes %{plural_model}? Não é possível desfazer esta ação." - succesfully_destroyed: - one: "Excluiu com sucesso 1 %{model}" - other: "Excluiu com sucesso %{count} %{plural_model}" - selection_toggle_explanation: "(Alternar Seleção)" - link: "Novo" action_label: "%{title} Selecionado" + button_label: Ações em lote + default_confirmation: Tem certeza que quer fazer isso? + delete_confirmation: Tem certeza que deseja excluir estes %{plural_model}? labels: - destroy: "Excluído" + destroy: Excluir + selection_toggle_explanation: "(Alternar Seleção)" + successfully_destroyed: + one: Excluiu com sucesso 1 %{model} + other: Excluiu com sucesso %{count} %{plural_model} + blank_slate: + content: Não existem %{resource_name} ainda. + link: Novo + cancel: Cancelar comments: - body: "Conteúdo" - author: "Autor" - title: "Comentário" - add: "Adicionar Comentário" - resource: "Objeto" - no_comments_yet: "Nenhum comentário." - title_content: "Comentários: %{count}" + add: Adicionar Comentário + author: Autor + author_missing: Anônimo + author_type: Tipo de Autor + body: Conteúdo + created_at: Criado em + delete: Deletar comentário + delete_confirmation: Tem certeza que deseja excluir este comentário? errors: - empty_text: "O comentário não foi salvo porque o texto estava vazio." + empty_text: O comentário não foi salvo porque o texto estava vazio. + no_comments_yet: Nenhum comentário. + resource: Objeto + resource_type: Tipo de Objeto + title_content: 'Comentários: %{count}' + create_another: Criar outro %{model} + dashboard: Painel Administrativo + delete: Remover + delete_confirmation: Você tem certeza que deseja remover este item? + delete_model: Remover %{model} + details: Detalhes do(a) %{model} devise: - login: - title: "Conta" - remember_me: "Lembrar da senha" - submit: "Entrar" - reset_password: - title: "Esqueceu sua senha?" - submit: "Reinicie minha senha" change_password: - title: "Troque sua senha" - submit: "Troque minha senha" + submit: Troque minha senha + title: Troque sua senha + email: + title: E-mail links: - sign_in: "Entrar" - forgot_your_password: "Esqueceu sua senha?" - sign_in_with_omniauth_provider: "Entre com o %{provider}" + forgot_your_password: Esqueceu sua senha? + resend_confirmation_instructions: Reenviar instruções de confirmação + resend_unlock_instructions: Reenviar instruções de desbloqueio + sign_in: Entrar + sign_in_with_omniauth_provider: Entre com o %{provider} + sign_up: Criar conta + login: + remember_me: Lembrar da senha + submit: Entrar + title: Conta + password: + title: Senha + password_confirmation: + title: Confirmação de senha + resend_confirmation_instructions: + submit: Reenviar instruções de confirmação + title: Reenviar instruções de confirmação + reset_password: + submit: Reinicie minha senha + title: Esqueceu sua senha? + sign_up: + submit: Continuar + title: Cadastre-se + subdomain: + title: Subdomínio + unlock: + submit: Reenviar instruções de desbloqueio + title: Reenviar instruções de desbloqueio + username: + title: Nome de Usuário + download: 'Baixar:' + edit: Editar + edit_model: Editar %{model} + empty: Vazio + filters: + buttons: + clear: Limpar Filtros + filter: Filtrar + predicates: + from: A partir de + to: Até + has_many_delete: Remover + has_many_new: Adicionar Novo(a) %{model} + has_many_remove: Remover + index_list: + table: Tabela + logout: Sair + move: Mover + new_model: Novo(a) %{model} + next: Próximo + pagination: + empty: Nenhum(a) %{model} encontrado(a) + entry: + one: registro + other: registros + multiple: Exibindo %{model} %{from} - %{to} de um total de %{total} + multiple_without_total: Exibindo %{model} %{from} - %{to} + next: Próximo + one: Exibindo 1 %{model} + one_page: Exibindo todos(as) os(as) %{n} %{model} + per_page: 'Por página: ' + previous: Anterior + truncate: "…" + powered_by: Powered by %{active_admin} %{version} + previous: Anterior + scopes: + all: Todos + search_status: + no_current_filters: Nenhum filtro aplicado + title: Active Search + title_with_scope: Active Search de %{name} + sidebars: + filters: Filtros + search_status: Buscou + status_tag: + 'no': Não + unset: Não + 'yes': Sim + toggle_dark_mode: Ativar modo escuro + toggle_main_navigation_menu: Ativar menu de navegação principal + toggle_section: Ativar seção + toggle_user_menu: Ativar menu de usuário + view: Visualizar + activerecord: + attributes: + active_admin/comment: + author_type: Tipo do autor + body: Corpo + created_at: Criado em + namespace: Namespace + resource_type: Tipo do recurso + updated_at: Atualizado em + models: + active_admin/comment: + one: Comentário + other: Comentários + comment: + one: Comentário + other: Comentários diff --git a/config/locales/pt-PT.yml b/config/locales/pt-PT.yml index fb0a1625066..729048d585d 100644 --- a/config/locales/pt-PT.yml +++ b/config/locales/pt-PT.yml @@ -1,88 +1,80 @@ -"pt-PT": +--- +pt-PT: active_admin: - dashboard: "Painel de Administração" - dashboard_welcome: - welcome: "Bem-vindo ao Active Admin. Esta é a página padrão." - call_to_action: "Se pretende adicionar seções ao painel, consulte 'app/admin/dashboard.rb'" - view: "Visualizar" - edit: "Editar" - delete: "Remover" - delete_confirmation: "Não tem a certeza de que deseja remover este ítem?" - new_model: "Novo(a) %{model}" - create_model: "Novo(a) %{model}" - edit_model: "Editar %{model}" - update_model: "Atualizar %{model}" - delete_model: "Remover %{model}" - details: "Detalhes do(a) %{model}" - cancel: "Cancelar" - empty: "Vazio" - previous: "Anterior" - next: "Próximo" - download: "Baixar:" - has_many_new: "Adicionar Novo(a) %{model}" - has_many_delete: "Remover" - has_many_remove: "Remover" - filters: - buttons: - filter: "Filtrar" - clear: "Limpar Filtros" - predicates: - contains: "Contém" - equals: "Igual A" - starts_with: "Começa com" - ends_with: "Termina com" - greater_than: "Maior Que" - less_than: "Menor Que" - main_content: "Por favor implemente %{model}#main_content para mostrar o conteúdo." - logout: "Sair" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Filtros" - pagination: - empty: "Nenhum(a) %{model} encontrado(a)" - one: "Mostrando 1 %{model}" - one_page: "Mostrando todos(as) os(as) %{n} %{model}" - multiple: "Mostrando %{model} %{from} - %{to} de um total de %{total}" - entry: - one: "registro" - other: "registros" - any: "Qualquer" - blank_slate: - content: "Ainda não existem %{resource_name}." - link: "Novo" + any: Qualquer batch_actions: - button_label: "Ações em quantidade" - delete_confirmation: "Tem a certeza de que deseja excluir estes %{plural_model}? Não é possível voltar atrás." - succesfully_destroyed: - one: "Excluiu com sucesso 1 %{model}" - other: "Excluiu com sucesso %{count} %{plural_model}" - selection_toggle_explanation: "(Alternar Seleção)" - link: "Novo" action_label: "%{title} Selecionado" + button_label: Ações em quantidade + default_confirmation: Tem a certeza que quer fazer isso? + delete_confirmation: Tem a certeza de que deseja excluir estes %{plural_model}? labels: - destroy: "Excluído" + destroy: Excluir + selection_toggle_explanation: "(Alternar Seleção)" + successfully_destroyed: + one: Excluiu com sucesso 1 %{model} + other: Excluiu com sucesso %{count} %{plural_model} + blank_slate: + content: Ainda não existem %{resource_name}. + link: Novo + cancel: Cancelar comments: - body: "Conteúdo" - author: "Autor" - title: "Comentário" - add: "Adicionar Comentário" - resource: "Objeto" - no_comments_yet: "Nenhum comentário." - title_content: "Comentários: %{count}" + add: Adicionar Comentário + author: Autor + body: Conteúdo errors: - empty_text: "O comentário não foi guardado porque o texto estava vazio." + empty_text: O comentário não foi guardado porque o texto estava vazio. + no_comments_yet: Nenhum comentário. + resource: Objeto + title_content: 'Comentários: %{count}' + dashboard: Painel de Administração + delete: Remover + delete_confirmation: Não tem a certeza de que deseja remover este ítem? + delete_model: Remover %{model} + details: Detalhes do(a) %{model} devise: - login: - title: "Conta" - remember_me: "Lembrar-me" - submit: "Entrar" - reset_password: - title: "Esqueceu-de da sua senha?" - submit: "Reiniciar a minha senha" change_password: - title: "Troque a sua senha" - submit: "Trocar a minha senha" + submit: Trocar a minha senha + title: Troque a sua senha links: - sign_in: "Entrar" - forgot_your_password: "Esqueceu-se da sua senha?" - sign_in_with_omniauth_provider: "Entre com o %{provider}" + forgot_your_password: Esqueceu-se da sua senha? + sign_in: Entrar + sign_in_with_omniauth_provider: Entre com o %{provider} + login: + remember_me: Lembrar-me + submit: Entrar + title: Conta + reset_password: + submit: Reiniciar a minha senha + title: Esqueceu-de da sua senha? + download: 'Baixar:' + edit: Editar + edit_model: Editar %{model} + empty: Vazio + filters: + buttons: + clear: Limpar Filtros + filter: Filtrar + has_many_delete: Remover + has_many_new: Adicionar Novo(a) %{model} + has_many_remove: Remover + logout: Sair + new_model: Novo(a) %{model} + next: Próximo + pagination: + empty: Nenhum(a) %{model} encontrado(a) + entry: + one: registro + other: registros + multiple: Mostrando %{model} %{from} - %{to} de um total de %{total} + multiple_without_total: Mostrando %{model} %{from} - %{to} + one: Mostrando 1 %{model} + one_page: Mostrando todos(as) os(as) %{n} %{model} + powered_by: Powered by %{active_admin} %{version} + previous: Anterior + sidebars: + filters: Filtros + status_tag: + 'no': Não + unset: Não + 'yes': Sim + view: Visualizar diff --git a/config/locales/ro.yml b/config/locales/ro.yml index 341c968ee75..e89a5444fbb 100644 --- a/config/locales/ro.yml +++ b/config/locales/ro.yml @@ -1,92 +1,85 @@ +--- ro: active_admin: - dashboard: "Pagina Principala" - dashboard_welcome: - welcome: "Bine ati venit pe Active Admin. Aceasta este pagina principala." - call_to_action: "Pentru a adauga sectiuni, vedeti 'app/admin/dashboard.rb'" - view: "Vizualizati" - edit: "Modificati" - delete: "Stergeti" - delete_confirmation: "Sigur vreti sa stergeti?" - new_model: "Un nou %{model}" - create_model: "Un nou %{model}" - edit_model: "Modificati %{model}" - update_model: "Modificati %{model}" - delete_model: "Stergeti %{model}" - details: "Detalii %{model}" - cancel: "Renuntati" - empty: "Gol" - previous: "Inapoi" - next: "Inainte" - download: "Descarcati:" - has_many_new: "Adaugati un nou %{model}" - has_many_delete: "Stergeti" - has_many_remove: "Scoate" - filters: - buttons: - filter: "Cautati" - clear: "Stergeti filtrele" - predicates: - contains: "Conține" - equals: "Egal Cu" - starts_with: "începe cu" - ends_with: "se termină cu" - greater_than: "Mai Mare Decat" - less_than: "Mai Mic Decat" - main_content: "Va rugam sa implementati %{model}#main_content pentru a afisa continut." - logout: "Iesire" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Filtre" - pagination: - empty: "Nu am gasit nici un %{model}" - one: "Afisare 1 %{model}" - one_page: "Sunt afisate toate %{n} inregistrarile" - multiple: "Sunt afisate %{from} - %{to} din %{total} inregistrari" - entry: - one: "inregistrare" - other: "inregistrari" - any: "Oricare" - blank_slate: - content: "Momentan nu exista %{resource_name}." - link: "Creati un" + any: Oricare batch_actions: - button_label: "Grupare Actiuni" - delete_confirmation: "Sunteti sigur ca doriti sa stergeti aceste %{plural_model}?" - succesfully_destroyed: - one: "1 %{model} sters" - few: "%{count} %{plural_model} sterse" - other: "%{count} %{plural_model} sterse" - selection_toggle_explanation: "(Modifica Selectia)" - link: "Creati unul" action_label: "%{title} Selectat" + button_label: Grupare Actiuni + default_confirmation: Sunteţi sigur că doriţi să faceţi acest lucru? + delete_confirmation: Sunteţi sigur că doriţi să stergeţi aceste %{plural_model}? labels: - destroy: "Sterge" + destroy: Sterge + selection_toggle_explanation: "(Modifica Selectia)" + successfully_destroyed: + few: "%{count} %{plural_model} sterse" + one: 1 %{model} sters + other: "%{count} %{plural_model} sterse" + blank_slate: + content: Momentan nu exista %{resource_name}. + link: Creati un + cancel: Renuntati comments: - body: "Text" - author: "Autor" - title: "Comentariu" - add: "Adaugati comentariu" - resource: "Resursa" - no_comments_yet: "Nu exista comentarii." - title_content: "Comentarii (%{count})" + add: Adaugati comentariu + author: Autor + body: Text errors: - empty_text: "Comentariul nu a fost salvat, textul lipseste." + empty_text: Comentariul nu a fost salvat, textul lipseste. + no_comments_yet: Nu exista comentarii. + resource: Resursa + title_content: Comentarii (%{count}) + dashboard: Pagina Principala + delete: Stergeti + delete_confirmation: Sigur vreti sa stergeti? + delete_model: Stergeti %{model} + details: Detalii %{model} devise: + change_password: + submit: Schimbă parola + title: Schimbați parola + links: + forgot_your_password: Ați uitat parola? + sign_in: Autentificare + sign_in_with_omniauth_provider: Conectați-vă cu %{provider} login: - title: "Autentificare" - remember_me: "Tine-ma minte" - submit: "Autentificare" + remember_me: Tine-ma minte + submit: Autentificare + title: Autentificare reset_password: - title: "Ați uitat parola?" - submit: "Reseta parola" - change_password: - title: "Schimbați parola" - submit: "Schimbă parola" + submit: Reseta parola + title: Ați uitat parola? unlock: - title: "Retrimite instrucțiunile de deblocare" - submit: "Retrimite instrucțiunile de deblocare" - links: - sign_in: "Autentificare" - forgot_your_password: "Ați uitat parola?" - sign_in_with_omniauth_provider: "Conectați-vă cu %{provider}" + submit: Retrimite instrucțiunile de deblocare + title: Retrimite instrucțiunile de deblocare + download: 'Descarcati:' + edit: Modificati + edit_model: Modificati %{model} + empty: Gol + filters: + buttons: + clear: Stergeti filtrele + filter: Cautati + has_many_delete: Stergeti + has_many_new: Adaugati un nou %{model} + has_many_remove: Scoate + logout: Iesire + new_model: Un nou %{model} + next: Inainte + pagination: + empty: Nu am gasit nici un %{model} + entry: + few: înregistrări + one: înregistrăre + other: înregistrări + multiple: Sunt afisate %{from} - %{to} din %{total} inregistrari + multiple_without_total: Sunt afisate %{from} - %{to} + one: Afisare 1 %{model} + one_page: Sunt afisate toate %{n} inregistrarile + powered_by: Powered by %{active_admin} %{version} + previous: Inapoi + sidebars: + filters: Filtre + status_tag: + 'no': Nu + unset: Nu + 'yes': Da + view: Vizualizati diff --git a/config/locales/ru.yml b/config/locales/ru.yml index a13d5701be1..74064738953 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -1,102 +1,155 @@ +--- ru: active_admin: - dashboard: "Панель управления" - dashboard_welcome: - welcome: "Добро пожаловать в Active Admin. Это стандартная страница управления сайтом." - call_to_action: "Чтобы добавить сюда что-нибудь загляните в 'app/admin/dashboard.rb'" - view: "Открыть" - edit: "Изменить" - delete: "Удалить" - delete_confirmation: "Вы уверены, что хотите удалить это?" - new_model: "Создать %{model}" - create_model: "Создать %{model}" - edit_model: "Изменить %{model}" - update_model: "Изменить %{model}" - delete_model: "Удалить %{model}" - details: "%{model} подробнее" - cancel: "Отмена" - empty: "Пусто" - previous: "Пред." - next: "След." - download: "Загрузка:" - has_many_new: "Добавить %{model}" - has_many_delete: "Убрать" - has_many_remove: "Удалить" - filters: - buttons: - filter: "Фильтровать" - clear: "Очистить" - predicates: - contains: "Содержит" - equals: "=" - starts_with: "Начинается с" - ends_with: "Заканчивается" - greater_than: "больше" - less_than: "меньше" - main_content: "Создайте %{model}#main_content для отображения содержимого." - logout: "Выйти" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Фильтры" - pagination: - empty: "%{model} не найдено" - one: "Результат: 1 %{model}" - one_page: "Результат: %{n} %{model}" - multiple: "Результат: %{model} %{from} - %{to} из %{total}" - entry: - one: "запись" - other: "записей" - any: "Любой" - blank_slate: - content: "Пока нет %{resource_name}." - link: "Создать" + access_denied: + message: Вы не авторизованы для выполнения данного действия. + any: Любой batch_actions: - button_label: "Групповые операции" - delete_confirmation: "Вы уверены, что хотите удалить %{plural_model}? Вы не сможете это отменить." - succesfully_destroyed: - one: "Успешно удалено: 1 %{model}" - few: "Успешно удалено: %{count} %{plural_model}" - many: "Успешно удалено: %{count} %{plural_model}" - other: "Успешно удалено: %{count} %{plural_model}" - selection_toggle_explanation: "(Отметить всё / Снять выделение)" - link: "Создать" action_label: "%{title} выбранное" + button_label: Групповые операции + default_confirmation: Вы уверены, что вы хотите это сделать? + delete_confirmation: Вы уверены, что хотите удалить %{plural_model}? labels: - destroy: "Удалить" + destroy: Удалить + selection_toggle_explanation: "(Отметить всё / Снять выделение)" + successfully_destroyed: + few: 'Успешно удалено: %{count} %{plural_model}' + many: 'Успешно удалено: %{count} %{plural_model}' + one: 'Успешно удалено: 1 %{model}' + other: 'Успешно удалено: %{count} %{plural_model}' + blank_slate: + content: Пока нет %{resource_name}. + link: Создать + cancel: Отмена comments: - resource_type: "Тип ресурса" - author_type: "Тип автора" - body: "Текст" - author: "Автор" - title: "Комментарий" - add: "Добавить Комментарий" - resource: "Ресурс" - no_comments_yet: "Пока нет комментариев." - title_content: "Комментарии (%{count})" + add: Добавить Комментарий + author: Автор + author_missing: Аноним + author_type: Тип автора + body: Текст + created_at: Дата создания + delete: Удалить Комментарий + delete_confirmation: Вы уверены, что хотите удалить этот комментарий? errors: - empty_text: "Комментарий не сохранен, текст не должен быть пустым." + empty_text: Комментарий не сохранен, текст не должен быть пустым. + no_comments_yet: Пока нет комментариев. + resource: Ресурс + resource_type: Тип ресурса + title_content: Комментарии (%{count}) + create_another: Создать ещё %{model} + dashboard: Панель управления + delete: Удалить + delete_confirmation: Вы уверены, что хотите удалить это? + delete_model: Удалить %{model} + details: "%{model} подробнее" devise: + change_password: + submit: Изменение пароля + title: Изменение пароля + email: + title: Эл. почта + links: + forgot_your_password: Забыли пароль? + resend_confirmation_instructions: Повторная отправка инструкций подтверждения + resend_unlock_instructions: Повторная отправка инструкций разблокировки + sign_in: Войти + sign_in_with_omniauth_provider: Войти с помощью %{provider} + sign_up: Зарегистрироваться login: - title: "Войти" - remember_me: "Запомнить меня" - submit: "Войти" + remember_me: Запомнить меня + submit: Войти + title: Войти + password: + title: Пароль + password_confirmation: + title: Подтверждение пароля + resend_confirmation_instructions: + submit: Выслать повторно письмо с активацией + title: Выслать повторно письмо с активацией reset_password: - title: "Забыли пароль?" - submit: "Сбросить пароль" - change_password: - title: "Изменение пароля" - submit: "Изменение пароля" + submit: Сбросить пароль + title: Забыли пароль? + sign_up: + submit: Зарегистрироваться + title: Зарегистрироваться + subdomain: + title: Поддомен unlock: - title: "Повторно отправить инструкции по разблокировке" - submit: "Повторно отправить инструкции по разблокировке" - links: - sign_in: "Войти" - forgot_your_password: "Забыли пароль?" - sign_in_with_omniauth_provider: "Войти с помощью %{provider}" - access_denied: - message: "Вы не авторизованы для выполнения данного действия." + submit: Повторно отправить инструкции по разблокировке + title: Повторно отправить инструкции по разблокировке + username: + title: Имя пользователя + download: 'Загрузка:' + edit: Изменить + edit_model: Изменить %{model} + empty: Пусто + filters: + buttons: + clear: Очистить + filter: Фильтровать + predicates: + from: От + to: До + has_many_delete: Удалить + has_many_new: Добавить %{model} + has_many_remove: Убрать index_list: - table: "Таблица" - block: "Список" - grid: "Сетка" - blog: "Блог" + table: Таблица + logout: Выйти + move: Переместить + new_model: Создать %{model} + next: След. + pagination: + empty: "%{model} не найдено" + entry: + few: записи + many: записей + one: запись + other: записей + multiple: 'Результат: %{model} %{from} - %{to} из %{total}' + multiple_without_total: 'Результат: %{model} %{from} - %{to}' + next: Следующая + one: 'Результат: 1 %{model}' + one_page: 'Результат: %{n} %{model}' + per_page: 'На странице ' + previous: Предыдущая + powered_by: Работает на %{active_admin} %{version} + previous: Пред. + scopes: + all: Все + search_status: + no_current_filters: Ни один + title: Текущий поиск + title_with_scope: Текущий поиск %{name} + sidebars: + filters: Фильтры + search_status: Статус поиска + status_tag: + 'no': Нет + unset: Нет + 'yes': Да + toggle_dark_mode: Переключить тёмную тему + toggle_main_navigation_menu: Переключить главное меню + toggle_section: Переключить секцию + toggle_user_menu: Переключить пользовательское меню + view: Открыть + activerecord: + attributes: + active_admin/comment: + author_type: Тип автора + body: Текст + created_at: Дата создания + namespace: Пространство имён + resource_type: Тип ресурса + updated_at: Дата обновления + models: + active_admin/comment: + few: Комментария + many: Комментариев + one: Комментарий + other: Комментариев + comment: + few: Комментария + many: Комментариев + one: Комментарий + other: Комментариев diff --git a/config/locales/sk.yml b/config/locales/sk.yml new file mode 100644 index 00000000000..a5bc92149f6 --- /dev/null +++ b/config/locales/sk.yml @@ -0,0 +1,145 @@ +--- +sk: + active_admin: + access_denied: + message: Nemáte oprávnenie k vykonaniu tejto akcie. + any: Akákoľvek + batch_actions: + action_label: "%{title}" + button_label: Hromadné akcie + default_confirmation: Ste si istí, že to chcete spraviť? + delete_confirmation: Ste si istí, že chcete zmazať tieto %{plural_model}? + labels: + destroy: Vymazať + selection_toggle_explanation: "(Zmeniť výber)" + successfully_destroyed: + few: Úspešne zmazané %{count} %{plural_model} + one: Úspešne zmazaný %{model} + other: Úspešne zmazaných %{count} %{plural_model} + zero: Nebol zmazaný žiaden %{model} + blank_slate: + content: Zatiaľ tu nie je žiadny obsah. + link: Vytvoriť + cancel: Zrušiť + comments: + add: Pridať komentár + author: Autor + author_missing: Anonymný + author_type: Typ autora + body: Telo + created_at: Vytvorený + delete: Zmazať komentár + delete_confirmation: Naozaj chcete zmazať tento komentár? + errors: + empty_text: Komentár nebol uložený, je prázdny. + no_comments_yet: Žiadny komentár + resource: Zdroj + resource_type: Typ zdroja + title_content: Komentáre administrátorov (%{count}) + create_another: Vytvoriť ďalší %{model} + dashboard: Úvod + delete: Zmazať + delete_confirmation: Ste si istí, že chcete túto položku zmazať? + delete_model: Zmazať + details: Detaily + devise: + change_password: + submit: Zmeniť svoje heslo + title: Zmeniť heslo + email: + title: Email + links: + forgot_your_password: Zabudli ste heslo? + resend_confirmation_instructions: Preposlať potvrdzovacie inštrukcie + resend_unlock_instructions: Poslať znovu inštrukcie na odomknutie účtu + sign_in: Prihlásiť sa + sign_in_with_omniauth_provider: Prihlásiť sa cez %{provider} + sign_up: Registrovať sa + login: + remember_me: Zapamätať si ma + submit: Prihlásiť + title: Prihlásenie + password: + title: Heslo + password_confirmation: + title: Potvrdenie hesla + resend_confirmation_instructions: + submit: Preposlať potvrdzovacie inštrukcie + title: Preposlanie potvrdzovacie inštrukcie + reset_password: + submit: Obnoviť heslo + title: Zabudli ste heslo? + sign_up: + submit: Registrovať + title: Registrácia + subdomain: + title: Subdoména + unlock: + submit: Zaslať inštrukcií k odomknutiu účtu + title: Zaslanie inštrukcií k odomknutiu účtu + username: + title: Užívateľské meno + download: 'Stiahnúť:' + edit: Upraviť + edit_model: Upraviť + empty: Prázdne + filters: + buttons: + clear: Vyčistiť filtre + filter: Filtrovať + predicates: + from: Od + to: Do + has_many_delete: Zmazať + has_many_new: Pridať nový + has_many_remove: Odstrániť + index_list: + table: Tabuľka + logout: Odhlásiť + move: Presunúť + new_model: Vytvoriť + next: Nasledujúce + pagination: + empty: Nenájdený. + entry: + few: položky + one: položka + other: položky + multiple: "%{from} - %{to} z %{total}" + multiple_without_total: "%{from} - %{to}" + one: Zobrazená 1 položka + one_page: Počet zobrazených položiek %{n} + powered_by: "%{active_admin} %{version}" + previous: Predchádzajúce + scopes: + all: Všetko + search_status: + no_current_filters: Žiadne + sidebars: + filters: Filtre + search_status: Stav vyhľadávania + status_tag: + 'no': Nie + unset: Nie + 'yes': Áno + view: Zobraziť + activerecord: + attributes: + active_admin/comment: + author_type: Typ autora + body: Telo + created_at: Vytvorený + namespace: Namespace + resource_type: Typ komentovanej položky + updated_at: Upravený + models: + active_admin/comment: + few: Komentáre + many: Komentárov + one: Komentár + other: Komentáre + comment: + few: Komentáre + many: Komentárov + one: Komentár + other: Komentáre diff --git a/config/locales/sv-SE.yml b/config/locales/sv-SE.yml index 47b4f5af1c4..647c23896da 100644 --- a/config/locales/sv-SE.yml +++ b/config/locales/sv-SE.yml @@ -1,88 +1,139 @@ -"sv-SE": +--- +sv-SE: active_admin: - dashboard: Skrivbord - dashboard_welcome: - welcome: "Välkommen till Active Admin. Detta är ditt standardskrivbord." - call_to_action: "För att lägga till sektioner, gör en checkout på 'app/admin/dashboard.rb'" - view: "Visa" - edit: "Editera" - delete: "Ta bort" - delete_confirmation: "Är du säker att du vill ta bort denna?" - new_model: "Ny %{model}" - create_model: "Ny %{model}" - edit_model: "Editera %{model}" - update_model: "Editera %{model}" - delete_model: "Ta bort %{model}" - details: "Detaljvy för %{model}" - cancel: "Avbryt" - empty: "Tom" - previous: "Föregående" - next: "Nästa" - download: "Ladda ner:" - has_many_new: "Skapa en ny %{model}" - has_many_delete: "Ta bort" - has_many_remove: "Ta bort" - filters: - buttons: - filter: "Filter" - clear: "Töm dina filters" - predicates: - contains: "Innehåller" - equals: "Lika med" - starts_with: "Börjar med" - ends_with: "Slutar med" - greater_than: "Större än" - less_than: "Mindre än" - main_content: "Implementera %{model}#main_content för att kunna visa något." - logout: "Logga ut" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Filter" - pagination: - empty: "Ingen %{model} funnen" - one: "Visar 1 utav %{model}" - one_page: "Visar alla %{n} utav %{model}" - multiple: "Visar %{model} %{from} - %{to} av %{total} totalt" - entry: - one: "inlägg" - other: "inlägg" - any: "Någon" - blank_slate: - content: "Finns ingen %{resource_name} än." - link: "Skapa en" + access_denied: + message: Du har inte behörighet att utföra denna åtgärd. + any: Alla batch_actions: - button_label: "Batch Behandling" - delete_confirmation: "Är du säker på att du vill radera dessa %{plural_model}? Du kan inte ångra detta." - succesfully_destroyed: - one: "Lyckades radera 1 %{model}" - other: "Lyckades radera %{count} %{plural_model}" - selection_toggle_explanation: "(Toggle Selection)" - link: "Skapa en" - action_label: "%{title} Markerad" + action_label: "%{title} markerad" + button_label: Batch-åtgärder + default_confirmation: Är du säker på att du vill göra detta? + delete_confirmation: Är du säker på att du vill radera dessa %{plural_model}? labels: - destroy: "Radera" + destroy: Radera + selection_toggle_explanation: "(Byt markering)" + successfully_destroyed: + one: Lyckades radera 1 %{model} + other: Lyckades radera %{count} %{plural_model} + blank_slate: + content: Det finns inga %{resource_name} än. + link: Skapa en + cancel: Avbryt comments: - body: "Innehåll" - author: "Författare" - title: "Kommentar" - add: "Lägg till kommentar" - resource: "Resurs" - no_comments_yet: "Inga kommentarer än." - title_content: "Kommentarer (%{count})" + add: Lägg till kommentar + author: Författare + author_missing: Anonym + author_type: Författartyp + body: Innehåll + created_at: Skapad + delete: Radera kommentar + delete_confirmation: Är du säker på att du vill radera dessa kommentarer? errors: - empty_text: "Kommentaren sparades inte, måste innehålla text." + empty_text: Kommentaren sparades inte. Textfältet får inte vara tomt. + no_comments_yet: Inga kommentarer än. + resource: Resurs + resource_type: Resurstyp + title_content: Kommentarer (%{count}) + create_another: Skapa en till %{model} + dashboard: Skrivbord + delete: Ta bort + delete_confirmation: Är du säker på att du vill ta bort detta? + delete_model: Ta bort %{model} + details: "%{model}-detaljer" devise: - login: - title: "inloggning" - remember_me: "Kom ihåg mig" - submit: "inloggning" - reset_password: - title: "Glömt ditt lösenord?" - submit: "Återställa mitt lösenord" change_password: - title: "Ändra ditt lösenord" - submit: "Ändra mitt lösenord" + submit: Ändra mitt lösenord + title: Ändra ditt lösenord + email: + title: E-post links: - sign_in: "Logga in" - forgot_your_password: "Glömt ditt lösenord?" - sign_in_with_omniauth_provider: "Logga in med %{provider}" + forgot_your_password: Glömt ditt lösenord? + resend_confirmation_instructions: Skicka bekräftningsinstruktioner igen + resend_unlock_instructions: Skicka upplåsningsinstruktioner igen + sign_in: Logga in + sign_in_with_omniauth_provider: Logga in med %{provider} + sign_up: Registera + login: + remember_me: Kom ihåg mig + submit: Inloggning + title: Inloggning + password: + title: Lösenord + password_confirmation: + title: Bekräfta lösenord + resend_confirmation_instructions: + submit: Skicka bekräftelseinstruktioner + title: Skicka bekräftelseinstruktioner + reset_password: + submit: Återställ mitt lösenord + title: Glömt ditt lösenord? + sign_up: + submit: Registera + title: Registera + subdomain: + title: Subdomän + unlock: + submit: Skicka upplåsningsinstruktioner + title: Skicka upplåsningsinstruktioner + username: + title: Användarnamn + download: 'Ladda ner:' + edit: Redigera + edit_model: Redigera %{model} + empty: Tom + filters: + buttons: + clear: Rensa filter + filter: Filtrera + predicates: + from: Från + to: Till + has_many_delete: Ta bort + has_many_new: Skapa en ny %{model} + has_many_remove: Ta bort + index_list: + table: Tabell + logout: Logga ut + move: Flytta + new_model: Ny %{model} + next: Nästa + pagination: + empty: Ingen %{model} hittades + entry: + one: inlägg + other: inlägg + multiple: Visar %{model} %{from} - %{to} av %{total} totalt + multiple_without_total: Visar %{model} %{from} - %{to} + one: Visar 1 %{model} + one_page: Visar alla %{n} %{model} + per_page: 'Per sida: ' + powered_by: Powered by %{active_admin} %{version} + previous: Föregående + scopes: + all: Alla + search_status: + no_current_filters: Inga + sidebars: + filters: Filter + search_status: Sökstatus + status_tag: + 'no': Nej + unset: Nej + 'yes': Ja + view: Visa + activerecord: + attributes: + active_admin/comment: + author_type: Författartyp + body: Innehåll + created_at: Skapad + namespace: Namnrymd + resource_type: Resurstyp + updated_at: Aktualiserad + models: + active_admin/comment: + one: Kommentar + other: Kommentarer + comment: + one: Kommentar + other: Kommentarer diff --git a/config/locales/tr.yml b/config/locales/tr.yml index e05547eb7bc..8f8c647912c 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -1,89 +1,118 @@ +--- tr: active_admin: - dashboard: "Kontrol Paneli" - dashboard_welcome: - welcome: "Active Admin'e Hoşgeldiniz. Burası öntanımlı Kontrol Paneli sayfasıdır." - call_to_action: "Kontrol Paneline bölümler eklemek için 'app/admin/dashboard.rb' dosyasına bakabilirsin." - view: "Görüntüle" - edit: "Değiştir" - delete: "Sil" - delete_confirmation: "Bu kaydı silmek istediğine emin misin?" - new_model: "Yeni %{model}" - create_model: "Yeni %{model}" - edit_model: "%{model} modelini düzenle" - update_model: "%{model} modelini düzenle" - delete_model: "%{model} modelini sil" - details: "%{model} ayrıntıları" - cancel: "İptal" - empty: "Boş" - previous: "Önceki" - next: "Sonraki" - download: "İndir:" - has_many_new: "Yeni %{model} kaydı ekle" - has_many_delete: "Sil" - has_many_remove: "Çıkarmak" - filters: - buttons: - filter: "Filtrele" - clear: "Filtreleri Temizle" - predicates: - contains: "içerir" - equals: "Eşittir" - starts_with: "ile başlar" - ends_with: "ile biter" - greater_than: "Büyükse" - less_than: "Küçükse" - main_content: "İçeriği görüntülemek için lütfen %{model}#main_content metodunu ekleyin." - logout: "Oturumu Sonlandır" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Filtreler" - pagination: - empty: "%{model} boş" - one: "1 %{model} kaydı görüntüleniyor" - one_page: "%{n} kayıt %{model} modelinde görüntüleniyor" - multiple: "%{model} toplam %{total} kayıt bulundu. %{from} ile %{to} arası arası görüntüleniyor" - entry: - one: "Girdi" - other: "Girdiler" - any: "Herhangi" - blank_slate: - content: "Herhangi bir %{resource_name} kaydı bulunamadı" - link: "Bir tane oluşturun" + access_denied: + message: Bu işlemi gerçekleştirmek için yetkiniz yok. + any: Herhangi biri batch_actions: - button_label: "Toplu işlemler" - delete_confirmation: "%{plural_model} kayıtlarını silmek istediğinize emin misiniz? Dikkat bu işlemin geri dönüşü yoktur!" - succesfully_destroyed: - one: "1 %{model} kaydı başarıyla silindi." - other: "Toplam %{count} kayıt %{plural_model} modelinden silindi" - selection_toggle_explanation: "Seçimi Değiştir" - link: "Ekle" - action_label: "%{title} Seçildi" + action_label: Seçilenleri %{title} + button_label: Toplu İşlemler + default_confirmation: Bunu yapmak istediğinizden emin misiniz? + delete_confirmation: Bu %{plural_model} kayıtlarını silmek istediğinizden emin misiniz? labels: - destroy: "Sil" + destroy: Sil + selection_toggle_explanation: "(Seçimi Değiştir)" + successfully_destroyed: + one: 1 %{model} başarıyla silindi + other: Toplam %{count} %{plural_model} başarıyla silindi + blank_slate: + content: Henüz %{resource_name} yok. + link: Bir tane oluşturun + cancel: İptal comments: - body: "Ayrıntı" - author: "Yazar" - title: "Yorum" - add: "Yorum Ekle" - resource: "Kaynak" - no_comments_yet: "Henüz Yorum Yok" - title_content: "Yorumlar (%{count})" + add: Yorum Ekle + author: Yazar + author_missing: Anonim + author_type: Yazar Tipi + body: Ayrıntı + created_at: Oluşturma Tarihi + delete: Yorumu Sil + delete_confirmation: Bu yorumları silmek istediğinizden emin misiniz? errors: - empty_text: "Yorum boş olarak kaydedilemez." + empty_text: Yorum boş olarak kaydedilemez. + no_comments_yet: Henüz yorum yok. + resource: Kayıt + resource_type: Kayıt Tipi + title_content: Yorumlar (%{count}) + create_another: Başka bir %{model} oluştur + dashboard: Gösterge Paneli + delete: Sil + delete_confirmation: Bu kaydı silmek istediğinizden emin misiniz? + delete_model: "%{model} Kaydını Sil" + details: "%{model} Ayrıntıları" devise: - login: - title: "Oturum aç" - remember_me: "Beni Hatırla" - submit: "Gönder" - reset_password: - title: "Şifreni mi unuttun?" - submit: "Şifremi sıfırla" change_password: - title: "Şifrenizi değiştirin" - submit: "Şifremi değiştir" + submit: Şifremi değiştir + title: Şifrenizi değiştirin + email: + title: E-posta adresi links: - sign_in: "Oturum aç" - forgot_your_password: "Şifreni mi unuttun?" + forgot_your_password: Şifrenizi mi unuttunuz? + resend_confirmation_instructions: Onaylama talimatlarını tekrar gönder + resend_unlock_instructions: Hesap geri açma talimatlarını tekrar gönder + sign_in: Giriş yap sign_in_with_omniauth_provider: "%{provider} ile giriş yapın" - + sign_up: Kaydol + login: + remember_me: Beni hatırla + submit: Giriş yap + title: Giriş yap + password: + title: Şifre + password_confirmation: + title: Şifreyi Tekrarla + resend_confirmation_instructions: + submit: Onaylama talimatlarını tekrar gönder + title: Onaylama talimatlarını tekrar gönder + reset_password: + submit: Şifremi sıfırla + title: Şifrenizi mi unuttunuz? + sign_up: + submit: Kaydol + title: Kaydol + subdomain: + title: Alt alan adı + unlock: + submit: Hesap geri açma talimatlarını tekrar gönder + title: Hesap geri açma talimatlarını tekrar gönder + username: + title: Kullanıcı adı + download: 'İndir:' + edit: Düzenle + edit_model: "%{model} Kaydını Düzenle" + empty: Boş + filters: + buttons: + clear: Filtreleri Temizle + filter: Filtrele + has_many_delete: Sil + has_many_new: Yeni %{model} Ekle + has_many_remove: Çıkar + index_list: + table: Tablo + logout: Çıkış Yap + move: Taşı + new_model: Yeni %{model} + next: Sonraki + pagination: + empty: Hiç %{model} yok + entry: + one: kayıt + other: kayıtlar + multiple: "%{from} - %{to} arası %{model} görüntüleniyor (toplam %{total} kayıt)" + multiple_without_total: "%{from} - %{to} arası %{model} görüntüleniyor" + one: "1 %{model} görüntüleniyor" + one_page: "%{n} %{model} kaydının tamamı görüntüleniyor" + per_page: 'Sayfa Başına: ' + powered_by: "%{active_admin} %{version} tarafından desteklenmektedir." + previous: Önceki + search_status: + no_current_filters: Yok + sidebars: + filters: Filtreler + search_status: Arama Durumu + status_tag: + 'no': Hayır + unset: Hayır + 'yes': Evet + view: Görüntüle diff --git a/config/locales/uk.yml b/config/locales/uk.yml index ead005a796c..5cf78bb6640 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -1,99 +1,153 @@ +--- uk: active_admin: - dashboard: "Панель керування" - dashboard_welcome: - welcome: "Ласкаво просимо в Active Admin. Це стандартна сторінка керування сайтом." - call_to_action: "Щоб додати сюди що-небудь загляніть в 'app/admin/dashboard.rb'" - view: "Переглянути" - edit: "Змінити" - delete: "Видалити" - delete_confirmation: "Ви впевнені, що хочете це видалити?" - new_model: "Створити %{model}" - create_model: "Створити %{model}" - edit_model: "Змінити %{model}" - update_model: "Змінити %{model}" - delete_model: "Видалити %{model}" - details: "%{model} детальніше" - cancel: "Відміна" - empty: "Пусто" - previous: "Поперед." - next: "Наст." - download: "Завантаження:" - has_many_new: "Додати %{model}" - has_many_delete: "Прибрати" - has_many_remove: "Видалити" - filter: "Фільтрувати" - clear_filters: "Очистити" - contains: "Містить" - starts_with: "Починається з" - ends_with: "Закінчується" - equal_to: "=" - greater_than: "більше" - less_than: "менше" - main_content: "Створіть %{model}#main_content для відображення вмісту." - logout: "Вийти" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Фільтри" - pagination: - empty: "%{model} не знайдено" - one: "Результат: 1 %{model}" - one_page: "Результат: %{n} %{model}" - multiple: "Результат: %{model} %{from} - %{to} з %{total}" - entry: - one: "запис" - other: "записів" - any: "Будь-який" - blank_slate: - content: "Покищо немає %{resource_name}." - link: "Створити" + access_denied: + message: Ви не авторизовані для виконання даної дії. + any: Будь-який batch_actions: - button_label: "Групові операції" - delete_confirmation: "Ви впевнені, що хочете видалити %{plural_model}? Ви не зможете це відмінити." - succesfully_destroyed: - one: "Успішно видалено: 1 %{model}" - few: "Успішно видалено: %{count} %{plural_model}" - many: "Успішно видалено: %{count} %{plural_model}" - other: "Успішно видалено: %{count} %{plural_model}" - selection_toggle_explanation: "(Відмінити все / Зняти виділення)" - link: "Створити" action_label: "%{title} вибране" + button_label: Групові операції + default_confirmation: Ви справді бажаєте це зробити? + delete_confirmation: Ви впевнені, що хочете видалити %{plural_model}? labels: - destroy: "Видалити" + destroy: Видалити + selection_toggle_explanation: "(Скасувати все / Зняти виділення)" + successfully_destroyed: + few: 'Успішно видалено: %{count} %{plural_model}' + many: 'Успішно видалено: %{count} %{plural_model}' + one: 'Успішно видалено: 1 %{model}' + other: 'Успішно видалено: %{count} %{plural_model}' + blank_slate: + content: Поки-що немає %{resource_name}. + link: Створити + cancel: Скасувати comments: - resource_type: "Тип ресурса" - author_type: "Тип автора" - body: "Текст" - author: "Автор" - title: "Коментар" - add: "Додати Коментар" - resource: "Ресурс" - no_comments_yet: "Покищо немає коментарів." - title_content: "Коментарі (%{count})" + add: Додати Коментар + author: Автор + author_missing: Анонім + author_type: Тип автора + body: Текст + created_at: Дата створення + delete: Видалити Коментар + delete_confirmation: Ви впевнені, що хочете видалити цей коментар? errors: - empty_text: "Коментар не збережено, текст не повинен бути пустим." + empty_text: Коментар не збережено, текст не повинен бути пустим. + no_comments_yet: Поки-що немає коментарів. + resource: Ресурс + resource_type: Тип ресурсу + title_content: Коментарі (%{count}) + create_another: Створити ще %{model} + dashboard: Панель керування + delete: Видалити + delete_confirmation: Ви впевнені, що хочете це видалити? + delete_model: Видалити %{model} + details: "%{model} детальніше" devise: + change_password: + submit: Змінити пароль + title: Зміна паролю + email: + title: Електронна пошта + links: + forgot_your_password: Забули пароль? + resend_confirmation_instructions: Повторна відправка інструкцій підтвердження + resend_unlock_instructions: Повторна відправка інструкцій розблокування + sign_in: Увійти + sign_in_with_omniauth_provider: Увійти з допомогою %{provider} + sign_up: Зареєструватись login: - title: "Вхід" - remember_me: "Запам'ятати мене" - submit: "Увійти" + remember_me: Запам'ятати мене + submit: Увійти + title: Вхід + password: + title: Пароль + resend_confirmation_instructions: + submit: Відправити повторно листа з активацією + title: Відправити повторно листа з активацією reset_password: - title: "Забули пароль?" - submit: "Скинути пароль" - change_password: - title: "Зміна паролю" - submit: "Змінити пароль" + submit: Скинути пароль + title: Забули пароль? + sign_up: + submit: Зареєструватися + title: Зареєструватися + subdomain: + title: Піддомен unlock: - title: "Відправити повторно інструкції з розблокування" - submit: "Відправити повторно інструкції з розблокування" - links: - sign_in: "Увійти" - forgot_your_password: "Забули пароль?" - sign_in_with_omniauth_provider: "Увійти з допомогою %{provider}" - access_denied: - message: "Ви не авторизовані для виконання даної дії." + submit: Відправити повторно інструкції з розблокування + title: Відправити повторно інструкції з розблокування + username: + title: Ім'я користувача + download: 'Завантаження:' + edit: Змінити + edit_model: Змінити %{model} + empty: Пусто + filters: + buttons: + clear: Очистити + filter: Фільтрувати + predicates: + from: від + to: до + has_many_delete: Прибрати + has_many_new: Додати %{model} + has_many_remove: Видалити index_list: - table: "Таблиця" - block: "Список" - grid: "Сітка" - blog: "Блог" + table: Таблиця + logout: Вийти + move: Перемістити + new_model: Створити %{model} + next: Наст. + pagination: + empty: "%{model} не знайдено" + entry: + few: записи + many: записів + one: запис + other: записів + multiple: 'Результат: %{model} %{from} - %{to} з %{total}' + multiple_without_total: 'Результат: %{model} %{from} - %{to}' + next: Наступна + one: 'Результат: 1 %{model}' + one_page: 'Результат: %{n} %{model}' + per_page: 'На сторінці ' + previous: Попередня + powered_by: Powered by %{active_admin} %{version} + previous: Поперед. + scopes: + all: Всі + search_status: + no_current_filters: Жоден + title: Поточний пошук + title_with_scope: Поточний пошук %{name} + sidebars: + filters: Фільтри + search_status: Статус пошуку + status_tag: + 'no': Ні + unset: Ні + 'yes': Так + toggle_dark_mode: Перемкнути темну тему + toggle_main_navigation_menu: Перемкнути головне меню + toggle_section: Перемкнути секцію + toggle_user_menu: Перемкнути меню користувача + view: Переглянути + activerecord: + attributes: + active_admin/comment: + author_type: Тип автора + body: Текст + created_at: Дата створення + namespace: Простір імен + resource_type: Тип ресурсу + updated_at: Дата оновлення + models: + active_admin/comment: + few: Коментаря + many: Коментарів + one: Коментар + other: Коментарів + comment: + few: Коментаря + many: Коментарів + one: Коментар + other: Коментарів diff --git a/config/locales/vi.yml b/config/locales/vi.yml index a8b05e5f7ec..92bd2d87812 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -1,89 +1,148 @@ +--- vi: active_admin: - dashboard: Dashboard - dashboard_welcome: - welcome: "Chào mừng bạn đến với Active Admin. Đây là trang Dashboard mặc định." - call_to_action: "Để thêm phần phần cho trang Dashboar hãy chỉnh sửa 'app/admin/dashboard.rb'" - view: "Xem" - edit: "Chỉnh sửa" - delete: "Xóa" - delete_confirmation: "Bạn có chắc chắn rằng mình muốn xóa cái này?" - new_model: "Tạo mới %{model}" - create_model: "Tạo mới %{model}" - edit_model: "Chỉnh sửa %{model}" - update_model: "Chỉnh sửa %{model}" - delete_model: "Xóa %{model}" - details: "%{model} Chi tiết" - cancel: "Hủy" - empty: "Trống" - previous: "Trước" - next: "Tiếp" - download: "Download:" - has_many_new: "Thêm mới %{model}" - has_many_delete: "Xóa" - has_many_remove: "Hủy bỏ" - filters: - buttons: - filter: "Lọc" - clear: "Xóa dữ liệu lọc" - predicates: - contains: "Thông tin" - equals: "Bằng" - starts_with: "Bắt đầu với" - ends_with: "Kết thúc với việc" - greater_than: "Lớn hơn" - less_than: "Nhỏ hơn" - main_content: "Xin bổ sung %{model}#main_content để hiển thị nội dung." - logout: "Đăng xuất" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "Bộ Lọc" - pagination: - empty: "Không có %{model} nào được tìm thấy" - one: "Đang hiển thị 1 %{model}" - one_page: "Đang hiển thị tất cả %{n} %{model}" - multiple: "Đang hiển thị %{model} %{from} - %{to} of %{total} trong tất cả." - entry: - one: "entry" - other: "entries" - any: "Bất kỳ" - blank_slate: - content: "Chưa có %{resource_name}." - link: "Tạo mới" + access_denied: + message: Bạn không có quyền thực hiện tính năng này + any: Bất kỳ batch_actions: - button_label: "Hành động hàng loạt" - delete_confirmation: "Bạn có chắc chắn muốn xóa những %{plural_model}? Bạn sẽ không thể lấy lại được dữ liệu." - succesfully_destroyed: - one: "Đã xóa thành công 1 %{model}" - other: "Đã xóa thành công %{count} %{plural_model}" - selection_toggle_explanation: "(Thay đổi lựa chọn)" - link: "Tạo mới" - action_label: "%{title} được chọn" + action_label: "%{title} đã được chọn" + button_label: Hành động hàng loạt + default_confirmation: Bạn có chắc bạn muốn làm điều này? + delete_confirmation: Bạn có chắc chắn muốn xóa những %{plural_model}? labels: - destroy: "Xóa" + destroy: Xóa + selection_toggle_explanation: "(Thay đổi lựa chọn)" + successfully_destroyed: + one: Đã xóa thành công 1 %{model} + other: Đã xóa thành công %{count} %{plural_model} + blank_slate: + content: Chưa có %{resource_name}. + link: Tạo mới + cancel: Hủy comments: - body: "Nội dung" - author: "Tác giả" - title: "Bình luận" - add: "Thêm bình luận" - resource: "Tài nguyên" - no_comments_yet: "Chưa có bình luận." - title_content: "Bình luận (%{count})" + add: Thêm bình luận + author: Tác giả + author_missing: Vô danh + author_type: Loại tác giả + body: Nội dung + created_at: Đã tạo + delete: Xoá bình luận + delete_confirmation: Bạn có chắc chắn muốn xóa những bình luận này? errors: - empty_text: "Lời bình luận chưa được lưu, vì nội dung còn trống." + empty_text: Lời bình luận chưa được lưu, vì nội dung còn trống. + no_comments_yet: Chưa có bình luận. + resource: Resource + resource_type: Resource Type + title_content: Bình luận (%{count}) + create_another: Tạo thêm %{model} + dashboard: Bảng điều khiển + delete: Xóa + delete_confirmation: Bạn có chắc chắn rằng mình muốn xóa không? + delete_model: Xóa %{model} + details: "%{model} Chi tiết" devise: - login: - title: "Đăng nhập" - remember_me: "Ghi nhớ tôi" - submit: "Đăng nhập" - reset_password: - title: "Quên mật khẩu của bạn?" - submit: "Thiết lập lại mật khẩu của tôi" change_password: - title: "Thay đổi mật khẩu của bạn" - submit: "Thay đổi mật khẩu của tôi" + submit: Thay đổi mật khẩu của tôi + title: Thay đổi mật khẩu của bạn + email: + title: Email links: - sign_in: "Đăng nhập" - forgot_your_password: "Quên mật khẩu của bạn?" - sign_in_with_omniauth_provider: "Đăng nhập với %{provider}" - + forgot_your_password: Quên mật khẩu của bạn? + resend_confirmation_instructions: Gửi lại hướng dẫn xác nhận + resend_unlock_instructions: Gửi lại hướng dẫn mở khoá + sign_in: Đăng nhập + sign_in_with_omniauth_provider: Đăng nhập với %{provider} + sign_up: Đăng ký + login: + remember_me: Ghi nhớ tôi + submit: Đăng nhập + title: Đăng nhập + password: + title: Mật khẩu + password_confirmation: + title: Mật khẩu xác nhận + resend_confirmation_instructions: + submit: Gửi lại hướng dẫn xác nhận + title: Gửi lại hướng dẫn xác nhận + reset_password: + submit: Thiết lập lại mật khẩu của tôi + title: Quên mật khẩu của bạn? + sign_up: + submit: Đăng ký + title: Đăng ký + subdomain: + title: Tên miền phụ + unlock: + submit: Gửi lại hướng dẫn mở khoá + title: Gửi lại hướng dẫn mở khoá + username: + title: Tên người dùng + download: 'Tải về:' + edit: Chỉnh sửa + edit_model: Chỉnh sửa %{model} + empty: Trống + filters: + buttons: + clear: Xóa dữ liệu lọc + filter: Lọc + predicates: + from: Từ + to: Đến + has_many_delete: Xóa + has_many_new: Thêm mới %{model} + has_many_remove: Hủy bỏ + index_list: + table: Bảng + logout: Đăng xuất + move: Di chuyển + new_model: Tạo mới %{model} + next: Sau + pagination: + empty: Không có %{model} nào được tìm thấy + entry: + one: entry + other: entries + multiple: Đang hiển thị %{model} %{from} - %{to} of %{total} trong tất cả. + multiple_without_total: Đang hiển thị %{model} %{from} - %{to}. + next: Tiếp + one: Đang hiển thị 1 %{model} + one_page: Đang hiển thị tất cả %{n} %{model} + per_page: 'Mỗi trang: ' + previous: Trước + truncate: "…" + powered_by: Bản quyền bởi %{active_admin} %{version} + previous: Trước + scopes: + all: Tất cả + search_status: + no_current_filters: Không có + title: Bộ lọc hiện tại + title_with_scope: Đang tìm kiếm theo %{name} + sidebars: + filters: Bộ Lọc + search_status: Trạng thái tìm kiếm + status_tag: + 'no': Không Có + unset: Không Có + 'yes': Có + toggle_dark_mode: Bật/tắt chế độ tối + toggle_main_navigation_menu: Bật/tắt menu điều hướng chính + toggle_section: Phần chuyển đổi + toggle_user_menu: Bật/tắt menu người dùng + view: Xem + activerecord: + attributes: + active_admin/comment: + author_type: Loại tác giả + body: Nội dung + created_at: Đã tạo + namespace: Namespace + resource_type: Resource type + updated_at: Đã cập nhật + models: + active_admin/comment: + one: Bình luận + other: Các bình luận + comment: + one: Bình luận + other: Các bình luận diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 6500249c696..db63e704970 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -1,89 +1,148 @@ -"zh-CN": +--- +zh-CN: active_admin: - dashboard: "控制面板" - dashboard_welcome: - welcome: "欢迎使用Active Admin. 这是默认的控制面板页." - call_to_action: "若要添加新的面板内容, 请修改 'app/admin/dashboard.rb'" - view: "查看" - edit: "编辑" - delete: "删除" - delete_confirmation: "确定删除?" - new_model: "新建%{model}" - create_model: "新建%{model}" - edit_model: "编辑%{model}" - update_model: "编辑%{model}" - delete_model: "删除%{model}" - details: "%{model}详情" - cancel: "取消" - empty: "清空" - previous: "上一个" - next: "下一个" - download: "下载:" - has_many_new: "新建一个%{model}" - has_many_delete: "删除" - has_many_remove: "清除" - filters: - buttons: - filter: "过滤" - clear: "清除条件" - predicates: - contains: "包含" - equals: "等于" - starts_with: "开头" - ends_with: "完与" - greater_than: "大于" - less_than: "小于" - main_content: "请执行 %{model}#main_content 来显示内容." - logout: "退出" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "所有条件" - pagination: - empty: "暂时没有%{model}" - one: "显示 1 %{model}" - one_page: "显示 所有 %{n} %{model}" - multiple: "显示所有 %{total} %{model}中的%{from} - %{to} 条" - entry: - one: "条目" - other: "条目" - any: "任何" - blank_slate: - content: "暂时还没有%{resource_name}." - link: "新建一个" + access_denied: + message: 您无权处理此操作 + any: 任何 batch_actions: - button_label: "批处理" - delete_confirmation: "你确定要删除所有%{plural_model}? 如果执行,将不可恢复." - succesfully_destroyed: - one: "成功删除 1 %{model}" - other: "成功删除 %{count} %{plural_model}" - selection_toggle_explanation: "(切换选择)" - link: "新建一个" action_label: "%{title} 被选中" + button_label: 批处理 + default_confirmation: 你确定要这样做? + delete_confirmation: 你确定要删除这些%{plural_model}? labels: - destroy: "删除" + destroy: 删除 + selection_toggle_explanation: "(切换选择)" + successfully_destroyed: + one: 成功删除 1 %{model} + other: 成功删除 %{count} %{plural_model} + blank_slate: + content: 暂时还没有%{resource_name}。 + link: 新增一个 + cancel: 取消 comments: - body: "内容" - author: "作者" - title: "评论" - add: "添加评论" - resource: "资源" - no_comments_yet: "暂时没有评论" - title_content: "(%{count})条评论" + add: 添加评论 + author: 作者 + author_missing: 匿名 + author_type: 作者类型 + body: 内容 + created_at: 创建于 + delete: 删除评论 + delete_confirmation: 你确定删除这些评论? errors: - empty_text: "评论保存失败,内空不能为空." + empty_text: 评论保存失败,内空不能为空。 + no_comments_yet: 暂时没有评论 + resource: 资源 + resource_type: 资源类型 + title_content: "(%{count})条评论" + create_another: 新增另一个%{model} + dashboard: 控制面板 + delete: 删除 + delete_confirmation: 确定删除? + delete_model: 删除%{model} + details: "%{model}详情" devise: - login: - title: "登录" - remember_me: "记住我" - submit: "登录" - reset_password: - title: "忘记密码?" - submit: "重置密码" change_password: - title: "更改您的密码" - submit: "更改我的密码" + submit: 修改密码 + title: 修改密码 + email: + title: 邮箱 links: - sign_in: "登录" - forgot_your_password: "忘记密码?" - sign_in_with_omniauth_provider: "登入%{provider}" - + forgot_your_password: 忘记了密码? + resend_confirmation_instructions: 重发确认邮件 + resend_unlock_instructions: 重发解锁邮件 + sign_in: 登录 + sign_in_with_omniauth_provider: 通过%{provider}登录 + sign_up: 注册 + login: + remember_me: 记住我 + submit: 登录 + title: 登录 + password: + title: 密码 + password_confirmation: + title: 确认密码 + resend_confirmation_instructions: + submit: " 重新发送确认说明" + title: " 重新发送确认说明" + reset_password: + submit: 重置我的密码 + title: 忘记了密码? + sign_up: + submit: 注册 + title: 注册 + subdomain: + title: 子域 + unlock: + submit: 重新发送送解锁命令 + title: 重新发送送解锁命令 + username: + title: 用户名 + download: 下载: + edit: 编辑 + edit_model: 编辑%{model} + empty: 未定义 + filters: + buttons: + clear: 清除条件 + filter: 过滤 + predicates: + from: 起 + to: 止 + has_many_delete: 删除 + has_many_new: 新增一个%{model} + has_many_remove: 清除 + index_list: + table: 表格 + logout: 退出 + move: 移动 + new_model: 新增%{model} + next: 下一个 + pagination: + empty: 暂时没有%{model} + entry: + one: 条目 + other: 条目 + multiple: 显示所有 %{total} %{model}中的%{from} - %{to} 条 + multiple_without_total: "%{model}中的%{from} - %{to} 条" + next: 下一页 + one: 显示 1 %{model} + one_page: 显示 所有 %{n} %{model} + per_page: 每页: + previous: 上一页 + truncate: "…" + powered_by: 构建程序为 %{active_admin} %{version} + previous: 上一个 + scopes: + all: 所有 + search_status: + no_current_filters: 无 + title: 搜索条件 + title_with_scope: 搜索条件 %{name} + sidebars: + filters: 所有条件 + search_status: 搜索条件 + status_tag: + 'no': 否 + unset: 否 + 'yes': 是 + toggle_dark_mode: 切换深色模式 + toggle_main_navigation_menu: 切换主导航 + toggle_section: 切换区块 + toggle_user_menu: 切换用户菜单 + view: 查看 + activerecord: + attributes: + active_admin/comment: + author_type: 作者类型 + body: 内容 + created_at: 创建 + namespace: Namespace + resource_type: Resource 类型 + updated_at: 更新 + models: + active_admin/comment: + one: 评论 + other: 评论 + comment: + one: 评论 + other: 评论 diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index cd353599ae4..65ccc8f8977 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -1,89 +1,148 @@ -"zh-TW": +--- +zh-TW: active_admin: - dashboard: 儀表板 - dashboard_welcome: - welcome: "歡迎來到 Active Admin 這是預設的儀表板頁面。" - call_to_action: "要新增儀表板內容,請查看 'app/admin/dashboard.rb'" - view: "檢視" - edit: "編輯" - delete: "刪除" - delete_confirmation: "你確定要刪除嗎?" - new_model: "新增 %{model}" - create_model: "新增 %{model}" - edit_model: "編輯 %{model}" - update_model: "編輯 %{model}" - delete_model: "刪除 %{model}" - details: "%{model} 明細" - cancel: "取消" - empty: "空的" - previous: "前一個" - next: "下一個" - download: "下載:" - has_many_new: "增加新的 %{model}" - has_many_delete: "刪除" - has_many_remove: "清除" - filters: - buttons: - filter: "篩選" - clear: "清除篩選條件" - predicates: - contains: "包含" - equals: "等於" - starts_with: "开头" - ends_with: "完与" - greater_than: "大於" - less_than: "小於" - main_content: "請編寫 %{model}#main_content 以顯示內容。" - logout: "登出" - powered_by: "Powered by %{active_admin} %{version}" - sidebars: - filters: "篩選條件" - pagination: - empty: "找不到 %{model} " - one: "顯示 1 %{model}" - one_page: "顯示 全部 %{n} %{model}" - multiple: "總計 %{total} 顯示 %{model} 中%{from} - %{to} 筆" - entry: - one: "筆" - other: "筆" - any: "任何" - blank_slate: - content: "尚無 %{resource_name}" - link: "建立一筆" + access_denied: + message: 你沒有權限執行此項操作 + any: 任何 batch_actions: - button_label: "批次作業" - delete_confirmation: "您確定要刪除這些 %{plural_model} 嗎?此動作無法復原。" - succesfully_destroyed: - one: "成功刪除 1 %{model}" - other: "成功刪除 %{count} %{plural_model}" - selection_toggle_explanation: "(切換選取)" - link: "建立一個" action_label: "%{title} 已選取" + button_label: 批次操作 + default_confirmation: 你確定要這樣做嗎? + delete_confirmation: 你確定要刪除這些 %{plural_model} 嗎? labels: - destroy: "刪除" + destroy: 刪除 + selection_toggle_explanation: "(切換選取)" + successfully_destroyed: + one: 成功刪除 1 %{model} + other: 成功刪除 %{count} %{plural_model} + blank_slate: + content: 尚無 %{resource_name}。 + link: 建立一筆 + cancel: 取消 comments: - body: "內文" - author: "作者" - title: "評論" - add: "新增評論" - resource: "資源" - no_comments_yet: "尚無評論" - title_content: "(%{count}) 則評論" + add: 新增評論 + author: 作者 + author_missing: 匿名 + author_type: 作者身份 + body: 內文 + created_at: 建立時間 + delete: 刪除評論 + delete_confirmation: 你確定要刪除這個評論嗎? errors: - empty_text: "評論儲存失敗,不允許空白的內容。" + empty_text: 評論儲存失敗,不允許空白的內容。 + no_comments_yet: 尚無評論 + resource: 資源 + resource_type: 資源類型 + title_content: "%{count} 則評論" + create_another: 新增另一個 %{model} + dashboard: 儀表板 + delete: 刪除 + delete_confirmation: 你確定要刪除嗎? + delete_model: 刪除 %{model} + details: "%{model} 明細" devise: - login: - title: "登錄" - remember_me: "記住我" - submit: "登錄" - reset_password: - title: "忘記密碼?" - submit: "重置密碼" change_password: - title: "更改您的密碼" - submit: "更改我的密碼" + submit: 更改我的密碼 + title: 更改你的密碼 + email: + title: 電子郵件信箱 links: - sign_in: "登錄" - forgot_your_password: "忘記密碼?" - sign_in_with_omniauth_provider: "登入%{provider}" - + forgot_your_password: 忘記密碼? + resend_confirmation_instructions: 重新發送確認信 + resend_unlock_instructions: 重新發送解鎖指示 + sign_in: 登入 + sign_in_with_omniauth_provider: 使用 %{provider} 登入 + sign_up: 註冊 + login: + remember_me: 記住我 + submit: 登入 + title: 登入 + password: + title: 密碼 + password_confirmation: + title: 確認密碼 + resend_confirmation_instructions: + submit: 重新發送確認信 + title: 重新發送確認信 + reset_password: + submit: 重設密碼 + title: 忘記密碼? + sign_up: + submit: 註冊 + title: 註冊 + subdomain: + title: 子網域 + unlock: + submit: 重新發送解鎖指示 + title: 重新發送解鎖指示 + username: + title: 帳號 + download: 下載: + edit: 編輯 + edit_model: 編輯 %{model} + empty: 空的 + filters: + buttons: + clear: 清除篩選條件 + filter: 篩選 + predicates: + from: 從 + to: 到 + has_many_delete: 刪除 + has_many_new: 增加新的 %{model} + has_many_remove: 移除 + index_list: + table: 表格 + logout: 登出 + move: 移動 + new_model: 新增 %{model} + next: 下一個 + pagination: + empty: 找不到 %{model} + entry: + one: 筆 + other: 筆 + multiple: 總計 %{total} 顯示 %{model} 中%{from} - %{to} 筆 + multiple_without_total: 顯示 %{model} 中%{from} - %{to} 筆 + next: 下一個 + one: 顯示 1 %{model} + one_page: 顯示 全部 %{n} %{model} + per_page: '每頁 ' + previous: 前一個 + truncate: "…" + powered_by: 由 %{active_admin} %{version} 提供 + previous: 前一個 + scopes: + all: 全部 + search_status: + no_current_filters: 未套用篩選條件 + title: 進行中的搜尋 + title_with_scope: 正在搜尋 %{name} + sidebars: + filters: 篩選條件 + search_status: 搜尋狀態 + status_tag: + 'no': 否 + unset: 未知 + 'yes': 是 + toggle_dark_mode: 切換暗黑模式 + toggle_main_navigation_menu: 切換主要導覽 + toggle_section: 切換區塊 + toggle_user_menu: 切換使用者選單 + view: 檢視 + activerecord: + attributes: + active_admin/comment: + author_type: 作者類型 + body: 內容 + created_at: 建立時間 + namespace: 命名空間 + resource_type: 資源類型 + updated_at: 更新時間 + models: + active_admin/comment: + one: 評論 + other: 評論 + comment: + one: 評論 + other: 評論 diff --git a/cucumber.yml b/cucumber.yml index 2b8ee256bd4..00dd8c452c6 100644 --- a/cucumber.yml +++ b/cucumber.yml @@ -1,3 +1,8 @@ -default: --format 'progress' --require features/support/env.rb --require features/step_definitions features --tags ~@requires-reloading --tags ~@wip -wip: --format 'progress' --require features/support/env.rb --require features/step_definitions features --tags @wip:3 --wip features -class-reloading: RAILS_ENV=cucumber_with_reloading --format 'progress' --require features/support/env.rb --require features/step_definitions features --tags @requires-reloading +<% + std_opts = "--format progress --order random --publish-quiet" + default_opts = std_opts + " --format ParallelTests::Gherkin::RuntimeLogger --out tmp/parallel_runtime_cucumber.log" +%> + +default: <%= default_opts %> --require features/support/simplecov_regular_env.rb --tags 'not @changes-filesystem' --tags 'not @requires-reloading' +filesystem-changes: <%= std_opts %> --require features/support/simplecov_changes_env.rb --tags @changes-filesystem +class-reloading: CLASS_RELOADING=true <%= std_opts %> --require features/support/simplecov_reload_env.rb --tags @requires-reloading diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js new file mode 100644 index 00000000000..96be91316af --- /dev/null +++ b/docs/.vitepress/config.js @@ -0,0 +1,138 @@ +import packageInfo from '../../package.json' with { type: 'json' } +import { defineConfig } from 'vitepress' +import { generateLegacyRedirects } from './redirects.js' + +function navigation(prefix, release) { + return [ + { text: 'Guide', link: `${prefix}0-installation` }, + { text: 'Discuss', link: 'https://github.com/activeadmin/activeadmin/discussions' }, + { + text: 'Demo', + items: [ + { text: 'GitHub Repository', link: 'https://github.com/activeadmin/demo.activeadmin.info' }, + { text: 'Demo App', link: 'https://demo.activeadmin.info/' }, + ] + }, + { + text: release, + items: [ + { text: 'Changelog', link: 'https://github.com/activeadmin/activeadmin/releases' }, + { text: 'Contributing', link: 'https://github.com/activeadmin/activeadmin/blob/master/CONTRIBUTING.md' }, + ], + } + ] +} + +function sidebar(prefix, legacy = false) { + return [ + { + text: legacy ? 'v3 Setup' : 'v4 (beta) Setup', + items: [ + { text: 'Installation', link: `${prefix}0-installation` }, + { text: 'Configuration', link: `${prefix}1-general-configuration` }, + { text: 'Upgrading', link: `${prefix}upgrading` }, + ] + }, + { + text: 'Resources', + items: [ + { text: 'Working with Resources', link: `${prefix}2-resource-customization` }, + { text: 'Customize the Index page', link: `${prefix}3-index-pages` }, + { text: 'Index as a Table', link: `${prefix}${legacy ? '3-index-pages/index-as-table' : '3-index-as-table'}` }, + ...(legacy ? [ + { text: 'Index as a Grid', link: `${prefix}3-index-pages/index-as-grid` }, + { text: 'Index as Blocks', link: `${prefix}3-index-pages/index-as-block` }, + { text: 'Index as a Blog', link: `${prefix}3-index-pages/index-as-blog` }, + { text: 'Custom Index View', link: `${prefix}3-index-pages/custom-index` }, + ] : []), + { text: 'CSV Format', link: `${prefix}4-csv-format` }, + { text: 'Forms', link: `${prefix}5-forms` }, + { text: 'Customize the Show Page', link: `${prefix}6-show-pages` }, + { text: 'Sidebar Sections', link: `${prefix}7-sidebars` }, + { text: 'Custom Controller Actions', link: `${prefix}8-custom-actions` }, + { text: 'Batch Actions', link: `${prefix}9-batch-actions` }, + { text: 'Decorators', link: `${prefix}11-decorators` }, + { text: 'Authorization Adapter', link: `${prefix}13-authorization-adapter` }, + ] + }, + { + text: 'Other', + items: [ + { text: 'Custom Pages', link: `${prefix}10-custom-pages` }, + { text: 'Arbre Components', link: `${prefix}12-arbre-components` }, + { text: 'Gotchas', link: `${prefix}14-gotchas` }, + ] + } + ] +} + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + title: 'ActiveAdmin', + description: 'The administration framework for business critical Ruby on Rails applications.', + // Keep the source files in docs/ while publishing them at /docs/. + rewrites: (page) => `docs/${page}`, + buildEnd: generateLegacyRedirects, + locales: { + root: { + label: 'v4 (beta)', + lang: 'en', + link: '/docs/', + themeConfig: { + nav: navigation('/docs/', packageInfo.version.replace('-', '.')), + sidebar: sidebar('/docs/'), + }, + }, + 'docs/v3': { + label: 'v3', + lang: 'en', + link: '/docs/v3/', + title: 'ActiveAdmin v3', + themeConfig: { + nav: navigation('/docs/v3/', 'v3'), + sidebar: sidebar('/docs/v3/', true), + }, + }, + }, + head: [ + ['meta', { property: 'og:type', content: 'website' }], + ['meta', { property: 'og:locale', content: 'en' }], + ['meta', { property: 'og:site_name', content: 'ActiveAdmin' }], + ], + themeConfig: { + langMenuLabel: 'Documentation version', + i18nRouting: (data, route, targetLocale) => { + const destinations = { + 'docs/3-index-as-table.md': `/docs/v3/3-index-pages/index-as-table.md${route.hash}`, + 'docs/v3/3-index-pages/index-as-table.md': `/docs/3-index-as-table.md${route.hash}`, + 'docs/v3/3-index-pages/custom-index.md': '/docs/3-index-pages.md#custom-index', + 'docs/v3/3-index-pages/index-as-block.md': '/docs/3-index-pages.md', + 'docs/v3/3-index-pages/index-as-blog.md': '/docs/3-index-pages.md', + 'docs/v3/3-index-pages/index-as-grid.md': '/docs/3-index-pages.md', + } + const page = route.data.relativePath.replace(/^docs\/(?:v3\/)?/, '') + const [destination, ...anchor] = ( + destinations[route.data.relativePath] ?? `${data.site.value.locales[targetLocale].link}${page}${route.hash}` + ).split('#') + const path = destination + .replace(/(^|\/)index\.md$/, '$1') + .replace(/\.md$/, data.site.value.cleanUrls ? '' : '.html') + return path + route.query + (anchor.length ? '#' + anchor.join('#') : '') + }, + editLink: { + pattern: 'https://github.com/activeadmin/activeadmin/edit/master/docs/:path', + text: 'Edit this page on GitHub' + }, + socialLinks: [ + { icon: 'github', link: 'https://github.com/activeadmin/activeadmin' }, + { icon: 'slack', link: 'https://activeadmin.slack.com/' }, + ], + footer: { + message: 'Released under the MIT License.', + copyright: 'Copyright © 2010-present' + }, + search: { + provider: 'local' + } + } +}) diff --git a/docs/.vitepress/redirects.js b/docs/.vitepress/redirects.js new file mode 100644 index 00000000000..3caf4223586 --- /dev/null +++ b/docs/.vitepress/redirects.js @@ -0,0 +1,31 @@ +import { mkdir, writeFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' + +// GitHub Pages needs real HTML files for the former root-level URLs. +// Generate old root URLs from the v3 archive, including pages removed from v4. +// The five archived index subpages do not depend on docs/3-index-pages/. +export async function generateLegacyRedirects({ outDir, pages, rewrites }) { + for (const page of pages) { + if (!page.startsWith('v3/')) continue + + const target = '/' + rewrites.map[page] + .replace(/(^|\/)index\.md$/, '$1') + .replace(/\.md$/, '.html') + const output = join(outDir, page.slice('v3/'.length).replace(/\.md$/, '.html')) + await mkdir(dirname(output), { recursive: true }) + await writeFile(output, ` + + + + Redirecting to ActiveAdmin documentation + + + + + +

This page has moved to the ActiveAdmin documentation.

+ + +`) + } +} diff --git a/docs/.vitepress/site.test.js b/docs/.vitepress/site.test.js new file mode 100644 index 00000000000..3d9e252b226 --- /dev/null +++ b/docs/.vitepress/site.test.js @@ -0,0 +1,139 @@ +import assert from 'node:assert/strict' +import { existsSync, readFileSync, readdirSync } from 'node:fs' +import { mkdtemp, readFile, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { test } from 'node:test' +import { URL, fileURLToPath } from 'node:url' +import { runInNewContext } from 'node:vm' +import { generateLegacyRedirects } from './redirects.js' + +const dist = fileURLToPath(new URL('./dist/', import.meta.url)) +const read = (path) => readFileSync(join(dist, path), 'utf8') +const docsPages = readdirSync(fileURLToPath(new URL('../', import.meta.url)), { recursive: true }) + .filter((path) => path.endsWith('.md') && !path.startsWith('.') && !['index.md', 'v3/index.md'].includes(path)) + .sort() + +test('every archived v3 page has a legacy redirect that preserves queries and anchors', () => { + const pages = readdirSync(join(dist, 'docs/v3'), { recursive: true }).filter((path) => path.endsWith('.html')) + for (const file of pages) { + const path = file.replace(/\.html$/, '') + const html = read(`${path}.html`) + const destination = path === 'index' ? '/docs/v3/' : `/docs/v3/${path}.html` + assert.ok(html.includes(``)) + assert.ok(html.includes(``)) + assert.ok(html.includes(`href="https://activeadmin.info${destination}"`)) + assert.ok(existsSync(join(dist, destination, path === 'index' ? 'index.html' : ''))) + + let redirected + runInNewContext(html.match(/" + And I press "Filter" + Then I should see current filter "title_cont" equal to "" with label "Title contains" Scenario: Filtering posts with no results Given 3 posts exist @@ -29,15 +42,33 @@ Feature: Index Filtering ActiveAdmin.register Post """ When I am on the index page for posts - Then I should see "Displaying all 3 Posts" + Then I should see "Showing all 3" When I fill in "Title" with "THIS IS NOT AN EXISTING TITLE!!" And I press "Filter" - Then I should not see ".index_table" + Then I should not see ".data-table" And I should not see a sortable table header And I should not see pagination And I should see "No Posts found" + Scenario: Filtering posts with pagination + Given 7 posts with the title "Hello World 3" exist + And 1 post with the title "Hello World 4" exist + And an index configuration of: + """ + ActiveAdmin.register Post do + config.per_page = 2 + end + """ + When I fill in "Title" with "Hello World 3" + And I press "Filter" + + Then I follow "2" + And I should see "Showing 3-4 of 7" + + And I follow "3" + And I should see "Showing 5-6 of 7" + Scenario: Filtering posts while not on the first page Given 9 posts exist And an index configuration of: @@ -47,12 +78,12 @@ Feature: Index Filtering end """ When I follow "2" - Then I should see "Displaying Posts 6 - 9 of 9 in total" + Then I should see "Showing 6-9 of 9" When I fill in "Title" with "Hello World 2" And I press "Filter" - And I should see 1 posts in the table - And I should see "Hello World 2" within ".index_table" + Then I should see 1 posts in the table + And I should see "Hello World 2" in the table Scenario: Checkboxes - Filtering posts written by anyone Given 1 post exists @@ -60,12 +91,12 @@ Feature: Index Filtering And an index configuration of: """ ActiveAdmin.register Post do - filter :author, :as => :check_boxes + filter :author, as: :check_boxes end """ When I press "Filter" Then I should see 2 posts in the table - And I should see "Hello World" within ".index_table" + And I should see "Hello World" in the table And the "Jane Doe" checkbox should not be checked Scenario: Checkboxes - Filtering posts written by Jane Doe @@ -74,14 +105,15 @@ Feature: Index Filtering And an index configuration of: """ ActiveAdmin.register Post do - filter :author, :as => :check_boxes + filter :author, as: :check_boxes end """ When I check "Jane Doe" And I press "Filter" Then I should see 1 posts in the table - And I should see "Hello World" within ".index_table" + And I should see "Hello World" in the table And the "Jane Doe" checkbox should be checked + And I should see current filter "author_id_in" equal to "Jane Doe" Scenario: Disabling filters Given an index configuration of: @@ -90,5 +122,259 @@ Feature: Index Filtering config.filters = false end """ - Then I should not see a sidebar titled "Filters" + Then I should not see "Filters" + + Scenario: Select - Filtering categories with posts written by Jane Doe + Given a category named "Mystery" exists + And 1 post with the title "Hello World" written by "Jane Doe" in category "Non-Fiction" exists + And an index configuration of: + """ + ActiveAdmin.register Category + """ + When I select "Jane Doe" from "Authors" + And I press "Filter" + Then I should see 1 categories in the table + And I should see "Non-Fiction" in the table + And I should not see "Mystery" in the table + And I should see current filter "posts_author_id_eq" equal to "Jane Doe" + + @javascript + Scenario: Clearing filters preserves custom parameters + Given a category named "Mystery" exists + And 1 post with the title "Hello World" written by "Jane Doe" in category "Non-Fiction" exists + And 1 post with the title "Lorem Ipsum" written by "Joe Smith" in category "Mystery" exists + And an index configuration of: + """ + ActiveAdmin.register Category + """ + Then I should see "Showing all 2" + When I add parameter "scope" with value "all" to the URL + And I add parameter "foo" with value "bar" to the URL + And I select "Hello World" from "Posts" + And I press "Filter" + Then I should see "Non-Fiction" + And I should not see "Mystery" + When I click "Clear Filters" + Then I should see "Non-Fiction" + And I should see "Mystery" + And I should have parameter "foo" with value "bar" + And I should have parameter "scope" with value "all" + + Scenario: Checkboxes - Filtering categories via posts written by anyone + Given a category named "Mystery" exists + And a post with the title "Hello World" written by "Jane Doe" in category "Non-Fiction" exists + And an index configuration of: + """ + ActiveAdmin.register Category do + filter :authors, as: :check_boxes + end + """ + When I press "Filter" + Then I should see 2 posts in the table + And I should see "Mystery" in the table + And I should see "Non-Fiction" in the table + And the "Jane Doe" checkbox should not be checked + And I should not see "Active Search" + + Scenario: Checkboxes - Filtering categories via posts written by Jane Doe + Given a category named "Mystery" exists + And a post with the title "Hello World" written by "Jane Doe" in category "Non-Fiction" exists + And an index configuration of: + """ + ActiveAdmin.register Category do + filter :authors, as: :check_boxes + end + + """ + When I check "Jane Doe" + And I press "Filter" + Then I should see 1 categories in the table + And I should see "Non-Fiction" in the table + And the "Jane Doe" checkbox should be checked + + Scenario: Filtering posts without default scope + Given a post with the title "Hello World" written by "Jane Doe" exists + And an index configuration of: + """ + ActiveAdmin.register Post do + scope :all + scope :published do |posts| + posts.where("published_date IS NOT NULL") + end + + filter :title + end + """ + When I fill in "Title" with "Hello" + And I press "Filter" + Then I should see current filter "title_cont" equal to "Hello" with label "Title contains" + + Scenario: Filtering posts by category + Given a category named "Mystery" exists + And a post with the title "Hello World" written by "Jane Doe" in category "Non-Fiction" exists + And an index configuration of: + """ + ActiveAdmin.register Category + ActiveAdmin.register Post do + filter :category + end + """ + And I am on the index page for posts + + When I select "Non-Fiction" from "Category" + And I press "Filter" + Then I should see "Active Search" + And I should see link "Non-Fiction" in current filters + + Scenario: Enabling filters status sidebar + Given an index configuration of: + """ + ActiveAdmin.application.current_filters = false + ActiveAdmin.register Post do + config.current_filters = true + end + """ + And I press "Filter" + Then I should see "Active Search" + + Scenario: Disabling filters status sidebar + Given an index configuration of: + """ + ActiveAdmin.application.current_filters = true + ActiveAdmin.register Post do + config.current_filters = false + end + """ + And I press "Filter" + Then I should not see "Active Search" + + Scenario: Filters and nested resources + Given a post with the title "The arrogant president" written by "Jane Doe" exists + And a configuration of: + """ + ActiveAdmin.register User + ActiveAdmin.register Post do + permit_params :user_id + + belongs_to :author, class_name: "User" + end + """ + And I am logged in + And I am on the index page for users + When I select "The arrogant president" from "Posts" + And I press "Filter" + And I should see 1 user in the table + Scenario: Too many categories to show + Given a category named "Astrology" exists + And a category named "Astronomy" exists + And a category named "Navigation" exists + And a post with the title "Star Signs" in category "Astrology" exists + And a post with the title "Constellations" in category "Astronomy" exists + And a post with the title "Compass and Sextant" in category "Navigation" exists + And an index configuration of: + """ + ActiveAdmin.register Category + ActiveAdmin.register Post do + config.namespace.maximum_association_filter_arity = 3 + end + """ + And I am on the index page for posts + Then I should see "Category" within "#filters_sidebar_section label[for="q_custom_category_id"]" + And I should not see "Category name starts with" within "#filters_sidebar_section" + + Given an index configuration of: + """ + ActiveAdmin.register Category + ActiveAdmin.register Post do + config.namespace.maximum_association_filter_arity = 2 + end + """ + And I am on the index page for posts + Then I should see "Category name start" within "#filters_sidebar_section" + When I fill in "Category name start" with "Astro" + And I press "Filter" + Then I should see "Star Signs" + And I should see "Constellations" + And I should not see "Compass and Sextant" + + Given an index configuration of: + """ + ActiveAdmin.register Category + ActiveAdmin.register Post do + config.namespace.maximum_association_filter_arity = 2 + config.namespace.filter_method_for_large_association = '_cont' + end + """ + And I am on the index page for posts + Then I should see "Category name cont" within "#filters_sidebar_section" + When I fill in "Category name cont" with "Astro" + And I press "Filter" + Then I should see "Star Signs" + And I should see "Constellations" + And I should not see "Compass and Sextant" + + Given an index configuration of: + """ + ActiveAdmin.register Category + ActiveAdmin.register Post do + config.namespace.maximum_association_filter_arity = :unlimited + config.namespace.filter_method_for_large_association = '_cont' + end + """ + And I am on the index page for posts + Then I should see "Category" within "#filters_sidebar_section" + When I select "Astronomy" from "Category" + And I press "Filter" + Then I should not see "Star Signs" + And I should see "Constellations" + And I should not see "Compass and Sextant" + + Given an index configuration of: + """ + ActiveAdmin.register Category + ActiveAdmin.register Post do + config.namespace.maximum_association_filter_arity = :unlimited + end + """ + And I am on the index page for posts + Then I should see "Category" within "#filters_sidebar_section label[for="q_custom_category_id"]" + And I should not see "Category name starts with" within "#filters_sidebar_section" + + Scenario: Custom ransackable scopes filters + Given an index configuration of: + """ + ActiveAdmin.register Post do + filter :fancy_filter, label: "Ransackable Custom Filter", as: :select, collection: ["Starred", "Not Starred"] + end + """ + And 1 unstarred post with the title "Hello World" written by "Jane Doe" exists + And 1 starred post with the title "Starred World" written by "Jane Doe" exists + When I select "Starred" from "Ransackable Custom Filter" + And I press "Filter" + Then I should see current filter "fancy_filter" equal to "Starred" with label "Ransackable Custom Filter" + And I should see "Starred World" + And I should not see "Hello World" + When I select "Not Starred" from "Ransackable Custom Filter" + And I press "Filter" + Then I should see current filter "fancy_filter" equal to "Not Starred" with label "Ransackable Custom Filter" + And I should see "Hello World" + And I should not see "Starred World" + + Scenario: "counter cache"-like filters + Given a user named "Jane Doe" exists + And an index configuration of: + """ + ActiveAdmin.register User + """ + When I am on the index page for users + Then I should see "Showing 1 of 1" + And I should see the following filters: + | Sign in count | number | + + When I fill in "Sign in count" with "0" + And I press "Filter" + Then I should see 1 users in the table + When I fill in "Sign in count" with "1" + And I press "Filter" + Then I should see "No Users found" diff --git a/features/index/format_as_csv.feature b/features/index/format_as_csv.feature index e9a084b9fbc..a49f5c5342d 100644 --- a/features/index/format_as_csv.feature +++ b/features/index/format_as_csv.feature @@ -1,3 +1,4 @@ +@csv Feature: Format as CSV Background: @@ -11,20 +12,34 @@ Feature: Format as CSV And a post with the title "Hello World" exists When I am on the index page for posts And I follow "CSV" - And I should download a CSV file for "posts" containing: - | Id | Title | Body | Published At | Starred | Created At | Updated At | - | \d+ | Hello World | | | | (.*) | (.*) | + Then I should download a CSV file for "posts" containing: + | Id | Title | Body | Published date | Position | Starred | Foo |Created at | Updated at | + | \d+ | Hello World | | | | | |(.*) | (.*) | Scenario: Default with alias Given a configuration of: """ - ActiveAdmin.register Post, :as => "MyArticle" + ActiveAdmin.register Post, as: "MyArticle" """ And 1 post exists When I am on the index page for my_articles And I follow "CSV" - And I should download a CSV file for "my-articles" containing: - | Id | Title | Body | Published At | Starred | Created At | Updated At | + Then I should download a CSV file for "my-articles" containing: + | Id | Title | Body | Published date | Position | Starred | Foo | Created at | Updated at | + + Scenario: Default with streaming disabled + Given a configuration of: + """ + ActiveAdmin.application.disable_streaming_in = ["test"] + + ActiveAdmin.register Post + """ + And a post with the title "Hello World" exists + When I am on the index page for posts + And I follow "CSV" + Then I should download a CSV file for "posts" containing: + | Id | Title | Body | Published date | Position | Starred | Foo |Created at | Updated at | + | \d+ | Hello World | | | | | |(.*) | (.*) | Scenario: With CSV format customization Given a configuration of: @@ -40,7 +55,7 @@ Feature: Format as CSV And a post with the title "Hello, World" exists When I am on the index page for posts And I follow "CSV" - And I should download a CSV file for "posts" containing: + Then I should download a CSV file for "posts" containing: | Title | Last update | Copyright | | Hello, World | (.*) | Greg Bell | @@ -48,7 +63,7 @@ Feature: Format as CSV Given a configuration of: """ ActiveAdmin.register Post do - csv :col_sep => ';' do + csv col_sep: ';' do column :title column :body end @@ -57,15 +72,64 @@ Feature: Format as CSV And a post with the title "Hello, World" exists When I am on the index page for posts And I follow "CSV" - And I should download a CSV file with ";" separator for "posts" containing: + Then I should download a CSV file with ";" separator for "posts" containing: | Title | Body | | Hello, World | (.*) | + Scenario: With humanize_name option + Given a configuration of: + """ + ActiveAdmin.register Post do + csv humanize_name: false do + column :title + column :body + end + end + """ + And a post with the title "Hello, World" exists + When I am on the index page for posts + And I follow "CSV" + And I should download a CSV file with "," separator for "posts" containing: + | title | body | + | Hello, World | (.*) | + + Scenario: With humanize_name option turned off globally + Given a configuration of: + """ + ActiveAdmin.application.csv_options = { humanize_name: false } + ActiveAdmin.register Post do + end + """ + And a post exists + When I am on the index page for posts + And I follow "CSV" + Then I should download a CSV file with "," separator for "posts" containing: + | id | title | body | published_date | position | starred | foo | created_at | updated_at | + | (.*)| (.*) | (.*) | (.*) | (.*) | (.*) | (.*) | (.*) | (.*) | + + Scenario: With humanize_name option turned off globally and enabled locally + Given a configuration of: + """ + ActiveAdmin.application.csv_options = { humanize_name: false } + ActiveAdmin.register Post do + csv humanize_name: true do + column :title + column :body + end + end + """ + And a post exists + When I am on the index page for posts + And I follow "CSV" + Then I should download a CSV file with "," separator for "posts" containing: + | Title | Body | + | (.*) | (.*) | + Scenario: With CSV option customization Given a configuration of: """ ActiveAdmin.register Post do - csv :force_quotes => true do + csv force_quotes: true, byte_order_mark: "" do column :title column :body end @@ -79,10 +143,22 @@ Feature: Format as CSV | 012345 | (.*) | And the CSV file should contain "012345" in quotes + Scenario: With CSV option byte order mark + Given a configuration of: + """ + ActiveAdmin.register Post do + csv byte_order_mark: "\xEF\xBB\xBF" do + column :title + end + end + """ + When I visit the csv index page for posts twice + Then the CSV file should start with BOM + Scenario: With default CSV separator option Given a configuration of: """ - ActiveAdmin.application.csv_options = { :col_sep => ';' } + ActiveAdmin.application.csv_options = { col_sep: ';' } ActiveAdmin.register Post do csv do column :title @@ -100,7 +176,7 @@ Feature: Format as CSV Scenario: With default CSV options Given a configuration of: """ - ActiveAdmin.application.csv_options = {:col_sep => ',', :force_quotes => true} + ActiveAdmin.application.csv_options = {col_sep: ',', force_quotes: true} ActiveAdmin.register Post do csv do column :title @@ -115,3 +191,95 @@ Feature: Format as CSV | Title | Body | | 012345 | (.*) | And the CSV file should contain "012345" in quotes + + Scenario: Without CSV column names explicitly specified + Given a configuration of: + """ + ActiveAdmin.application.csv_options = {col_sep: ',', force_quotes: true} + ActiveAdmin.register Post do + csv column_names: true do + column :title + column :body + end + end + """ + And a post with the title "012345" exists + When I am on the index page for posts + And I follow "CSV" + And I should download a CSV file with "," separator for "posts" containing: + | Title | Body | + | 012345 | (.*) | + + Scenario: Without CSV column names + Given a configuration of: + """ + ActiveAdmin.application.csv_options = {col_sep: ',', force_quotes: true} + ActiveAdmin.register Post do + csv column_names: false do + column :title + column :body + end + end + """ + And a post with the title "012345" exists + When I am on the index page for posts + And I follow "CSV" + Then I should download a CSV file with "," separator for "posts" containing: + | 012345 | (.*) | + + Scenario: With encoding CSV options + Given a configuration of: + """ + # Currently manually setting a non-UTF8 encoding crashes in combination + # with default csv options. It crashes with a cryptic incompatible + # encoding error, because the BOM is set in UTF-8 encoding by default. + # We should probably fix that, but for now we just set empty csv options + # for this scenario. + ActiveAdmin.application.csv_options = {} + ActiveAdmin.register Post do + csv encoding: 'SJIS' do + column :title + column :body + end + end + """ + And a post with the title "あいうえお" exists + When I am on the index page for posts + And I follow "CSV" + Then the encoding of the CSV file should be "SJIS" + + Scenario: With default encoding CSV options + Given a configuration of: + """ + ActiveAdmin.application.csv_options = { encoding: 'SJIS' } + ActiveAdmin.register Post do + csv do + column :title + column :body + end + end + """ + And a post with the title "あいうえお" exists + When I am on the index page for posts + And I follow "CSV" + Then the encoding of the CSV file should be "SJIS" + + Scenario: With decorator + Given a configuration of: + """ + ActiveAdmin.register Post do + decorate_with PostDecorator + + csv do + column :id + column :title + column :decorator_method + end + end + """ + And a post with the title "Hello World" exists + When I am on the index page for posts + And I follow "CSV" + Then I should download a CSV file for "posts" containing: + | Id | Title | Decorator method | + | \d+ | Hello World | A method only available on the decorator | diff --git a/features/index/formats.feature b/features/index/formats.feature index 8ef2f364259..b48c7b6c5ce 100644 --- a/features/index/formats.feature +++ b/features/index/formats.feature @@ -15,7 +15,7 @@ Feature: Index Formats Given an index configuration of: """ ActiveAdmin.register Post do - index :download_links => false + index download_links: false end """ And 1 post exists @@ -28,7 +28,7 @@ Feature: Index Formats Given an index configuration of: """ ActiveAdmin.register Post do - index :download_links => [:pdf] + index download_links: [:pdf] end """ And 1 post exists @@ -37,3 +37,52 @@ Feature: Index Formats And I should not see a link to download "XML" And I should not see a link to download "JSON" And I should see a link to download "PDF" + + Scenario: View index with download_links block which returns false + Given an index configuration of: + """ + ActiveAdmin.register Post do + index download_links: proc { false } + end + """ + And 1 post exists + When I am on the index page for posts + Then I should not see a link to download "CSV" + And I should not see a link to download "XML" + And I should not see a link to download "JSON" + + When I go to the csv index page for posts + Then access denied + + When I go to the xml index page for posts + Then access denied + + When I go to the json index page for posts + Then access denied + + Scenario: View index with download_links block which returns [:csv] + Given an index configuration of: + """ + ActiveAdmin.register Post do + index download_links: -> { [:csv] } + end + """ + And 1 post exists + When I am on the index page for posts + Then I should see a link to download "CSV" + And I should not see a link to download "XML" + And I should not see a link to download "JSON" + And I should not see a link to download "PDF" + + Scenario: View index with restricted formats + Given an index configuration of: + """ + ActiveAdmin.register Post do + index download_links: -> { [:json] } + end + """ + When I go to the csv index page for posts + Then access denied + + When I go to the xml index page for posts + Then access denied diff --git a/features/index/index_as_block.feature b/features/index/index_as_block.feature deleted file mode 100644 index ce06ac46e88..00000000000 --- a/features/index/index_as_block.feature +++ /dev/null @@ -1,15 +0,0 @@ -Feature: Index as Block - - Viewing the resource as a block which is renderered by the user - - Scenario: Viewing the index as a block - Given a post with the title "Hello World from Block" exists - And an index configuration of: - """ - ActiveAdmin.register Post do - index :as => :block do |post| - span(link_to(post.title, admin_post_path(post))) - end - end - """ - Then I should see "Hello World from Block" within ".index_as_block" diff --git a/features/index/index_as_blog.feature b/features/index/index_as_blog.feature deleted file mode 100644 index e8ecd7de45f..00000000000 --- a/features/index/index_as_blog.feature +++ /dev/null @@ -1,69 +0,0 @@ -Feature: Index as Blog - - Viewing resources as a blog on the index page - - Scenario: Viewing the blog with a resource - Given a post with the title "Hello World" exists - And an index configuration of: - """ - ActiveAdmin.register Post do - index :as => :blog - end - """ - And I am logged in - When I am on the index page for posts - Then I should see "Hello World" within "h3" - And I should see a link to "Hello World" - - Scenario: Viewing the blog with a resource as a simple configuration - Given a post with the title "Hello World" and body "My great post body" exists - And an index configuration of: - """ - ActiveAdmin.register Post do - index :as => :blog do - title :title - body :body - end - end - """ - Then I should see "Hello World" within "h3" - And I should see a link to "Hello World" - And I should see "My great post body" within ".post" - - Scenario: Viewing the blog with a resource as a block configuration - Given a post with the title "Hello World" and body "My great post body" exists - And an index configuration of: - """ - ActiveAdmin.register Post do - index :as => :blog do - title do |post| - post.title + " From Block" - end - body do |post| - post.body + " From Block" - end - end - end - """ - Then I should see "Hello World From Block" within "h3" - And I should see a link to "Hello World From Block" - And I should see "My great post body From Block" within ".post" - - Scenario: Viewing a blog with a resource as a block configuration should render Abre components - Given a post with the title "Hello World" and body "My great post body" exists - And an index configuration of: - """ - ActiveAdmin.register Post do - index :as => :blog do - title do |post| - span(:class => :title_span) { post.title + " From Block " } - end - body do |post| - span(:class => :body_span) { post.body + " From Block" } - end - end - end - """ - Then I should see "Hello World From Block" within "span.title_span" - And I should see a link to "Hello World From Block" - And I should see "My great post body From Block" within ".post span.body_span" diff --git a/features/index/index_as_grid.feature b/features/index/index_as_grid.feature deleted file mode 100644 index 5a65418133e..00000000000 --- a/features/index/index_as_grid.feature +++ /dev/null @@ -1,45 +0,0 @@ -Feature: Index as Grid - - Viewing resources as a grid on the index page - - Scenario: Viewing index as a grid with a simple block configuration - Given 9 posts exist - And an index configuration of: - """ - ActiveAdmin.register Post do - index :as => :grid do |post| - h2 auto_link(post) - end - end - """ - Then the table ".index_grid" should have 3 rows - And the table ".index_grid" should have 3 columns - And there should be 9 "a" tags within index grid - - Scenario: Viewing index as a grid and set the number of columns - Given 9 posts exist - And an index configuration of: - """ - ActiveAdmin.register Post do - index :as => :grid, :columns => 1 do |post| - h2 auto_link(post) - end - end - """ - Then the table ".index_grid" should have 9 rows - And the table ".index_grid" should have 1 columns - And there should be 9 "a" tags within "table.index_grid" - - Scenario: Viewing index as a grid with an odd number of items - Given 9 posts exist - And an index configuration of: - """ - ActiveAdmin.register Post do - index :as => :grid, :columns => 2 do |post| - h2 auto_link(post) - end - end - """ - Then the table ".index_grid" should have 5 rows - And the table ".index_grid" should have 2 columns - And there should be 9 "a" tags within "table.index_grid" diff --git a/features/index/index_as_table.feature b/features/index/index_as_table.feature index ccf40157687..2b9cb7909fe 100644 --- a/features/index/index_as_table.feature +++ b/features/index/index_as_table.feature @@ -20,11 +20,32 @@ Feature: Index as Table ActiveAdmin.register Post """ Then I should see "Hello World" - Then I should see nicely formatted datetimes + And I should see nicely formatted datetimes And I should see a link to "View" And I should see a link to "Edit" And I should see a link to "Delete" + Scenario: Viewing the default table with no show action + Given a post with the title "Hello World" exists + And an index configuration of: + """ + ActiveAdmin.register Post do + actions :index, :edit, :update + end + """ + Then I should see "Hello World" + And I should see an id_column link to edit page + And I should see a link to "Edit" + + Scenario: Viewing the default table with "counter-cache"-like columns + Given a user named "Jane Doe" exists + And an index configuration of: + """ + ActiveAdmin.register User + """ + When I am on the index page for users + Then I should see a sortable table header with "Sign In Count" + Scenario: Customizing the columns with symbols Given a post with the title "Hello World" and body "From the body" exists And an index configuration of: @@ -93,6 +114,19 @@ Feature: Index as Table And I should see a member link to "Edit" And I should not see a member link to "Delete" + Scenario: Actions with custom column options + Given a post with the title "Hello World" and body "From the body" exists + And an index configuration of: + """ + ActiveAdmin.register Post do + index do + column :category + actions class: 'custom-column-class', name: 'That text' + end + end + """ + Then I should see the actions column with the class "custom-column-class" and the title "That text" + Scenario: Actions with defaults and custom actions Given a post with the title "Hello World" and body "From the body" exists And an index configuration of: @@ -103,7 +137,7 @@ Feature: Index as Table index do column :category actions do |resource| - link_to 'Custom Action', edit_admin_post_path(resource), :class => 'member_link' + item 'Custom Action', edit_admin_post_path(resource) end end end @@ -122,8 +156,8 @@ Feature: Index as Table index do column :category - actions :defaults => false do |resource| - link_to 'Custom Action', edit_admin_post_path(resource), :class => 'member_link' + actions defaults: false do |resource| + item 'Custom Action', edit_admin_post_path(resource) end end end @@ -133,6 +167,19 @@ Feature: Index as Table And I should not see a member link to "Delete" And I should see a member link to "Custom Action" + Scenario: Index page without show action + Given a post with the title "Hello World" and body "From the body" exists + And an index configuration of: + """ + ActiveAdmin.register Post do + actions :index + end + """ + Then I should not see a member link to "View" + And I should not see a member link to "Edit" + And I should not see a member link to "Delete" + And I should see "Hello World" + Scenario: Associations are not sortable Given 1 post exists And an index configuration of: @@ -145,6 +192,36 @@ Feature: Index as Table """ Then I should not see a sortable table header with "Category" + Scenario: Columns with block are sortable by default + Given 1 post exists + And an index configuration of: + """ + ActiveAdmin.register Post do + index do + column :author_id do end + column 'published_date' do end + column :category do end + end + end + """ + Then I should see a sortable table header with "Author" + And I should see a sortable table header with "published_date" + And I should not see a sortable table header with "Category" + + Scenario: Columns with block are not sortable by when sortable option equals to false + Given 1 post exists + And an index configuration of: + """ + ActiveAdmin.register Post do + index do + column :author_id, sortable: false do end + column 'published_date', sortable: false do end + end + end + """ + Then I should not see a sortable table header with "Author" + And I should not see a sortable table header with "published_date" + Scenario: Sorting Given a post with the title "Hello World" and body "From the body" exists And a post with the title "Bye bye world" and body "Move your..." exists @@ -154,14 +231,14 @@ Feature: Index as Table """ When I am on the index page for posts Then I should see the "index_table_posts" table: - | [ ] | Id | Title | Body | Published At | Starred | Created At | Updated At | | - | [ ] | 2 | Bye bye world | Move your... | | | /.*/ | /.*/ | ViewEditDelete | - | [ ] | 1 | Hello World | From the body | | | /.*/ | /.*/ | ViewEditDelete | + | [ ] | Id | Title | Body | Published Date | Author | Position | Category | Starred | Foo | Created At | Updated At | | + | [ ] | 2 | Bye bye world | Move your... | | | | | Unknown | | /.*/ | /.*/ | View Edit Delete | + | [ ] | 1 | Hello World | From the body | | | | | Unknown | | /.*/ | /.*/ | View Edit Delete | When I follow "Id" Then I should see the "index_table_posts" table: - | [ ] | Id | Title | Body | Published At | Starred | Created At | Updated At | | - | [ ] | 1 | Hello World | From the body | | | /.*/ | /.*/ | ViewEditDelete | - | [ ] | 2 | Bye bye world | Move your... | | | /.*/ | /.*/ | ViewEditDelete | + | [ ] | Id | Title | Body | Published Date | Author | Position | Category | Starred | Foo | Created At | Updated At | | + | [ ] | 1 | Hello World | From the body | | | | | Unknown | | /.*/ | /.*/ | View Edit Delete | + | [ ] | 2 | Bye bye world | Move your... | | | | | Unknown | | /.*/ | /.*/ | View Edit Delete | Scenario: Sorting by a virtual column Given a post with the title "Hello World" exists @@ -178,7 +255,7 @@ Feature: Index as Table index do column :id column :title - column("Title Length", :sortable => :title_length) { |post| post.title_length } + column("Title Length", sortable: :title_length) { |post| post.title_length } end end """ diff --git a/features/index/index_blank_slate.feature b/features/index/index_blank_slate.feature index 6063650f437..237e0a7afa8 100644 --- a/features/index/index_blank_slate.feature +++ b/features/index/index_blank_slate.feature @@ -9,12 +9,13 @@ Feature: Index Blank Slate batch_action :favourite do # nothing end - scope :all, :default => true + scope :all, default: true end """ Then I should not see a sortable table header - And I should see "There are no Posts yet. Create one" - And I should not see ".index_table" + And I should see "There are no Posts yet." + And I should see "Create one" + And I should not see ".data-table" And I should not see pagination When I follow "Create one" Then I should be on the new post page @@ -29,33 +30,24 @@ Feature: Index Blank Slate And I should see "There are no Posts yet." And I should not see "Create one" - Scenario: Viewing a index using a grid with no resources + Scenario: Customizing the default table with no resources Given an index configuration of: """ - ActiveAdmin.register Post do - index :as => :grid do |post| - h2 auto_link(post) + ActiveAdmin.register Post do + index blank_slate_link: ->{link_to("Go to dashboard", admin_root_path)} do |post| + end end - end """ - And I should see "There are no Posts yet. Create one" + When I follow "Go to dashboard" + Then I should see "Dashboard" - Scenario: Viewing a index using blocks with no resources + Scenario: Customizing the default table with no blank slate link Given an index configuration of: """ - ActiveAdmin.register Post do - index :as => :block do |post| - span(link_to(post.title, admin_post_path(post))) + ActiveAdmin.register Post do + index blank_slate_link: false do |post| + end end - end """ - And I should see "There are no Posts yet. Create one" - - Scenario: Viewing a blog with no resources - Given an index configuration of: - """ - ActiveAdmin.register Post do - index :as => :blog - end - """ - And I should see "There are no Posts yet. Create one" + Then I should see "There are no Posts yet." + And I should not see "Create one" diff --git a/features/index/index_parameters.feature b/features/index/index_parameters.feature index 380bcabfae4..d57dfa19a44 100644 --- a/features/index/index_parameters.feature +++ b/features/index/index_parameters.feature @@ -4,10 +4,10 @@ Feature: Index Parameters Given an index configuration of: """ ActiveAdmin.register Post do - index :as => :table, :download_links => false + index as: :table, download_links: false end """ - Given 31 posts exist + And 31 posts exist When I am on the index page for posts Then I should not see a link to download "CSV" @@ -16,16 +16,16 @@ Feature: Index Parameters """ ActiveAdmin.application.download_links = false """ - Given an index configuration of: + And an index configuration of: """ ActiveAdmin.register Post do - index :as => :table + index as: :table end """ - Given 1 posts exist + And 1 posts exist When I am on the index page for posts Then I should be on the index page for posts - Then I should not see a link to download "CSV" + And I should not see a link to download "CSV" Given a configuration of: """ ActiveAdmin.application.download_links = true @@ -34,48 +34,42 @@ Feature: Index Parameters Scenario: Viewing index when download_links disabled only in one namespace Given a configuration of: """ - ActiveAdmin.setup do |config| - config.namespace :superadmin do |namespace| - namespace.download_links = false - end - end + ActiveAdmin.application.namespace(:superadmin).download_links = false + ActiveAdmin.register AdminUser, namespace: :superadmin """ - Given an index configuration of: + And an index configuration of: """ ActiveAdmin.register Post do - index :as => :table + index as: :table end - ActiveAdmin.register Post, :namespace => :superadmin do - index :as => :table + ActiveAdmin.register Post, namespace: :superadmin do + index as: :table end """ - Given 1 posts exist + And 1 posts exist When I am on the index page for posts in the superadmin namespace Then I should be on the index page for posts in the superadmin namespace - Then I should not see a link to download "CSV" + And I should not see a link to download "CSV" When I am on the index page for posts Then I should be on the index page for posts - Then I should see a link to download "CSV" + And I should see a link to download "CSV" Scenario: Viewing index when download_links enabled only for a resource Given a configuration of: """ - ActiveAdmin.setup do |config| - config.namespace :superadmin do |namespace| - namespace.download_links = false - end - end + ActiveAdmin.application.namespace(:superadmin).download_links = false + ActiveAdmin.register AdminUser, namespace: :superadmin """ - Given an index configuration of: + And an index configuration of: """ ActiveAdmin.register Post do - index :as => :table + index as: :table end - ActiveAdmin.register Post, :namespace => :superadmin do - index :as => :table, :download_links => true + ActiveAdmin.register Post, namespace: :superadmin do + index as: :table, download_links: true end """ - Given 1 posts exist + And 1 posts exist When I am on the index page for posts in the superadmin namespace Then I should be on the index page for posts in the superadmin namespace - Then I should see a link to download "CSV" + And I should see a link to download "CSV" diff --git a/features/index/index_scope_to.feature b/features/index/index_scope_to.feature index 23ad20cb15e..3a9c8e1a854 100644 --- a/features/index/index_scope_to.feature +++ b/features/index/index_scope_to.feature @@ -3,27 +3,49 @@ Feature: Index Scope To Viewing resource configs scoped to another object Background: - Given 10 posts exist - And a post with the title "Hello World" written by "John Doe" exists - And a published post with the title "Hello World" written by "John Doe" exists - Given an index configuration of: - """ - ActiveAdmin.register Post do - # Scope section to a specific author - scope_to do - User.find_by_first_name_and_last_name("John", "Doe") - end - - # Setup some scopes - scope :all, :default => true + Given 10 posts exist + And a post with the title "Hello World" written by "John Doe" exists + And a published post with the title "Hello World" written by "John Doe" exists + + Scenario: Viewing the default scope counts + Given an index configuration of: + """ + ActiveAdmin.register Post do + # Scope section to a specific author + scope_to do + User.find_by_first_name_and_last_name "John", "Doe" + end + + # Set up some scopes + scope :all, default: true scope :published do |posts| - posts.where("published_at IS NOT NULL") + posts.where "published_date IS NOT NULL" end - end - """ + end + """ + When I am on the index page for posts + Then I should see the scope "All" selected + And I should see the scope "All" with the count 2 + And I should see 2 posts in the table - Scenario: Viewing the default scope counts - When I am on the index page for posts + When I follow "Published" + Then I should see 1 posts in the table + + Scenario: Viewing the default scope counts when using proc + Given an index configuration of: + """ + ActiveAdmin.register Post do + # Scope section to a specific author + scope_to ->{ User.find_by_first_name_and_last_name "John", "Doe" } + + # Set up some scopes + scope :all, default: true + scope :published do |posts| + posts.where "published_date IS NOT NULL" + end + end + """ + When I am on the index page for posts Then I should see the scope "All" selected And I should see the scope "All" with the count 2 And I should see 2 posts in the table @@ -35,7 +57,7 @@ Feature: Index Scope To Given an index configuration of: """ ActiveAdmin.register Post do - scope_to :if => proc{ false } do + scope_to if: proc{ false } do User.find_by_first_name_and_last_name("John", "Doe") end end @@ -47,7 +69,7 @@ Feature: Index Scope To Given an index configuration of: """ ActiveAdmin.register Post do - scope_to :unless => proc{ true } do + scope_to unless: proc{ true } do User.find_by_first_name_and_last_name("John", "Doe") end end diff --git a/features/index/index_scopes.feature b/features/index/index_scopes.feature index b3af40c1f36..faf448ebf4f 100644 --- a/features/index/index_scopes.feature +++ b/features/index/index_scopes.feature @@ -14,12 +14,34 @@ Feature: Index Scoping And I should see the scope "All" with the count 3 And I should see 3 posts in the table + Scenario: Viewing resources with one scope with dynamic name + Given 3 posts exist + And an index configuration of: + """ + ActiveAdmin.register Post do + scope -> { scope_title }, :all + + controller do + def scope_title + 'Neat scope' + end + + helper_method :scope_title + end + end + """ + Then I should see the scope with label "Neat scope" + And I should see 3 posts in the table + When I follow "Neat scope" + And I should see 3 posts in the table + And I should see the content "Active Search for Neat scope" + Scenario: Viewing resources with one scope as the default Given 3 posts exist And an index configuration of: """ ActiveAdmin.register Post do - scope :all, :default => true + scope :all, default: true end """ Then I should see the scope "All" selected @@ -31,7 +53,7 @@ Feature: Index Scoping And an index configuration of: """ ActiveAdmin.register Post do - scope :all, :default => proc{ false } + scope :all, default: proc{ false } end """ Then I should see the scope "All" not selected @@ -43,7 +65,7 @@ Feature: Index Scoping And an index configuration of: """ ActiveAdmin.register Post do - scope :all, :default => true + scope :all, default: true filter :title end """ @@ -56,21 +78,20 @@ Feature: Index Scoping And an index configuration of: """ ActiveAdmin.register Post do - scope :all, :default => true - index :as => :table, :scope_count => false + scope :all, default: true + index as: :table, scope_count: false end """ Then I should see the scope "All" selected And I should see the scope "All" with no count And I should see 3 posts in the table - @scope Scenario: Viewing resources with a scope and scope count turned off for a single scope Given 3 posts exist And an index configuration of: """ ActiveAdmin.register Post do - scope :all, :default => true, :show_count => false + scope :all, default: true, show_count: false end """ Then I should see the scope "All" selected @@ -83,9 +104,9 @@ Feature: Index Scoping And an index configuration of: """ ActiveAdmin.register Post do - scope :all, :default => true + scope :all, default: true scope :published do |posts| - posts.where("published_at IS NOT NULL") + posts.where("published_date IS NOT NULL") end end """ @@ -94,21 +115,22 @@ Feature: Index Scoping And I should see the scope "Published" with the count 3 When I follow "Published" Then I should see the scope "Published" selected + And I should see the content "Active Search for Published" And I should see 3 posts in the table Scenario: Viewing resources when scoping and filtering Given 2 posts written by "Daft Punk" exist - Given 1 published posts written by "Daft Punk" exist + And 1 published posts written by "Daft Punk" exist - Given 1 posts written by "Alfred" exist - Given 2 published posts written by "Alfred" exist + And 1 posts written by "Alfred" exist + And 2 published posts written by "Alfred" exist And an index configuration of: """ ActiveAdmin.register Post do - scope :all, :default => true + scope :all, default: true scope :published do |posts| - posts.where("published_at IS NOT NULL") + posts.where("published_date IS NOT NULL") end end """ @@ -118,6 +140,7 @@ Feature: Index Scoping When I follow "Published" Then I should see the scope "Published" selected + And I should see the content "Active Search for Published" And I should see the scope "All" with the count 6 And I should see the scope "Published" with the count 3 And I should see 3 posts in the table @@ -135,30 +158,58 @@ Feature: Index Scoping And an index configuration of: """ ActiveAdmin.register Post do - scope :all, :if => proc { false } - scope "Shown", :if => proc { true } do |posts| + scope :all, if: proc { false } + scope "Shown", if: proc { true } do |posts| + posts + end + scope "Shown with lambda", if: -> { true } do |posts| + posts + end + scope "Shown with method name", if: :neat_scope? do |posts| posts end - scope "Default", :default => true do |posts| + scope "Default", default: true do |posts| posts end - scope 'Today', :if => proc { false } do |posts| + scope 'Today', if: proc { false } do |posts| posts.where(["created_at > ? AND created_at < ?", ::Time.zone.now.beginning_of_day, ::Time.zone.now.end_of_day]) end + + controller do + def neat_scope? + true + end + + helper_method :neat_scope? + end end """ Then I should see the scope "Default" selected And I should not see the scope "All" And I should not see the scope "Today" And I should see the scope "Shown" + And I should see the scope "Shown with lambda" + And I should see the scope "Shown with method name" And I should see the scope "Default" with the count 3 + Scenario: Viewing resources with only optional scopes and their conditions are false + Given 3 posts exist + And an index configuration of: + """ + ActiveAdmin.register Post do + scope :all, if: proc { false } + scope(:hidden, if: proc { false }) { |posts| posts } + scope(:hidden_as_well, if: proc { false }) { |posts| posts } + end + """ + Then I should not see any scopes + Scenario: Viewing resources with multiple scopes as blocks Given 3 posts exist And an index configuration of: """ ActiveAdmin.register Post do - scope 'Today', :default => true do |posts| + scope 'Today', default: true do |posts| posts.where(["created_at > ? AND created_at < ?", ::Time.zone.now.beginning_of_day, ::Time.zone.now.end_of_day]) end scope 'Tomorrow' do |posts| @@ -177,6 +228,7 @@ Feature: Index Scoping Then I should see the scope "Tomorrow" selected And I should see the scope "Today" not selected And I should see a link to "Today" + And I should see the content "Active Search for Tomorrow" Scenario: Viewing resources with scopes when scoping to user Given 2 posts written by "Daft Punk" exist @@ -186,7 +238,7 @@ Feature: Index Scoping """ ActiveAdmin.register Post do scope_to :current_user - scope :all, :default => true + scope :all, default: true filter :title @@ -206,16 +258,19 @@ Feature: Index Scoping Scenario: Viewing resources when scoping and filtering and group bys and stuff Given 2 posts written by "Daft Punk" exist - Given 1 published posts written by "Daft Punk" exist + And 1 published posts written by "Daft Punk" exist - Given 1 posts written by "Alfred" exist + And 1 posts written by "Alfred" exist And an index configuration of: """ ActiveAdmin.register Post do - scope :all, :default => true + scope :all, default: true scope :published do |posts| - posts.where("published_at IS NOT NULL") + posts.where("published_date IS NOT NULL") + end + scope :single do |posts| + posts.page(1).per(1) end index do @@ -234,8 +289,12 @@ Feature: Index Scoping """ Then I should see the scope "All" with the count 2 And I should see the scope "Published" with the count 1 + And I should see the scope "Single" with the count 1 And I should see 2 posts in the table + When I follow "Single" + Then I should see 1 posts in the table + When I follow "Published" Then I should see the scope "Published" selected And I should see the scope "All" with the count 2 @@ -249,3 +308,28 @@ Feature: Index Scoping And I should see the scope "All" with the count 1 And I should see the scope "Published" with the count 1 And I should see 1 posts in the table + And I should see the content "Active Search for Published" + + Scenario: Viewing resources with grouped scopes + Given 3 posts exist + And an index configuration of: + """ + ActiveAdmin.register Post do + scope :all + scope "Published", group: :status do |posts| + posts.where("published_date IS NOT NULL") + end + scope "Unpublished", group: :status do |posts| + posts.where("published_date IS NULL") + end + scope "Today", group: :date do |posts| + posts.where(["created_at > ? AND created_at < ?", ::Time.zone.now.beginning_of_day, ::Time.zone.now.end_of_day]) + end + scope "Tomorrow", group: :date do |posts| + posts.where(["created_at > ? AND created_at < ?", ::Time.zone.now.beginning_of_day + 1.day, ::Time.zone.now.end_of_day + 1.day]) + end + end + """ + Then I should see a default group with a single scope "All" + And I should see a group "status" with the scopes "Published" and "Unpublished" + And I should see a group "date" with the scopes "Today" and "Tomorrow" diff --git a/features/index/non_activerecord_filters.feature b/features/index/non_activerecord_filters.feature new file mode 100644 index 00000000000..2bc9e572a56 --- /dev/null +++ b/features/index/non_activerecord_filters.feature @@ -0,0 +1,100 @@ +@filters +Feature: Filters on non-ActiveRecord resources + + ActiveAdmin should let admins use the `filter` DSL on resources that + aren't ActiveRecord-backed, without monkey-patching. + + Known limitation: the "Active Search" sidebar is coupled to Ransack, + so for a non-Ransack `@search` it renders empty ("No filters applied") + instead of filter chips. The filter form, "Clear Filters" link, + pagination, and table are unaffected. + + Background: + Given I am logged in + And a configuration of: + """ + ActiveAdmin.register ApiPost do + actions :index + + filter :title_cont, as: :string, label: "Title contains" + filter :status_eq, as: :select, label: "Status", + collection: %w[active inactive] + filter :title, as: :select, label: "Title (no predicate)", + collection: %w[foo bar] + + index do + column :id + column :title + column :status + end + + controller do + # Non-ActiveRecord resources override `find_collection`; Kaminari.paginate_array + # adapts the API response for ActiveAdmin's pagination UI. + def find_collection(*) + q = params[:q] || {} + @search = ApiPost::Search.new( + title_cont: q[:title_cont], + status_eq: q[:status_eq] + ) + page = (params[:page] || 1).to_i + per_page = 5 + + result = ApiPost.fetch( + page: page, + per_page: per_page, + title_cont: @search.title_cont, + status_eq: @search.status_eq + ) + + Kaminari.paginate_array( + result.records, + total_count: result.total_count, + limit: result.limit, + offset: result.offset + ).page(page).per(per_page) + end + end + end + """ + + Scenario: Server-side pagination across multiple pages + When I am on the index page for api_posts + Then I should see "Alpha" + And I should see "Epsilon" + And I should not see "Zeta" + And I should see "Showing 1-5 of 7" + And I should see pagination page 2 link + And I should see the following filters: + | Title contains | string | + | Status | select | + | Title (no predicate) | select | + + When I follow "2" + Then I should see "Zeta" + And I should see "Eta" + And I should see "Showing 6-7 of 7" + And I should not see "Alpha" + + Scenario: Filter narrows the result set to a single page (no pagination) + When I am on the index page for api_posts + Then I should see pagination page 2 link + + When I select "inactive" from "Status" + And I press "Filter" + Then I should see "Beta" + And I should see "Epsilon" + And I should not see "Alpha" + And I should not see "Gamma" + And I should see "Showing all 2" + And I should not see pagination + + Scenario: Filter narrows the result set to a single record + When I am on the index page for api_posts + And I fill in "Title contains" with "Bet" + And I press "Filter" + Then I should see "Beta" + And I should not see "Alpha" + And I should not see "Epsilon" + And I should see "Showing 1 of 1" + And I should not see pagination diff --git a/features/index/page_title.feature b/features/index/page_title.feature index f6f87819eb4..e43ee554ebf 100644 --- a/features/index/page_title.feature +++ b/features/index/page_title.feature @@ -6,7 +6,7 @@ Feature: Index - Page Title Given an index configuration of: """ ActiveAdmin.register Post do - index :title => "Awesome Title" + index title: "Awesome Title" end """ Then I should see the page title "Awesome Title" @@ -15,7 +15,7 @@ Feature: Index - Page Title Given an index configuration of: """ ActiveAdmin.register Post do - index :title => proc{ 'Custom title from proc' } + index title: proc{ 'Custom title from proc' } end """ Then I should see the page title "Custom title from proc" @@ -24,7 +24,18 @@ Feature: Index - Page Title Given an index configuration of: """ ActiveAdmin.register Post do - index :title => proc{ "List of #{resource_class.model_name.plural}" } + index title: proc{ "List of #{resource_class.model_name.plural}" } + end + """ + Then I should see the page title "List of posts" + + Scenario: Set the title in controller + Given an index configuration of: + """ + ActiveAdmin.register Post do + controller do + before_action { @page_title = "List of #{resource_class.model_name.plural}" } + end end """ Then I should see the page title "List of posts" diff --git a/features/index/pagination.feature b/features/index/pagination.feature index 53807be569b..70c9167b14c 100644 --- a/features/index/pagination.feature +++ b/features/index/pagination.feature @@ -1,14 +1,13 @@ Feature: Index Pagination - Background: Scenario: Viewing index when one page of resources exist Given an index configuration of: """ ActiveAdmin.register Post """ - Given 20 posts exist + And 20 posts exist When I am on the index page for posts - Then I should see "Displaying all 20 Posts" + Then I should see "Showing all 20" And I should not see pagination Scenario: Viewing index when multiple pages of resources exist @@ -16,9 +15,10 @@ Feature: Index Pagination """ ActiveAdmin.register Post """ - Given 31 posts exist + And 31 posts exist When I am on the index page for posts - Then I should see pagination with 2 pages + Then I should see pagination page 1 link + And I should see pagination page 2 link Scenario: Viewing index with a custom per page set Given an index configuration of: @@ -27,10 +27,11 @@ Feature: Index Pagination config.per_page = 2 end """ - Given 3 posts exist + And 3 posts exist When I am on the index page for posts - Then I should see pagination with 2 pages - And I should see "Displaying Posts 1 - 2 of 3 in total" + Then I should see pagination page 1 link + And I should see pagination page 2 link + And I should see "Showing 1-2 of 3" Scenario: Viewing index with pagination disabled Given an index configuration of: @@ -39,6 +40,25 @@ Feature: Index Pagination config.paginate = false end """ - Given 31 posts exist + And 31 posts exist When I am on the index page for posts Then I should not see pagination + + Scenario: Viewing index with pagination_total set to false + Given an index configuration of: + """ + ActiveAdmin.register Post do + config.per_page = 10 + index pagination_total: false do + end + end + """ + And 11 posts exist + When I am on the index page for posts + Then I should see "Showing 1-10" + And I should not see "of 11" + And I should see the pagination "Next" link + + When I follow "Next" + Then I should see "Showing 11-11" + And I should not see the pagination "Next" link diff --git a/features/index/switch_index_view.feature b/features/index/switch_index_view.feature index 2793942030b..4d7c381b42c 100644 --- a/features/index/switch_index_view.feature +++ b/features/index/switch_index_view.feature @@ -1,73 +1,55 @@ Feature: Switch Index View - In order to switch index views - As a user - I want to view links to views + In order to switch index views + As a user + I want to view links to views Scenario: Show default Page Presenter - Given a post with the title "Hello World from Table" exists - And an index configuration of: - """ - ActiveAdmin.register Post do - index :as => :table do - column :title - end - index :as => :block do |post| - span(link_to(post.title, admin_post_path(post))) - end - end - """ - Then I should see "Hello World from Table" within ".index_as_table" - - Scenario: Show default Page Presenter when default is specified - Given a post with the title "Hello World from Table" exists - And an index configuration of: - """ + Given a post with the title "Hello World from Table" exists + And an index configuration of: + """ ActiveAdmin.register Post do - index :as => :block do |post| - span(link_to(post.title, admin_post_path(post))) - end - index :as => :table, :default => true do + index do column :title end - end - """ - Then I should see "Hello World from Table" within ".index_as_table" - - Scenario: Show links to different page views - Given a post with the title "Hello World from Table" exists - And an index configuration of: - """ - ActiveAdmin.register Post do - index :as => :block do |post| + index as: CustomIndexView do |post| span(link_to(post.title, admin_post_path(post))) end - index :as => :table, :default => true do - column :title - end end """ - Then I should see "Hello World from Table" within ".index_as_table" - And I should see a link to "Table" - And I should see a link to "List" + Then I should see "Hello World from Table" within ".index-as-table" - Scenario: Show change between page views - Given a post with the title "Hey from Table" and body "My body is awesome" exists - And an index configuration of: - """ + Scenario: Show links to different page views + Given a post with the title "Hello World from Table" exists + And an index configuration of: + """ ActiveAdmin.register Post do - index :as => :block do |post| + index as: CustomIndexView do |post| span(link_to(post.title, admin_post_path(post))) end - index :as => :table, :default => true do + index default: true do column :title - column :body end end """ - Then I should see "My body is awesome" within ".index_as_table" - When I click "List" - Then I should not see "My body is awesome" within ".index_as_block" - - - + Then I should see "Hello World from Table" within ".index-as-table" + And I should see a link to "Table" + And I should see a link to "Custom" + + # Scenario: Show change between page views + # Given a post with the title "Hey from Table" and body "My body is awesome" exists + # And an index configuration of: + # """ + # ActiveAdmin.register Post do + # index as: CustomIndexView do |post| + # span(link_to(post.title, admin_post_path(post))) + # end + # index default: true do + # column :title + # column :body + # end + # end + # """ + # Then I should see "My body is awesome" within ".index-as-table" + # When I follow "Custom" + # Then I should not see "My body is awesome" within ".custom-index-view" diff --git a/features/menu.feature b/features/menu.feature index 7cd4b3d7dbe..74f3873c022 100644 --- a/features/menu.feature +++ b/features/menu.feature @@ -17,7 +17,7 @@ Feature: Menu Given a configuration of: """ ActiveAdmin.register Post do - menu :label => "Articles" + menu label: "Articles" end """ When I am on the dashboard @@ -29,7 +29,7 @@ Feature: Menu """ ActiveAdmin.application.namespace :admin do |admin| admin.build_menu do |menu| - menu.add :label => "Custom Menu", :url => :admin_dashboard_path + menu.add label: "Custom Menu", url: :admin_dashboard_path end end """ @@ -38,16 +38,47 @@ Feature: Menu When I follow "Custom Menu" Then I should be on the admin dashboard page + Scenario: Add a non-resource menu item with method delete + Given a configuration of: + """ + ActiveAdmin.application.namespace :admin do |admin| + admin.build_menu do |menu| + menu.add label: "Delete Menu", url: :admin_dashboard_path, html_options: { method: :delete } + end + end + """ + When I am on the dashboard + Then I should see a menu item for "Delete Menu" + And I should see the element "a[data-method='delete']:contains('Delete Menu')" + Scenario: Adding a resource as a sub menu item Given a configuration of: """ ActiveAdmin.register User ActiveAdmin.register Post do - menu :parent => 'User' + menu parent: 'Users' end """ When I am on the dashboard Then I should see a menu item for "Users" + And the "Posts" menu item should be hidden When I follow "Users" - Then the "Users" tab should be selected + Then the "Users" menu item should be selected + And I should see a nested menu item for "Posts" + + Scenario: Adding a resources as a sub menu items + Given a configuration of: + """ + ActiveAdmin.register Category do + menu parent: 'Anything' + end + ActiveAdmin.register Post do + menu parent: 'Anything' + end + """ + When I am on the dashboard + Then I should see a menu parent for "Anything" + And the "Categories" menu item should be hidden + And the "Posts" menu item should be hidden + And I should see a nested menu item for "Categories" And I should see a nested menu item for "Posts" diff --git a/features/new_page.feature b/features/new_page.feature index 5b1ad68fef7..6548476755f 100644 --- a/features/new_page.feature +++ b/features/new_page.feature @@ -4,11 +4,14 @@ Feature: New Page Background: Given a category named "Music" exists - Given a user named "John Doe" exists + And a user named "John Doe" exists And I am logged in - Given a configuration of: + And a configuration of: """ - ActiveAdmin.register Post + ActiveAdmin.register Post do + permit_params :custom_category_id, :author_id, :title, + :body, :position, :published_date, :starred + end """ When I am on the index page for posts @@ -29,19 +32,21 @@ Feature: New Page Given a configuration of: """ ActiveAdmin.register Post do + permit_params :custom_category_id, :author_id, :title, :body, :published_date, :starred + form do |f| f.inputs "Your Post" do f.input :title f.input :body end f.inputs "Publishing" do - f.input :published_at + f.input :published_date end f.actions end end """ - Given I follow "New Post" + And I follow "New Post" Then I should see a fieldset titled "Your Post" And I should see a fieldset titled "Publishing" When I fill in "Title" with "Hello World" @@ -51,23 +56,85 @@ Feature: New Page And I should see the attribute "Title" with "Hello World" And I should see the attribute "Body" with "This is the body" + Scenario: A form where calling a helper method with given kwargs is successful + Given a configuration of: + """ + ActiveAdmin.register Post do + form do |f| + f.inputs "Publishing" do + f.input :published_date, input_html: { "data-time" => format_time(Time.current, format: :short) } + end + f.actions + end + end + """ + And I follow "New Post" + Then I should see a fieldset titled "Publishing" + + Scenario: A form where calling a helper method with no kwargs is successful + Given a configuration of: + """ + ActiveAdmin.register Post do + form do |f| + f.inputs "Publishing" do + f.input :published_date, input_html: { "data-time" => format_time(Time.current) } + end + f.actions + end + end + """ + And I follow "New Post" + Then I should see a fieldset titled "Publishing" + + Scenario: Generating a custom form decorated with virtual attributes + Given a configuration of: + """ + ActiveAdmin.register Post do + decorate_with PostDecorator + permit_params :custom_category_id, :author_id, :virtual_title, :body, :published_date, :starred + + form decorate: true do |f| + f.inputs "Your Post" do + f.input :virtual_title + f.input :body + end + f.inputs "Publishing" do + f.input :published_date + end + f.actions + end + end + """ + And I follow "New Post" + Then I should see a fieldset titled "Your Post" + And I should see a fieldset titled "Publishing" + When I fill in "Virtual title" with "Hello World" + And I fill in "Body" with "This is the body" + And I press "Create Post" + Then I should see "Post was successfully created." + And I should see the attribute "Title" with "Hello World" + And I should see the attribute "Body" with "This is the body" + + @changes-filesystem Scenario: Generating a form from a partial Given "app/views/admin/posts/_form.html.erb" contains: """ <% url = @post.new_record? ? admin_posts_path : admin_post_path(@post) %> - <%= active_admin_form_for @post, :url => url do |f| + <%= active_admin_form_for @post, url: url do |f| f.inputs :title, :body f.actions end %> """ - Given a configuration of: + And a configuration of: """ ActiveAdmin.register Post do - form :partial => "form" + permit_params :custom_category_id, :author_id, :title, :body, :published_date, :starred + + form partial: "form" end """ - Given I follow "New Post" - When I fill in "Title" with "Hello World" + When I follow "New Post" + And I fill in "Title" with "Hello World" And I fill in "Body" with "This is the body" And I press "Create Post" Then I should see "Post was successfully created." @@ -78,6 +145,8 @@ Feature: New Page Given a configuration of: """ ActiveAdmin.register Post do + permit_params :custom_category_id, :author_id, :title, :body, :published_date, :starred + form do |f| f.inputs "Your Post" do if current_admin_user && false @@ -87,12 +156,12 @@ Feature: New Page f.input :body end f.inputs "Publishing" do - f.input :published_at + f.input :published_date end f.actions end end """ - Given I follow "New Post" + When I follow "New Post" Then I should not see "Title" And I should see "Body" diff --git a/features/registering_assets.feature b/features/registering_assets.feature deleted file mode 100644 index ec89c21db6f..00000000000 --- a/features/registering_assets.feature +++ /dev/null @@ -1,35 +0,0 @@ -Feature: Registering Assets - - Registering CSS and JS files - - Background: - Given a configuration of: - """ - ActiveAdmin.register Post - """ - And I am logged in - - - Scenario: Viewing default asset files - When I am on the index page for posts - Then I should see the css file "admin/active_admin.css" - Then I should see the js file "active_admin_vendor.js" - Then I should see the js file "active_admin.js" - - Scenario: Registering a CSS file - Given a configuration of: - """ - ActiveAdmin.application.register_stylesheet "some-random-css.css", :media => :print - ActiveAdmin.register Post - """ - When I am on the index page for posts - Then I should see the css file "some-random-css.css" of media "print" - - Scenario: Registering a JS file - Given a configuration of: - """ - ActiveAdmin.application.register_javascript "some-random-js.js" - ActiveAdmin.register Post - """ - When I am on the index page for posts - Then I should see the js file "some-random-js.js" diff --git a/features/registering_pages.feature b/features/registering_pages.feature index 63535222961..a58bf4ee8c4 100644 --- a/features/registering_pages.feature +++ b/features/registering_pages.feature @@ -17,7 +17,6 @@ Feature: Registering Pages When I go to the dashboard And I follow "Status" Then I should see the page title "Status" - And I should see the Active Admin layout And I should see the content "I love chocolate." Scenario: Registering a page with a complex name @@ -32,7 +31,6 @@ Feature: Registering Pages When I go to the dashboard And I follow "Chocolate I lØve You!" Then I should see the page title "Chocolate I lØve You!" - And I should see the Active Admin layout And I should see the content "I love chocolate." Scenario: Registering an empty page @@ -43,13 +41,12 @@ Feature: Registering Pages When I go to the dashboard And I follow "Status" Then I should see the page title "Status" - And I should see the Active Admin layout Scenario: Registering a page with a custom title as a string Given a configuration of: """ ActiveAdmin.register_page "Status" do - content :title => "Custom Page Title" do + content title: "Custom Page Title" do "I love chocolate." end end @@ -62,7 +59,7 @@ Feature: Registering Pages Given a configuration of: """ ActiveAdmin.register_page "Status" do - content :title => proc{ "Custom Page Title from Proc" } do + content title: proc{ "Custom Page Title from Proc" } do "I love chocolate." end end @@ -86,14 +83,13 @@ Feature: Registering Pages """ When I go to the dashboard And I follow "Status" - Then I should see a sidebar titled "Help" - + Then I should see "Need help? Email us at help@example.com" Scenario: Adding an action item to a page Given a configuration of: """ ActiveAdmin.register_page "Status" do - action_item do + action_item :visit do link_to "Visit", "/" end @@ -106,6 +102,34 @@ Feature: Registering Pages And I follow "Status" Then I should see an action item link to "Visit" + Scenario: Adding a page action to a page with multiple http methods + Given a configuration of: + """ + ActiveAdmin.register_page "Status" do + page_action :check, method: [:get, :post] do + redirect_to admin_status_path, notice: "Checked via #{request.method}" + end + + action_item :post_check do + link_to "Post Check", admin_status_check_path, method: :post + end + + action_item :get_check do + link_to "Get Check", admin_status_check_path + end + + content do + "I love chocolate." + end + end + """ + When I go to the dashboard + And I follow "Status" + And I follow "Post Check" + Then I should see "Checked via POST" + When I follow "Get Check" + Then I should see "Checked via GET" + Scenario: Adding a page action to a page Given a configuration of: """ @@ -123,8 +147,8 @@ Feature: Registering Pages And I follow "Status" And I follow "Check" Then I should see the content "Chocolate I lØve You!" - And I should see the Active Admin layout + @changes-filesystem Scenario: Adding a page action to a page with erb view Given a configuration of: """ @@ -137,7 +161,7 @@ Feature: Registering Pages end end """ - Given "app/views/admin/status/check.html.erb" contains: + And "app/views/admin/status/check.html.erb" contains: """
Chocolate lØves You Too!
""" @@ -145,4 +169,61 @@ Feature: Registering Pages And I follow "Status" And I follow "Check" Then I should see the content "Chocolate lØves You Too!" - And I should see the Active Admin layout + + Scenario: Registering a page with paginated index table for a collection Array + Given a user named "John Doe" exists + And a configuration of: + """ + ActiveAdmin.register_page "Special users" do + content do + collection = Kaminari.paginate_array(User.all).page(params.fetch(:page, 1)) + + table_for(collection) do + column :first_name + column :last_name + end + + paginated_collection(collection, entry_name: "Special users") + end + end + """ + When I go to the dashboard + And I follow "Special users" + Then I should see the page title "Special users" + And I should see 1 user in the table + + Scenario: Displaying parent information from a belongs_to page + Given a configuration of: + """ + ActiveAdmin.register Post + ActiveAdmin.register_page "Status" do + belongs_to :post + + content do + "Status page for #{helpers.parent.title}" + end + end + """ + And 1 post with the title "Post 1" exists + When I go to the first post custom status page + Then I should see the content "Status page for Post 1" + And I should see a link to "Post 1" in the breadcrumb + + Scenario: Rendering sortable table_for within page + Given a configuration of: + """ + ActiveAdmin.register Post + ActiveAdmin.register_page "Last Posts" do + content do + table_for Post.last(2), sortable: true do + column :id + column :title + column :author + end + end + end + """ + And a post with the title "Hello World" written by "Jane Doe" exists + When I go to the last posts page + Then I should see the page title "Last Posts" + And I should see 1 post in the table diff --git a/features/registering_resources.feature b/features/registering_resources.feature index 20d1bfc0cc8..094e612c897 100644 --- a/features/registering_resources.feature +++ b/features/registering_resources.feature @@ -22,7 +22,7 @@ Feature: Registering Resources Scenario: Registering a resource with another name Given a configuration of: """ - ActiveAdmin.register Post, :as => "My Post" + ActiveAdmin.register Post, as: "My Post" """ When I go to the dashboard Then I should see "My Posts" diff --git a/features/renamed_resource.feature b/features/renamed_resource.feature new file mode 100644 index 00000000000..86d986ebfce --- /dev/null +++ b/features/renamed_resource.feature @@ -0,0 +1,70 @@ +Feature: Renamed Resource + + Resources renamed with as: 'NewName' + + Scenario: Default form with no config + Given a category named "Music" exists + And a user named "John Doe" exists + And I am logged in + And a configuration of: + """ + ActiveAdmin.register Blog::Post, as: 'Post' do + permit_params :custom_category_id, :author_id, :title, + :body, :position, :published_date, :starred + end + """ + When I am on the index page for posts + And I follow "New Post" + And I fill in "Title" with "Hello World" + And I fill in "Body" with "This is the body" + And I select "Music" from "Category" + And I select "John Doe" from "Author" + And I press "Create Post" + Then I should see "Post was successfully created." + And I should see the attribute "Title" with "Hello World" + And I should see the attribute "Body" with "This is the body" + And I should see the attribute "Category" with "Music" + And I should see the attribute "Author" with "John Doe" + + Scenario: With a belongs_to optional association + Given a category named "Music" exists + And a user named "John Doe" exists + And I am logged in + And a configuration of: + """ + ActiveAdmin.register User, as: 'Author' do + show do |author| + attributes_table_for(resource) do + row :articles do + link_to 'Author Articles', admin_author_articles_path(author) + end + end + end + end + + ActiveAdmin.register Post, as: 'Article' do + belongs_to :author, optional: true + permit_params :custom_category_id, :author_id, :title, + :body, :position, :published_date, :starred + end + + ActiveAdmin.register Post, as: 'News', namespace: :admin2 + """ + When I am on the index page for articles + And I follow "New Article" + And I fill in "Title" with "Hello World" + And I fill in "Body" with "This is the body" + And I select "Music" from "Category" + And I select "John Doe" from "Author" + And I press "Create Post" + Then I should see "Post was successfully created." + And I should see the attribute "Title" with "Hello World" + And I should see the attribute "Body" with "This is the body" + And I should see the attribute "Category" with "Music" + And I should see the attribute "Author" with "John Doe" + When I click "John Doe" + And I click "Author Articles" + Then I should see a table header with "Title" + And I should see a table header with "Body" + And I should see "Hello World" + And I should see "This is the body" diff --git a/features/root_to.feature b/features/root_to.feature index 42a05ce7f2d..89a374d24df 100644 --- a/features/root_to.feature +++ b/features/root_to.feature @@ -1,3 +1,4 @@ +@root Feature: Namespace root As a developer @@ -13,5 +14,5 @@ Feature: Namespace root """ ActiveAdmin.application.root_to = 'stores#index' """ - Given I am logged in with capybara + And I am logged in with capybara Then I should see the page title "Bookstores" diff --git a/features/show/default_content.feature b/features/show/default_content.feature index 5718cd2aae3..ee85de658f8 100644 --- a/features/show/default_content.feature +++ b/features/show/default_content.feature @@ -3,7 +3,7 @@ Feature: Show - Default Content Viewing the show page for a resource Background: - Given a post with the title "Hello World" written by "Jane Doe" exists + Given a unstarred post with the title "Hello World" written by "Jane Doe" in category "Stories" exists Scenario: Viewing the default show page Given a show configuration of: @@ -14,8 +14,9 @@ Feature: Show - Default Content And I should see the attribute "Body" with "Empty" And I should see the attribute "Created At" with a nicely formatted datetime And I should see the attribute "Author" with "Jane Doe" - And I should see an action item button "Delete Post" - And I should see an action item button "Edit Post" + And I should see the attribute "Starred" with "No" + And I should see an action item link to "Delete Post" + And I should see an action item link to "Edit Post" Scenario: Attributes should link when linked resource is registered Given a show configuration of: @@ -32,7 +33,7 @@ Feature: Show - Default Content ActiveAdmin.register Post do show do - attributes_table :title, :body, :created_at, :updated_at + attributes_table_for(resource, :title, :body, :created_at, :updated_at) end end @@ -41,3 +42,20 @@ Feature: Show - Default Content And I should see the attribute "Body" with "Empty" And I should see the attribute "Created At" with a nicely formatted datetime And I should not see the attribute "Author" + + Scenario: Columns with "counter cache"-like names + Given a show configuration of: + """ + ActiveAdmin.register User + """ + Then I should see the attribute "First Name" with "Jane" + And I should see the attribute "Last Name" with "Doe" + And I should see the attribute "Sign In Count" with "0" + + Scenario: Counter cache columns + Given a show configuration of: + """ + ActiveAdmin.register Category + """ + Then I should see the attribute "Name" with "Stories" + And I should not see the attribute "Posts Count" diff --git a/features/show/page_title.feature b/features/show/page_title.feature index 20f3c889d32..6b77e695bb7 100644 --- a/features/show/page_title.feature +++ b/features/show/page_title.feature @@ -9,7 +9,7 @@ Feature: Show - Page Title Given a show configuration of: """ ActiveAdmin.register Post do - show :title => :title + show title: :title end """ Then I should see the page title "Hello World" @@ -18,7 +18,7 @@ Feature: Show - Page Title Given a show configuration of: """ ActiveAdmin.register Post do - show :title => "Title From String" + show title: "Title From String" end """ Then I should see the page title "Title From String" @@ -27,7 +27,7 @@ Feature: Show - Page Title Given a show configuration of: """ ActiveAdmin.register Post do - show :title => proc{|post| "Title: " + post.title } + show title: proc{|post| "Title: " + post.title } end """ Then I should see the page title "Title: Hello World" @@ -45,3 +45,14 @@ Feature: Show - Page Title ActiveAdmin.register Tag """ Then I should see the page title "Tag #" + + Scenario: Set the title in controller + Given a show configuration of: + """ + ActiveAdmin.register Post do + controller do + before_action { @page_title = "List of #{resource_class.model_name.plural}" } + end + end + """ + Then I should see the page title "List of posts" diff --git a/features/show/page_title_with_html.feature b/features/show/page_title_with_html.feature new file mode 100644 index 00000000000..500ba6d7910 --- /dev/null +++ b/features/show/page_title_with_html.feature @@ -0,0 +1,11 @@ +Feature: Show - Page Title with HTML + + Page Title is escaped + + Scenario: Set an HTML string as the title + Given a post with the title "
John Doe" written by "Jane Doe" exists + And a show configuration of: + """ + ActiveAdmin.register Post + """ + Then I should see the page title "John Doe" diff --git a/features/sidebar_sections.feature b/features/sidebar_sections.feature index 6e5c1b90495..fc2a1f98ba2 100644 --- a/features/sidebar_sections.feature +++ b/features/sidebar_sections.feature @@ -16,87 +16,83 @@ Feature: Sidebar Sections end """ When I am on the index page for posts - Then I should see a sidebar titled "Help" - Then I should see "Need help" within the "Help" sidebar + Then I should see "Need help? Email us at help@example.com" When I follow "View" - Then I should see a sidebar titled "Help" + Then I should see "Need help? Email us at help@example.com" When I follow "Edit Post" - Then I should see a sidebar titled "Help" + Then I should see "Need help? Email us at help@example.com" When I am on the index page for posts - When I follow "New Post" - Then I should see a sidebar titled "Help" - + And I follow "New Post" + Then I should see "Need help? Email us at help@example.com" Scenario: Create a sidebar for only one action Given a configuration of: """ ActiveAdmin.register Post do - sidebar :help, :only => :index do + sidebar :help, only: :index do "Need help? Email us at help@example.com" end end """ When I am on the index page for posts - Then I should see a sidebar titled "Help" - Then I should see "Need help" within the "Help" sidebar + Then I should see "Need help? Email us at help@example.com" When I follow "View" - Then I should not see a sidebar titled "Help" + Then I should not see "Need help? Email us at help@example.com" When I follow "Edit Post" - Then I should not see a sidebar titled "Help" + Then I should not see "Need help? Email us at help@example.com" When I am on the index page for posts - When I follow "New Post" - Then I should not see a sidebar titled "Help" - + And I follow "New Post" + Then I should not see "Need help? Email us at help@example.com" Scenario: Create a sidebar for all except one action Given a configuration of: """ ActiveAdmin.register Post do - sidebar :help, :except => :index do + sidebar :help, except: :index do "Need help? Email us at help@example.com" end end """ When I am on the index page for posts - Then I should not see a sidebar titled "Help" + Then I should not see "Need help? Email us at help@example.com" When I follow "View" - Then I should see a sidebar titled "Help" + Then I should see "Need help? Email us at help@example.com" When I follow "Edit Post" - Then I should see a sidebar titled "Help" + Then I should see "Need help? Email us at help@example.com" When I am on the index page for posts - When I follow "New Post" - Then I should see a sidebar titled "Help" + And I follow "New Post" + Then I should see "Need help? Email us at help@example.com" Scenario: Create a sidebar for only one action with if clause that returns false Given a configuration of: """ ActiveAdmin.register Post do - sidebar :help, :only => :index, :if => proc{ current_active_admin_user.nil? } do + sidebar :help, only: :index, if: proc{ current_active_admin_user.nil? } do "Need help? Email us at help@example.com" end end """ When I am on the index page for posts - Then I should not see a sidebar titled "Help" + Then I should not see "Need help? Email us at help@example.com" When I follow "View" - Then I should not see a sidebar titled "Help" + Then I should not see "Need help? Email us at help@example.com" When I follow "Edit Post" - Then I should not see a sidebar titled "Help" + Then I should not see "Need help? Email us at help@example.com" When I am on the index page for posts - When I follow "New Post" - Then I should not see a sidebar titled "Help" + And I follow "New Post" + Then I should not see "Need help? Email us at help@example.com" Scenario: Create a sidebar for only one action with if clause with method symbol Given a configuration of: @@ -106,46 +102,45 @@ Feature: Sidebar Sections end ActiveAdmin.register Post do controller { helper SidebarHelper } - sidebar :help, :only => :index, :if => :can_sidebar? do + sidebar :help, only: :index, if: :can_sidebar? do "Need help? Email us at help@example.com" end end """ When I am on the index page for posts - Then I should not see a sidebar titled "Help" + Then I should not see "Need help? Email us at help@example.com" When I follow "View" - Then I should not see a sidebar titled "Help" + Then I should not see "Need help? Email us at help@example.com" When I follow "Edit Post" - Then I should not see a sidebar titled "Help" + Then I should not see "Need help? Email us at help@example.com" When I am on the index page for posts - When I follow "New Post" - Then I should not see a sidebar titled "Help" + And I follow "New Post" + Then I should not see "Need help? Email us at help@example.com" Scenario: Create a sidebar for only one action with if clause that returns true Given a configuration of: """ ActiveAdmin.register Post do - sidebar :help, :only => :show, :if => proc{ !current_active_admin_user.nil? } do + sidebar :help, only: :show, if: proc{ !current_active_admin_user.nil? } do "Need help? Email us at help@example.com" end end """ When I am on the index page for posts - Then I should not see a sidebar titled "Help" + Then I should not see "Need help? Email us at help@example.com" When I follow "View" - Then I should see a sidebar titled "Help" Then I should see "Need help" within the "Help" sidebar When I follow "Edit Post" - Then I should not see a sidebar titled "Help" + Then I should not see "Need help? Email us at help@example.com" When I am on the index page for posts - When I follow "New Post" - Then I should not see a sidebar titled "Help" + And I follow "New Post" + Then I should not see "Need help? Email us at help@example.com" Scenario: Create a sidebar with deep content Given a configuration of: @@ -164,16 +159,16 @@ Feature: Sidebar Sections end """ When I am on the index page for posts - Then I should see a sidebar titled "Help" And I should see "First List First Item" within the "Help" sidebar And I should see "Second List Second Item" within the "Help" sidebar + @changes-filesystem Scenario: Rendering sidebar by default without a block or partial name Given "app/views/admin/posts/_help_sidebar.html.erb" contains: """

Hello World from a partial

""" - Given a configuration of: + And a configuration of: """ ActiveAdmin.register Post do sidebar :help @@ -182,17 +177,29 @@ Feature: Sidebar Sections When I am on the index page for posts Then I should see "Hello World from a partial" within the "Help" sidebar + @changes-filesystem Scenario: Rendering a partial as the sidebar content Given "app/views/admin/posts/_custom_help_partial.html.erb" contains: """

Hello World from a custom partial

""" - Given a configuration of: + And a configuration of: """ ActiveAdmin.register Post do - sidebar :help, :partial => "custom_help_partial" + sidebar :help, partial: "custom_help_partial" end """ When I am on the index page for posts Then I should see "Hello World from a custom partial" within the "Help" sidebar + Scenario: Position sidebar at the top using priority option + Given a configuration of: + """ + ActiveAdmin.register Post do + sidebar :help, priority: 0 do + "Need help? Email us at help@example.com" + end + end + """ + When I am on the index page for posts + Then I should see content "Need help? Email us at help@example.com" above other content "Filters" diff --git a/features/site_title.feature b/features/site_title.feature index 03cd236defb..2dc92d2d027 100644 --- a/features/site_title.feature +++ b/features/site_title.feature @@ -1,3 +1,4 @@ +@site_title Feature: Site title As a developer @@ -7,41 +8,30 @@ Feature: Site title Background: Given I am logged in - Scenario: Set the site title and site title link + Scenario: Set the site title Given a configuration of: """ ActiveAdmin.application.site_title = "My Great Site" - ActiveAdmin.application.site_title_link = "/" """ When I am on the dashboard And I should see the site title "My Great Site" - When I follow "My Great Site" - Then I should see "Ruby on Rails: Welcome aboard" - Scenario: Set the site title image - Given a configuration of: - """ - ActiveAdmin.application.site_title_image = "http://railscasts.com/assets/episodes/stills/284-active-admin.png?1316476106" - """ - When I am on the dashboard - And I should not see the site title "My Great Site" - And I should see the site title image "http://railscasts.com/assets/episodes/stills/284-active-admin.png?1316476106" - - Scenario: Set the site title image with link + Scenario: Set the site title to a proc Given a configuration of: """ - ActiveAdmin.application.site_title_link = "http://www.google.com" - ActiveAdmin.application.site_title_image = "http://railscasts.com/assets/episodes/stills/284-active-admin.png?1316476106" + ActiveAdmin.application.site_title = proc { "Hello #{controller.current_admin_user.try(:email) || 'you!'}" } """ When I am on the dashboard - And I should see the site title image "http://railscasts.com/assets/episodes/stills/284-active-admin.png?1316476106" - And I should see the site title image linked to "http://www.google.com" + And I should see the site title "Hello admin@example.com" - Scenario: Set the site title to a proc + Scenario: Set the site title by namespace Given a configuration of: """ - ActiveAdmin.application.site_title_image = nil # Configuration is not reset between scenarios - ActiveAdmin.application.site_title = proc { "Hello #{controller.current_admin_user.try(:email) || 'you!'}" } + ActiveAdmin.application.site_title = "My Great Site" + ActiveAdmin.application.namespace(:superadmin).site_title = "Namespace Site Title" + ActiveAdmin.register AdminUser, namespace: :superadmin """ + When I am on the index page for admin_users in the superadmin namespace + Then I should see the site title "Namespace Site Title" When I am on the dashboard - And I should see the site title "Hello admin@example.com" + Then I should see the site title "My Great Site" diff --git a/features/specifying_actions.feature b/features/specifying_actions.feature index 8a49949a617..44390f4a50e 100644 --- a/features/specifying_actions.feature +++ b/features/specifying_actions.feature @@ -19,18 +19,19 @@ Feature: Specifying Actions When I am on the index page for posts Then an "ActionController::RoutingError" exception should be raised when I follow "View" + @changes-filesystem Scenario: Specify a custom collection action with template Given a configuration of: """ ActiveAdmin.register Post do - action_item(:only => :index) do + action_item(:import, only: :index) do link_to('Import Posts', import_admin_posts_path) end collection_action :import end """ - Given "app/views/admin/posts/import.html.arb" contains: + And "app/views/admin/posts/import.html.arb" contains: """ para "We are currently working on this feature..." """ @@ -39,11 +40,12 @@ Feature: Specifying Actions And I follow "Import" Then I should see "We are currently working on this feature" + @changes-filesystem Scenario: Specify a custom member action with template Given a configuration of: """ ActiveAdmin.register Post do - action_item(:only => :show) do + action_item(:review, only: :show) do link_to('Review', review_admin_post_path) end @@ -52,7 +54,7 @@ Feature: Specifying Actions end end """ - Given "app/views/admin/posts/review.html.erb" contains: + And "app/views/admin/posts/review.html.erb" contains: """

Review: <%= @post.title %>

""" @@ -63,13 +65,13 @@ Feature: Specifying Actions And I follow "Review" Then I should see "Review: Hello World" And I should see the page title "Review" - And I should see the Active Admin layout + @changes-filesystem Scenario: Specify a custom member action with template using arb Given a configuration of: """ ActiveAdmin.register Post do - action_item(:only => :show) do + action_item(:review, only: :show) do link_to('Review', review_admin_post_path) end @@ -78,9 +80,9 @@ Feature: Specifying Actions end end """ - Given "app/views/admin/posts/review.html.arb" contains: + And "app/views/admin/posts/review.html.arb" contains: """ - h1 "Review: #{@post.title}" + h1 "Review: #{post.title}" """ And I am logged in And a post with the title "Hello World" exists @@ -89,5 +91,29 @@ Feature: Specifying Actions And I follow "Review" Then I should see "Review: Hello World" And I should see the page title "Review" - And I should see the Active Admin layout + Scenario: Specify a custom member action with multiple http methods + Given a configuration of: + """ + ActiveAdmin.register Post do + action_item(:get_check, only: :show) do + link_to('Get Check', check_admin_post_path) + end + + action_item(:post_check, only: :show) do + link_to('Post Check', check_admin_post_path, method: :post) + end + + member_action :check, method: [:get, :post] do + redirect_to admin_post_path(resource), notice: "Checked via #{request.method}" + end + end + """ + And I am logged in + And a post with the title "Hello World" exists + When I am on the index page for posts + And I follow "View" + And I follow "Get Check" + Then I should see "Checked via GET" + When I follow "Post Check" + Then I should see "Checked via POST" diff --git a/features/step_definitions/action_item_steps.rb b/features/step_definitions/action_item_steps.rb index 8030dc2d08e..be27c221bb0 100644 --- a/features/step_definitions/action_item_steps.rb +++ b/features/step_definitions/action_item_steps.rb @@ -1,7 +1,8 @@ -Then /^I should see an action item link to "([^"]*)"$/ do |link| - page.should have_css('.action_item a', :text => link) +# frozen_string_literal: true +Then(/^I should see an action item link to "([^"]*)"$/) do |link| + expect(page).to have_css("[data-test-action-items] > a", text: link) end -Then /^I should not see an action item link to "([^"]*)"$/ do |link| - page.should_not have_css('.action_item a', :text => link) +Then(/^I should not see an action item link to "([^"]*)"$/) do |link| + expect(page).to have_no_css("[data-test-action-items] > a", text: link) end diff --git a/features/step_definitions/action_link_steps.rb b/features/step_definitions/action_link_steps.rb index 46273f2db29..9531ac3f540 100644 --- a/features/step_definitions/action_link_steps.rb +++ b/features/step_definitions/action_link_steps.rb @@ -1,7 +1,12 @@ -Then /^I should see a member link to "([^"]*)"$/ do |name| - page.should have_css("a.member_link", :text => name) +# frozen_string_literal: true +Then(/^I should see a member link to "([^"]*)"$/) do |name| + expect(page).to have_css(".data-table-resource-actions > a", text: name) end -Then /^I should not see a member link to "([^"]*)"$/ do |name| - %{Then I should not see "#{name}" within "a.member_link"} +Then(/^I should not see a member link to "([^"]*)"$/) do |name| + %{Then I should not see "#{name}" within ".data-table-resource-actions > a"} +end + +Then(/^I should see the actions column with the class "([^"]*)" and the title "([^"]*)"$/) do |klass, title| + expect(page).to have_css "th#{'.' + klass}", text: title end diff --git a/features/step_definitions/additional_web_steps.rb b/features/step_definitions/additional_web_steps.rb index f8cd625eb7a..e1217c3c279 100644 --- a/features/step_definitions/additional_web_steps.rb +++ b/features/step_definitions/additional_web_steps.rb @@ -1,82 +1,79 @@ -Then /^I should see a table header with "([^"]*)"$/ do |content| - page.should have_xpath('//th', :text => content) +# frozen_string_literal: true +Then(/^I should see a table header with "([^"]*)"$/) do |content| + expect(page).to have_xpath "//th", text: content end -Then /^I should not see a table header with "([^"]*)"$/ do |content| - page.should_not have_xpath('//th', :text => content) +Then(/^I should not see a table header with "([^"]*)"$/) do |content| + expect(page).to have_no_xpath "//th", text: content end -Then /^I should see a sortable table header with "([^"]*)"$/ do |content| - page.should have_css('th.sortable', :text => content) +Then(/^I should see a sortable table header with "([^"]*)"$/) do |content| + expect(page).to have_css "th[data-sortable]", text: content end -Then /^I should not see a sortable table header with "([^"]*)"$/ do |content| - page.should_not have_css('th.sortable', :text => content) +Then(/^I should not see a sortable table header with "([^"]*)"$/) do |content| + expect(page).to have_no_css "th[data-sortable]", text: content end -Then /^I should not see a sortable table header$/ do - step %{I should not see "th.sortable"} +Then(/^I should not see a sortable table header$/) do + step %{I should not see "th[data-sortable]"} end -Then /^the table "([^"]*)" should have (\d+) rows/ do |selector, count| - table = page.find(selector) - table.all(:css, 'tr').size.should == count.to_i +Then(/^the table "([^"]*)" should have (\d+) rows/) do |selector, count| + trs = page.find(selector).all :css, "tr" + expect(trs.size).to eq count.to_i end -Then /^the table "([^"]*)" should have (\d+) columns/ do |selector, count| - table = page.find(selector) - row = table.find('tr:first') - row.all(:css, "td").size.should == count.to_i +Then(/^the table "([^"]*)" should have (\d+) columns/) do |selector, count| + tds = page.find(selector).find("tr:first").all :css, "td" + expect(tds.size).to eq count.to_i end -Then /^there should be (\d+) "([^"]*)" tags$/ do |count, tag| - page.all(:css, tag).size.should == count.to_i +Then(/^there should be (\d+) "([^"]*)" tags?$/) do |count, tag| + expect(page.all(:css, tag).size).to eq count.to_i end -Then /^I should see a link to "([^"]*)"$/ do |link| - page.should have_xpath('//a', :text => link) +Then(/^I should see a link to "([^"]*)"$/) do |link| + if Capybara.current_driver == Capybara.javascript_driver + expect(page).to have_xpath "//a", text: link, wait: 30 + else + expect(page).to have_xpath "//a", text: link + end end -Then /^an "([^"]*)" exception should be raised when I follow "([^"]*)"$/ do |error, link| - lambda { +Then(/^an "([^"]*)" exception should be raised when I follow "([^"]*)"$/) do |error, link| + expect do step "I follow \"#{link}\"" - }.should raise_error(error.constantize) -end - -Then /^I should be in the resource section for (.+)$/ do |resource_name| - current_url.should include resource_name.gsub(' ', '').underscore.pluralize + end.to raise_error(error.constantize) end -Then /^I should wait and see "([^"]*)"(?: within "([^"]*)")?$/ do |text, selector| - sleep 1 - step 'show me the page' - selector ||= "*" - locate(:xpath, "//#{selector}[text()='#{text}']") +Then(/^I should be in the resource section for (.+)$/) do |resource_name| + expect(current_url).to include resource_name.tr(" ", "").underscore.pluralize end -Then /^I should see the page title "([^"]*)"$/ do |title| - within('h2#page_title') do - page.should have_content(title) +Then(/^I should see the page title "([^"]*)"$/) do |title| + within("[data-test-page-header]") do + expect(page).to have_text title end end -Then /^I should see a fieldset titled "([^"]*)"$/ do |title| - page.should have_css('fieldset legend', :text => title) +Then(/^I should see a fieldset titled "([^"]*)"$/) do |title| + expect(page).to have_css "fieldset legend", text: title end -Then /^the "([^"]*)" field should contain the option "([^"]*)"$/ do |field, option| +Then(/^the "([^"]*)" field should contain the option "([^"]*)"$/) do |field, option| field = find_field(field) - field.should have_css("option", :text => option) + expect(field).to have_css "option", text: option end -Then /^I should see the content "([^"]*)"$/ do |content| - page.should have_css("#active_admin_content", :text => content) +Then(/^I should see the content "([^"]*)"$/) do |content| + expect(page).to have_css "[data-test-page-content]", text: content end -Then /^I should see a validation error "([^"]*)"$/ do |error_message| - page.should have_css(".inline-errors", :text => error_message) +Then(/^I should see a validation error "([^"]*)"$/) do |error_message| + expect(page).to have_css ".inline-errors", text: error_message end -Then /^I should see a table with id "([^"]*)"$/ do |dom_id| - page.should have_css('table', :id => dom_id) +Then(/^I should see a table with id "([^"]*)"$/) do |dom_id| + page.find("table##{dom_id}") end diff --git a/features/step_definitions/asset_steps.rb b/features/step_definitions/asset_steps.rb deleted file mode 100644 index 537fd13f5c8..00000000000 --- a/features/step_definitions/asset_steps.rb +++ /dev/null @@ -1,11 +0,0 @@ -Then /^I should see the css file "([^"]*)"$/ do |path| - step %{I should see the css file "#{path}" of media "screen"} -end - -Then /^I should see the css file "([^"]*)" of media "([^"]*)"$/ do |path, media| - page.should have_xpath("//link[contains(@href, /stylesheets/#{path}) and contains(@media, #{media})]") -end - -Then /^I should see the js file "([^"]*)"$/ do |path| - page.should have_xpath("//script[contains(@src, /javascripts/#{path})]") -end diff --git a/features/step_definitions/attribute_steps.rb b/features/step_definitions/attribute_steps.rb index 6f1b624d6d0..20f652ba434 100644 --- a/features/step_definitions/attribute_steps.rb +++ b/features/step_definitions/attribute_steps.rb @@ -1,13 +1,19 @@ -Then /^I should see the attribute "([^"]*)" with "([^"]*)"$/ do |title, value| - elems = all ".attributes_table th:contains('#{title}') ~ td:contains('#{value}')" - elems.first.should_not be_nil, 'attribute missing' +# frozen_string_literal: true +Then(/^I should( not)? see the attribute "([^"]*)" with "([^"]*)"$/) do |negate, title, value| + elems = all ".attributes-table th:contains('#{title}') ~ td:contains('#{value}')" + + if negate + expect(elems.first).to eq(nil), "attribute missing" + else + expect(elems.first).to_not eq(nil), "attribute missing" + end end -Then /^I should see the attribute "([^"]*)" with a nicely formatted datetime$/ do |title| - text = all(".attributes_table th:contains('#{title}') ~ td").first.text - text.should match /\w+ \d{1,2}, \d{4} \d{2}:\d{2}/ +Then(/^I should see the attribute "([^"]*)" with a nicely formatted datetime$/) do |title| + text = first(".attributes-table th:contains('#{title}') ~ td").text + expect(text).to match(/\w+ \d{1,2}, \d{4} \d{2}:\d{2}/) end -Then /^I should not see the attribute "([^"]*)"$/ do |title| - page.should_not have_css('.attributes_table th', :text => title) +Then(/^I should not see the attribute "([^"]*)"$/) do |title| + expect(page).to have_no_css ".attributes-table th", text: title end diff --git a/features/step_definitions/batch_action_steps.rb b/features/step_definitions/batch_action_steps.rb index 9f124143c82..2a0a126d4b3 100644 --- a/features/step_definitions/batch_action_steps.rb +++ b/features/step_definitions/batch_action_steps.rb @@ -1,55 +1,58 @@ -Then /^I (should|should not) be asked to confirm "([^"]*)" for "([^"]*)"$/ do |maybe, confirmation, title| - selector = "#batch_actions_popover a.batch_action:contains('#{title}')" - selector << "[data-confirm='#{confirmation}']" if maybe == 'should' - page.send maybe.sub(' ', '_'), have_css(selector) -end +# frozen_string_literal: true +Then(/^I (should|should not) see the batch action :([^\s]*) "([^"]*)"$/) do |maybe, sym, title| + selector = "[data-batch-action-item]" + selector += "[href='#'][data-action='#{sym}']" if maybe == "should" -Then /^I (should|should not) see the batch action :([^\s]*) "([^"]*)"$/ do |maybe, sym, title| - selector = "#batch_actions_selector a.batch_action:contains('#{title}')" - selector << "[href='#'][data-action='#{sym}']" if maybe == 'should' - page.send maybe.sub(' ', '_'), have_css(selector) + verb = maybe == "should" ? :to : :to_not + expect(page).send verb, have_css(selector, text: title) end -Then /^the (\d+)(?:st|nd|rd|th) batch action should be "([^"]*)"$/ do |index, title| - page.all("#batch_actions_selector a.batch_action")[index.to_i - 1].text.should match title +Then(/^the (\d+)(?:st|nd|rd|th) batch action should be "([^"]*)"$/) do |index, title| + batch_action = page.all("[data-batch-action-item]")[index.to_i - 1] + expect(batch_action.text).to match title end -When /^I check the (\d+)(?:st|nd|rd|th) record$/ do |index| - page.all("table.index_table input[type=checkbox]")[index.to_i].set true +When(/^I check the (\d+)(?:st|nd|rd|th) record$/) do |index| + page.all(".batch-actions-resource-selection")[index.to_i].set true end -When /^I toggle the collection selection$/ do - page.find("#collection_selection_toggle_all").click +Then(/^I should see that the batch action button is disabled$/) do + expect(page).to have_css ".batch-actions-dropdown button[disabled]" end -Then /^I should see that the batch action button is disabled$/ do - page.should have_css "#batch_actions_selector .dropdown_menu_button.disabled" -end +Then(/^I (should|should not) see the batch action (button|selector)$/) do |maybe, type| + selector = ".batch-actions-dropdown" + selector += " button" if maybe == "should" && type == "button" -Then /^I (should|should not) see the batch action (button|selector)$/ do |maybe, type| - selector = "div.table_tools #batch_actions_selector" - selector << ' .dropdown_menu_button' if maybe == 'should' && type == 'button' - page.send maybe.sub(' ', '_'), have_css(selector) + verb = maybe == "should" ? :to : :to_not + expect(page).send verb, have_css(selector) end -Then /^I should see the batch action popover exists$/ do - page.should have_css "#batch_actions_selector" +Then(/^I should see the batch action popover$/) do + expect(page).to have_css ".batch-actions-dropdown" end -Given /^I submit the batch action form with "([^"]*)"$/ do |action| - page.find("#batch_action").set action - form = page.find "#collection_selection" - params = page.all("#main_content input").each_with_object({}) do |input, obj| - key, value = input['name'], input['value'] - if key == 'collection_selection[]' +Given(/^I submit the batch action form with "([^"]*)"$/) do |action| + page.find_by_id('batch_action', visible: false).set action + form = page.find_by_id 'collection_selection' + params = page.all("#collection_selection input", visible: false).each_with_object({}) do |input, obj| + key = input["name"] + value = input["value"] + if key == "collection_selection[]" (obj[key] ||= []).push value if input.checked? else obj[key] = value end end - page.driver.submit form['method'], form['action'], params + page.driver.submit form["method"], form["action"], params +end + +When(/^I click "(.*?)" and accept confirmation$/) do |link| + accept_confirm do + click_on(link) + end end -Then /^I should not see checkboxes in the table$/ do - page.should_not have_css ".paginated_collection table input[type=checkbox]" +Then(/^I should not see checkboxes in the table$/) do + expect(page).to have_no_css ".paginated-collection table input[type=checkbox]" end diff --git a/features/step_definitions/breadcrumb_steps.rb b/features/step_definitions/breadcrumb_steps.rb index 144a01e9baf..e423287f8c6 100644 --- a/features/step_definitions/breadcrumb_steps.rb +++ b/features/step_definitions/breadcrumb_steps.rb @@ -1,3 +1,14 @@ -Then /^I should see a link to "([^"]*)" in the breadcrumb$/ do |text| - page.should have_css('.breadcrumb > a', :text => text) +# frozen_string_literal: true +Around "@breadcrumb" do |scenario, block| + previous_breadcrumb = ActiveAdmin.application.breadcrumb + + begin + block.call + ensure + ActiveAdmin.application.breadcrumb = previous_breadcrumb + end +end + +Then(/^I should see a link to "([^"]*)" in the breadcrumb$/) do |text| + expect(page).to have_css "nav[aria-label=breadcrumb] a", text: text end diff --git a/features/step_definitions/comment_steps.rb b/features/step_definitions/comment_steps.rb index 669e9df7890..d59240f5a40 100644 --- a/features/step_definitions/comment_steps.rb +++ b/features/step_definitions/comment_steps.rb @@ -1,12 +1,35 @@ -Then /^I should see a comment by "([^"]*)"$/ do |name| - step %{I should see "#{name}" within ".active_admin_comment_author"} +# frozen_string_literal: true +Then(/^I should see a comment by "([^"]*)"$/) do |name| + step %{I should see "#{name}" within "[data-test-comment-container]"} end -When /^I add a comment "([^"]*)"$/ do |comment| - step %{I fill in "active_admin_comment_body" with "#{comment}"} - step %{I press "Add Comment"} +Then(/^I should( not)? be able to add a comment$/) do |negate| + should = negate ? :not_to : :to + expect(page).send should, have_button("Add Comment") end -Given /^a tag with the name "([^"]*)" exists$/ do |tag_name| - Tag.create(:name => tag_name) +When(/^I add a comment "([^"]*)"$/) do |comment| + step %{I fill in "comment_body" with "#{comment}"} + step %{I press "Add Comment"} +end + +Given(/^(a|\d+) comments added by admin with an email "([^"]+)"?$/) do |number, email| + number = number == "a" ? 1 : number.to_i + admin_user = ensure_user_created(email) + + comment_text = "Comment %i" + + number.times do |i| + ActiveAdmin::Comment.create!( + namespace: "admin", + body: comment_text % i, + resource_type: Post.to_s, + resource_id: Post.first.id, + author_type: admin_user.class.to_s, + author_id: admin_user.id) + end +end + +Then(/^I should see (\d+) comments?$/) do |number| + expect(page).to have_css("[data-test-comment-container]", count: number.to_i) end diff --git a/features/step_definitions/configuration_steps.rb b/features/step_definitions/configuration_steps.rb index 68d25a9b13e..8cc5c97fa96 100644 --- a/features/step_definitions/configuration_steps.rb +++ b/features/step_definitions/configuration_steps.rb @@ -1,93 +1,56 @@ +# frozen_string_literal: true module ActiveAdminReloading def load_aa_config(config_content) - ActiveAdmin::Event.dispatch ActiveAdmin::Application::BeforeLoadEvent, ActiveAdmin.application + ActiveSupport::Notifications.instrument ActiveAdmin::Application::BeforeLoadEvent, { active_admin_application: ActiveAdmin.application } eval(config_content) - ActiveAdmin::Event.dispatch ActiveAdmin::Application::AfterLoadEvent, ActiveAdmin.application + ActiveSupport::Notifications.instrument ActiveAdmin::Application::AfterLoadEvent, { active_admin_application: ActiveAdmin.application } Rails.application.reload_routes! - ActiveAdmin.application.namespaces.values.each &:reset_menu! - end -end - -module ActiveAdminContentsRollback - def files - @files ||= {} - end - - # Records the contents of a file the first time we are - # about to change it - def record(filename) - contents = File.read(filename) rescue nil - files[filename] = contents unless files.has_key? filename - end - - # Rolls the recorded files back to their original states - def rollback! - files.each{ |file, contents| rollback_file(file, contents) } - @files = {} - end - - # If the file originally had content, override the stuff on disk. - # Else, remove the file and its parent folder structure until Rails.root OR other files exist. - def rollback_file(file, contents) - if contents.present? - File.open(file,'w') { |f| f << contents } - else - File.delete(file) - begin - dir = File.dirname(file) - until dir == Rails.root - Dir.rmdir(dir) # delete current folder - dir = dir.split('/')[0..-2].join('/') # select parent folder - end - rescue Errno::ENOTEMPTY # Directory not empty - end - end + ActiveAdmin.application.namespaces.each(&:reset_menu!) end end World(ActiveAdminReloading) -World(ActiveAdminContentsRollback) - -After do - rollback! -end -Given /^a(?:n? (index|show))? configuration of:$/ do |action, config_content| +Given(/^a(?:n? (index|show))? configuration of:$/) do |action, config_content| load_aa_config(config_content) case action - when 'index' - step 'I am logged in' - step 'I am on the index page for posts' - when 'show' + when "index" + step "I am logged in" case resource = config_content.match(/ActiveAdmin\.register (\w+)/)[1] - when 'Post' - step 'I am logged in' - step 'I am on the index page for posts' + when "Post" + step "I am on the index page for posts" + when "Category" + step "I am on the index page for categories" + when "User" + step "I am on the index page for users" + else + # simplecov:disable + raise "#{resource} is not supported" + # simplecov:enable + end + when "show" + case resource = config_content.match(/ActiveAdmin\.register (\w+)/)[1] + when "Post" + step "I am logged in" + step "I am on the index page for posts" + step 'I follow "View"' + when "User" + step "I am logged in" + step "I am on the index page for users" + step 'I follow "View"' + when "Category" + step "I am logged in" + step "I am on the index page for categories" step 'I follow "View"' - when 'Tag' - step 'I am logged in' + when "Tag" + step "I am logged in" Tag.create! visit admin_tag_path Tag.last else + # simplecov:disable raise "#{resource} is not supported" + # simplecov:enable end end end - -Given /^"([^"]*)" contains:$/ do |filename, contents| - path = Rails.root + filename - FileUtils.mkdir_p File.dirname path - record path - - File.open(path,'w+'){ |f| f << contents } -end - -Given /^I add "([^"]*)" to the "([^"]*)" model$/ do |code, model_name| - path = File.join Rails.root, "app", "models", "#{model_name}.rb" - record path - - str = File.read(path).gsub /^(class .+)$/, "\\1\n #{code}\n" - File.open(path, 'w+') { |f| f << str } - ActiveSupport::Dependencies.clear -end diff --git a/features/step_definitions/dashboard_steps.rb b/features/step_definitions/dashboard_steps.rb deleted file mode 100644 index c37d9174be6..00000000000 --- a/features/step_definitions/dashboard_steps.rb +++ /dev/null @@ -1,15 +0,0 @@ -Then /^I should see the default welcome message$/ do - step %{I should see "Welcome to Active Admin" within "#dashboard_default_message"} -end - -Then /^I should not see the default welcome message$/ do - step %{I should not see "Welcome to Active Admin"} -end - -Then /^I should see a dashboard widget "([^"]*)"$/ do |name| - page.should have_css('.dashboard .panel h3', :text => name) -end - -Then /^I should not see a dashboard widget "([^"]*)"$/ do |name| - page.should_not have_css('.dashboard .panel h3', :text => name) -end diff --git a/features/step_definitions/factory_steps.rb b/features/step_definitions/factory_steps.rb index e25c1658b82..a97af4d9ce9 100644 --- a/features/step_definitions/factory_steps.rb +++ b/features/step_definitions/factory_steps.rb @@ -1,33 +1,46 @@ -def create_user(name, type = 'User') - type.camelize.constantize. - find_or_create_by_first_name_and_last_name *name.split(' '), :username => name.gsub(' ', '').underscore +# frozen_string_literal: true +def create_user(name, type = "User") + first_name, last_name = name.split(" ") + type.camelize.constantize.where(first_name: first_name, last_name: last_name).first_or_create(username: name.tr(" ", "").underscore) end -Given /^(a|\d+)( published)? posts?(?: with the title "([^"]*)")?(?: and body "([^"]*)")?(?: written by "([^"]*)")? exists?$/ do |count, published, title, body, user| - count = count == 'a' ? 1 : count.to_i - published = Time.now if published - author = create_user(user) if user - title ||= "Hello World %i" +Given(/^(a|\d+)( published)?( unstarred| starred)? posts?(?: with the title "([^"]*)")?(?: and body "([^"]*)")?(?: written by "([^"]*)")?(?: in category "([^"]*)")? exists?$/) do |count, published, starred, title, body, user, category_name| + count = count == "a" ? 1 : count.to_i + published = Time.now if published + starred = starred == " starred" if starred + author = create_user(user) if user + category = Category.where(name: category_name).first_or_create if category_name + title ||= "Hello World %i" count.times do |i| - Post.create! :title => title % i, :body => body, :author => author, :published_at => published + Post.create! title: title % i, body: body, author: author, published_date: published, custom_category_id: category.try(:id), starred: starred end end -Given /^a category named "([^"]*)" exists$/ do |name| - Category.create! :name => name +Given(/^a category named "([^"]*)" exists$/) do |name| + Category.create! name: name end -Given /^a (user|publisher) named "([^"]*)" exists$/ do |type, name| +Given(/^a (user|publisher) named "([^"]*)" exists$/) do |type, name| create_user name, type end -Given /^a store named "([^"]*)" exists$/ do |name| - Store.create! :name => name +Given(/^a store named "([^"]*)" exists$/) do |name| + Store.create! name: name end -Given /^I create a new post with the title "([^"]*)"$/ do |title| - click_link "Posts" - click_link "New Post" - fill_in :title, :with => title - click_button "Create Post" +Given(/^a tag named "([^"]*)" exists$/) do |name| + Tag.create! name: name +end + +Given(/^a company named "([^"]*)"(?: with a store named "([^"]*)")? exists$/) do |name, store_name| + store = Store.create! name: store_name if store_name + + Company.create! name: name, stores: [store].compact +end + +Given(/^I create a new post with the title "([^"]*)"$/) do |title| + first(:link, "Posts").click + click_on "New Post" + fill_in "post_title", with: title + click_on "Create Post" end diff --git a/features/step_definitions/filesystem_steps.rb b/features/step_definitions/filesystem_steps.rb new file mode 100644 index 00000000000..c7a3190e4e5 --- /dev/null +++ b/features/step_definitions/filesystem_steps.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true +module ActiveAdminContentsRollback + def files + @files ||= {} + end + + # Records the contents of a file the first time we are + # about to change it + def record(filename) + contents = File.read(filename) rescue nil + files[filename] = contents unless files.has_key? filename + end + + # Rolls the recorded files back to their original states + def rollback! + files.each { |file, contents| rollback_file(file, contents) } + @files = {} + end + + # If the file originally had content, override the stuff on disk. + # Else, remove the file and its parent folder structure until Rails.root OR other files exist. + def rollback_file(file, contents) + if contents.present? + File.open(file, "w") { |f| f << contents } + else + File.delete(file) + begin + dir = File.dirname(file) + until dir == Rails.root + Dir.rmdir(dir) # delete current folder + dir = dir.split("/")[0..-2].join("/") # select parent folder + end + rescue Errno::ENOTEMPTY # Directory not empty + end + end + end +end + +World(ActiveAdminContentsRollback) + +After "@changes-filesystem or @requires-reloading" do + rollback! +end + +Given(/^"([^"]*)" contains:$/) do |filename, contents| + path = Rails.root + filename + FileUtils.mkdir_p File.dirname path + record path + + File.open(path, "w+") { |f| f << contents } +end + +Given(/^I add "([^"]*)" to the "([^"]*)" model$/) do |code, model_name| + path = Rails.root.join "app", "models", "#{model_name}.rb" + record path + + str = File.read(path).gsub(/^(class .+)$/, "\\1\n #{code}\n") + File.open(path, "w+") { |f| f << str } +end diff --git a/features/step_definitions/filter_steps.rb b/features/step_definitions/filter_steps.rb index b9e6e2107dc..4ec225bf05d 100644 --- a/features/step_definitions/filter_steps.rb +++ b/features/step_definitions/filter_steps.rb @@ -1,17 +1,57 @@ -Then /^I should see a select filter for "([^"]*)"$/ do |label| - page.should have_css(".filter_select label", :text => label) +# frozen_string_literal: true +Around "@filters" do |scenario, block| + previous_current_filters = ActiveAdmin.application.current_filters + + begin + block.call + ensure + ActiveAdmin.application.current_filters = previous_current_filters + end +end + +Then(/^I should see a select filter for "([^"]*)"$/) do |label| + expect(page).to have_css ".filters-form-field.select label", text: label end -Then /^I should see a string filter for "([^"]*)"$/ do |label| - page.should have_css(".filter_string label", :text => label) +Then(/^I should see a string filter for "([^"]*)"$/) do |label| + expect(page).to have_css ".filters-form-field.string label", text: label end -Then /^I should see a date range filter for "([^"]*)"$/ do |label| - page.should have_css(".filter_date_range label", :text => label) +Then(/^I should see a date range filter for "([^"]*)"$/) do |label| + expect(page).to have_css ".filters-form-field.date_range label", text: label end -Then /^I should see the following filters:$/ do |table| +Then(/^I should see a number filter for "([^"]*)"$/) do |label| + expect(page).to have_css ".filters-form-field.numeric label", text: label +end + +Then(/^I should see the following filters:$/) do |table| table.rows_hash.each do |label, type| step %{I should see a #{type} filter for "#{label}"} end end + +Given(/^I add parameter "([^"]*)" with value "([^"]*)" to the URL$/) do |key, value| + url = page.current_url + separator = url.include?("?") ? "&" : "?" + visit url + separator + key.to_s + "=" + value.to_s +end + +Then(/^I should have parameter "([^"]*)" with value "([^"]*)"$/) do |key, value| + query = URI(page.current_url).query + params = Rack::Utils.parse_query query + expect(params[key]).to eq value +end + +Then(/^I should see current filter "([^"]*)" equal to "([^"]*)" with label "([^"]*)"$/) do |name, value, label| + expect(page).to have_css ".active-filters [data-filter='#{name}'] span", text: label + expect(page).to have_css ".active-filters [data-filter='#{name}'] strong", text: value +end + +Then(/^I should see current filter "([^"]*)" equal to "([^"]*)"$/) do |name, value| + expect(page).to have_css ".active-filters [data-filter='#{name}'] strong", text: value +end + +Then(/^I should see link "([^"]*)" in current filters/) do |label| + expect(page).to have_css ".active-filters [data-filter] strong a", text: label +end diff --git a/features/step_definitions/flash_steps.rb b/features/step_definitions/flash_steps.rb deleted file mode 100644 index cdb9c30e428..00000000000 --- a/features/step_definitions/flash_steps.rb +++ /dev/null @@ -1,11 +0,0 @@ -Then /^I should see a flash with "([^"]*)"$/ do |text| - page.should have_content(text) -end - -Then /^I should see a successful create flash$/ do - page.should have_css('div.flash_notice', :text => /was successfully created/) -end - -Then /^I should not see a successful create flash$/ do - page.should_not have_css('div.flash_notice', :text => /was successfully created/) -end diff --git a/features/step_definitions/format_steps.rb b/features/step_definitions/format_steps.rb index 3412a0b9f9e..ad7f3660499 100644 --- a/features/step_definitions/format_steps.rb +++ b/features/step_definitions/format_steps.rb @@ -1,52 +1,67 @@ -CSVLib = if RUBY_VERSION =~ /^1.8/ - require 'fastercsv' - FasterCSV - else - require 'csv' - CSV - end +# frozen_string_literal: true +require "csv" + +Around "@csv" do |scenario, block| + default_csv_options = ActiveAdmin.application.csv_options + default_disable_streaming_in = ActiveAdmin.application.disable_streaming_in + + begin + block.call + ensure + ActiveAdmin.application.disable_streaming_in = default_disable_streaming_in + ActiveAdmin.application.csv_options = default_csv_options + end +end Then "I should see nicely formatted datetimes" do - page.body.should =~ /\w+ \d{1,2}, \d{4} \d{2}:\d{2}/ + expect(page.body).to match(/\w+ \d{1,2}, \d{4} \d{2}:\d{2}/) end -Then /^I should( not)? see a link to download "([^"]*)"$/ do |negate, format| - method = negate ? :should_not : :should - page.send method, have_css("#index_footer a", :text => format) +Then(/^I should( not)? see a link to download "([^"]*)"$/) do |negate, format| + method = negate ? :to_not : :to + expect(page).send method, have_css("a", text: format) end # Check first rows of the displayed CSV. -Then /^I should download a CSV file with "([^"]*)" separator for "([^"]*)" containing:$/ do |sep, resource_name, table| - body = page.driver.response.body - headers = page.response_headers - headers['Content-Type'].should eq 'text/csv; charset=utf-8' - headers['Content-Disposition'].should eq %{attachment; filename="#{resource_name}-#{Time.now.strftime("%Y-%m-%d")}.csv"} +Then(/^I should download a CSV file with "([^"]*)" separator for "([^"]*)" containing:$/) do |sep, resource_name, table| + body = page.driver.response.body + content_type_header, content_disposition_header, last_modified_header = %w[Content-Type Content-Disposition Last-Modified].map do |header_name| + page.response_headers[header_name] + end + expect(content_type_header).to eq "text/csv; charset=utf-8" + expect(content_disposition_header).to match(/\Aattachment; filename=".+?\.csv"\z/) + expect(last_modified_header).to_not be_nil + expect(Date.strptime(last_modified_header, "%a, %d %b %Y %H:%M:%S GMT")).to be_a(Date) - begin - csv = CSVLib.parse(body, :col_sep => sep) - table.raw.each_with_index do |expected_row, row_index| - expected_row.each_with_index do |expected_cell, col_index| - cell = csv.try(:[], row_index).try(:[], col_index) - if expected_cell.blank? - cell.should be_nil - else - (cell || '').should match /#{expected_cell}/ - end + csv = CSV.parse(body, col_sep: sep) + table.raw.each_with_index do |expected_row, row_index| + expected_row.each_with_index do |expected_cell, col_index| + cell = csv.try(:[], row_index).try(:[], col_index) + if expected_cell.blank? + expect(cell).to eq nil + else + expect(cell || "").to match(/#{expected_cell}/) end end - rescue - puts "Expecting:" - p table.raw - puts "to match:" - p csv - raise $! end end -Then /^I should download a CSV file for "([^"]*)" containing:$/ do |resource_name, table| +Then(/^I should download a CSV file for "([^"]*)" containing:$/) do |resource_name, table| step %{I should download a CSV file with "," separator for "#{resource_name}" containing:}, table end -Then /^the CSV file should contain "([^"]*)" in quotes$/ do |text| - page.driver.response.body.should match /"#{text}"/ +Then(/^the CSV file should contain "([^"]*)" in quotes$/) do |text| + expect(page.driver.response.body).to match(/"#{text}"/) +end + +Then(/^the encoding of the CSV file should be "([^"]*)"$/) do |text| + expect(page.driver.response.body.encoding).to be Encoding.find(Encoding.aliases[text] || text) +end + +Then(/^the CSV file should start with BOM$/) do + expect(page.driver.response.body.bytes).to start_with(239, 187, 191) +end + +Then(/^access denied$/) do + expect(page).to have_text(I18n.t("active_admin.access_denied.message")) end diff --git a/features/step_definitions/i18n_steps.rb b/features/step_definitions/i18n_steps.rb index bc93daf0ea1..de9dd3f19cf 100644 --- a/features/step_definitions/i18n_steps.rb +++ b/features/step_definitions/i18n_steps.rb @@ -1,3 +1,4 @@ -When /^I set my locale to "([^"]*)"$/ do |lang| +# frozen_string_literal: true +When(/^I set my locale to "([^"]*)"$/) do |lang| I18n.locale = lang end diff --git a/features/step_definitions/index_scope_steps.rb b/features/step_definitions/index_scope_steps.rb index 78c0b487fd4..e785a0e2383 100644 --- a/features/step_definitions/index_scope_steps.rb +++ b/features/step_definitions/index_scope_steps.rb @@ -1,21 +1,39 @@ -Then /^I should( not)? see the scope "([^"]*)"( selected)?$/ do |negate, name, selected| +# frozen_string_literal: true +Then(/^I should( not)? see the scope "([^"]*)"( selected)?$/) do |negate, name, selected| should = "I should#{' not' if negate}" - scope = ".scopes#{' .selected' if selected}" + scope = ".scopes#{' .index-button-selected' if selected}" step %{#{should} see "#{name}" within "#{scope}"} end -Then /^I should see the scope "([^"]*)" not selected$/ do |name| +Then(/^I should see the scope "([^"]*)" not selected$/) do |name| step %{I should see the scope "#{name}"} - page.should_not have_css('.scopes .selected', :text => name) + expect(page).to have_no_css ".scopes .index-button-selected", text: name end -Then /^I should see the scope "([^"]*)" with the count (\d+)$/ do |name, count| - name = name.gsub(' ','').underscore.downcase - step %{I should see "#{count}" within ".scopes .#{name} .count"} +Then(/^I should see the scope "([^"]*)" with the count (\d+)$/) do |name, count| + expect(page).to have_css ".scopes a", text: name + expect(page).to have_css ".scopes-count", text: count end -Then /^I should see the scope "([^"]*)" with no count$/ do |name| - name = name.gsub(" ", "").underscore.downcase - page.should have_css(".scopes .#{name}") - page.should_not have_css(".scopes .#{name} .count") +Then(/^I should see the scope with label "([^"]*)"$/) do |label| + expect(page).to have_link(label) +end + +Then(/^I should see the scope "([^"]*)" with no count$/) do |name| + expect(page).to have_css ".scopes a", text: name + expect(page).to have_no_css ".scopes-count" +end + +Then "I should see a group {string} with the scopes {string} and {string}" do |group, name1, name2| + group = group.tr(" ", "").underscore.downcase + expect(page).to have_css ".scopes [data-group='#{group}'] a", text: name1 + expect(page).to have_css ".scopes [data-group='#{group}'] a", text: name2 +end + +Then "I should see a default group with a single scope {string}" do |name| + expect(page).to have_css ".scopes [data-group=default] a", text: name +end + +Then "I should not see any scopes" do + expect(page).to have_no_css ".scopes a" end diff --git a/features/step_definitions/index_views_steps.rb b/features/step_definitions/index_views_steps.rb deleted file mode 100644 index 9827e852b7a..00000000000 --- a/features/step_definitions/index_views_steps.rb +++ /dev/null @@ -1,3 +0,0 @@ -When /^I click "(.*?)"$/ do |link| - click_link(link) -end diff --git a/features/step_definitions/layout_steps.rb b/features/step_definitions/layout_steps.rb deleted file mode 100644 index 28ff1211f0f..00000000000 --- a/features/step_definitions/layout_steps.rb +++ /dev/null @@ -1,3 +0,0 @@ -Then /^I should see the Active Admin layout$/ do - page.should have_css("#active_admin_content #main_content_wrapper") -end diff --git a/features/step_definitions/member_link_steps.rb b/features/step_definitions/member_link_steps.rb deleted file mode 100644 index 8e044dd63a9..00000000000 --- a/features/step_definitions/member_link_steps.rb +++ /dev/null @@ -1,7 +0,0 @@ -Then /^I should see an action item button "([^"]*)"$/ do |content| - page.should have_css(".action_items a", :text => content) -end - -Then /^I should not see an action item button "([^"]*)"$/ do |content| - page.should_not have_css(".action_items", :text => content) -end diff --git a/features/step_definitions/menu_steps.rb b/features/step_definitions/menu_steps.rb index 6568fa9cdc4..f010f336aff 100644 --- a/features/step_definitions/menu_steps.rb +++ b/features/step_definitions/menu_steps.rb @@ -1,11 +1,24 @@ -Then /^I should see a menu item for "([^"]*)"$/ do |name| - page.should have_css('#tabs li a', :text => name) +# frozen_string_literal: true +Then(/^I should see a menu item for "([^"]*)"$/) do |name| + expect(page).to have_css "#main-menu li a", text: name end -Then /^I should not see a menu item for "([^"]*)"$/ do |name| - page.should_not have_css('#tabs li a', :text => name) +Then(/^I should not see a menu item for "([^"]*)"$/) do |name| + expect(page).to have_no_css "#main-menu li a", text: name end -Then /^I should see a nested menu item for "([^"]*)"$/ do |name| - page.should have_css('#tabs > li > ul > li > a', :text => name) +Then(/^the "([^"]*)" menu item should be hidden$/) do |name| + expect(page).to have_css "#main-menu .hidden a", text: name +end + +Then(/^I should see a menu parent for "([^"]*)"$/) do |name| + expect(page).to have_css "#main-menu li button, #main-menu li a", text: name +end + +Then(/^I should see a nested menu item for "([^"]*)"$/) do |name| + expect(page).to have_css "#main-menu li li a", text: name +end + +Then(/^the "([^"]*)" menu item should be selected$/) do |name| + expect(page).to have_css "#main-menu li a.selected", text: name end diff --git a/features/step_definitions/pagination_steps.rb b/features/step_definitions/pagination_steps.rb index ed86dcc7988..33a15772da8 100644 --- a/features/step_definitions/pagination_steps.rb +++ b/features/step_definitions/pagination_steps.rb @@ -1,8 +1,16 @@ -Then /^I should not see pagination$/ do - page.should_not have_css(".pagination") +# frozen_string_literal: true +Then(/^I should not see pagination$/) do + expect(page).to have_no_css "[data-test-pagination]" end -Then /^I should see pagination with (\d+) pages$/ do |count| - step %{I should see "#{count}" within ".pagination a"} - step %{I should not see "#{count.to_i + 1}" within ".pagination a"} +Then(/^I should see pagination page (\d+) link$/) do |num| + expect(page).to have_css "[data-test-pagination] a", text: num, count: 1 +end + +Then(/^I should see the pagination "Next" link/) do + expect(page).to have_css "[data-test-pagination] a", text: "Next" +end + +Then(/^I should not see the pagination "Next" link/) do + expect(page).to have_no_css "[data-test-pagination] a", text: "Next" end diff --git a/features/step_definitions/root_steps.rb b/features/step_definitions/root_steps.rb new file mode 100644 index 00000000000..dc035569571 --- /dev/null +++ b/features/step_definitions/root_steps.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true +Around "@root" do |scenario, block| + previous_root = ActiveAdmin.application.root_to + + begin + block.call + ensure + ActiveAdmin.application.root_to = previous_root + end +end diff --git a/features/step_definitions/sidebar_steps.rb b/features/step_definitions/sidebar_steps.rb deleted file mode 100644 index 007dc1f980d..00000000000 --- a/features/step_definitions/sidebar_steps.rb +++ /dev/null @@ -1,8 +0,0 @@ -Then /^I should see a sidebar titled "([^"]*)"$/ do |title| - page.should have_css(".sidebar_section h3", :text => title) -end - -Then /^I should not see a sidebar titled "([^"]*)"$/ do |title| - title = title.gsub(' ', '').underscore - page.all(:css, "##{title}_sidebar_section").count.should == 0 -end diff --git a/features/step_definitions/site_title_steps.rb b/features/step_definitions/site_title_steps.rb index bff3734c6a2..601281b5808 100644 --- a/features/step_definitions/site_title_steps.rb +++ b/features/step_definitions/site_title_steps.rb @@ -1,15 +1,14 @@ -Then /^I should see the site title "([^"]*)"$/ do |title| - page.should have_css('h1#site_title', :text => title) -end - -Then /^I should not see the site title "([^"]*)"$/ do |title| - page.should_not have_css('h1#site_title', :text => title) -end +# frozen_string_literal: true +Around "@site_title" do |scenario, block| + previous_site_title = ActiveAdmin.application.site_title -Then /^I should see the site title image "([^"]*)"$/ do |image| - page.should have_css('h1#site_title img', :src => image) + begin + block.call + ensure + ActiveAdmin.application.site_title = previous_site_title + end end -Then /^I should see the site title image linked to "([^"]*)"$/ do |url| - page.should have_css('h1#site_title a', :href => url) +Then(/^I should see the site title "([^"]*)"$/) do |title| + expect(page).to have_css "[data-test-site-title]", text: title end diff --git a/features/step_definitions/tab_steps.rb b/features/step_definitions/tab_steps.rb deleted file mode 100644 index fef7a72fb6b..00000000000 --- a/features/step_definitions/tab_steps.rb +++ /dev/null @@ -1,3 +0,0 @@ -Then /^the "([^"]*)" tab should be selected$/ do |name| - step %{I should see "#{name}" within "ul#tabs li.current"} -end diff --git a/features/step_definitions/table_steps.rb b/features/step_definitions/table_steps.rb index 0ec372ce210..5a7b15860fe 100644 --- a/features/step_definitions/table_steps.rb +++ b/features/step_definitions/table_steps.rb @@ -1,5 +1,18 @@ -Then /^I should see (\d+) ([\w]*) in the table$/ do |count, resource_type| - all("table.index_table tr > td:first").count.should eq count.to_i +# frozen_string_literal: true +Then(/^I should see (\d+) ([\w]*) in the table$/) do |count, resource_type| + expect(page).to have_css(".data-table tr > td:first-child", count: count.to_i) +end + +Then("I should see {string} in the table") do |string| + expect(page).to have_css(".data-table tr > td", text: string) +end + +Then("I should not see {string} in the table") do |string| + expect(page).to have_no_css(".data-table tr > td", text: string) +end + +Then(/^I should see an id_column link to edit page$/) do + expect(page).to have_css(".data-table a[href*='/edit']", text: /^\d+$/) end # TODO: simplify this, if possible? @@ -12,7 +25,7 @@ def initialize(html, table_css_selector = "table") def to_array rows = Nokogiri::HTML(@html).css("#{@selector} tr") rows.map do |row| - row.css('th, td').map do |td| + row.css("th, td").map do |td| cell_to_string(td) end end @@ -22,44 +35,28 @@ def to_array def cell_to_string(td) str = "" - input = td.css('input').last + input = td.css("input").last if input - str << input_to_string(input) + str += input_to_string(input) end - str << td.content.strip.gsub("\n", ' ') + str + td.content.strip.tr("\n", " ") end def input_to_string(input) case input.attribute("type").value when "checkbox" - if input.attribute("disabled") - "_" - else - if input.attribute("checked") - "[X]" - else - "[ ]" - end - end - when "text" - if input.attribute("value").present? - "[#{input.attribute("value")}]" - else - "[ ]" - end - when "submit" - input.attribute("value") + "[ ]" else + # simplecov:disable raise "I don't know what to do with #{input}" + # simplecov:enable end end end module TableMatchHelper - - # @param table [Array[Array]] # @param expected_table [Array[Array[String]]] # The expected_table values are String. They are converted to @@ -78,30 +75,30 @@ def assert_tables_match(table, expected_table) begin assert_cells_match(cell, expected_cell) rescue + # simplecov:disable puts "Cell at line #{row_index} and column #{column_index}: #{cell.inspect} does not match #{expected_cell.inspect}" puts "Expecting:" table.each { |row| puts row.inspect } puts "to match:" expected_table.each { |row| puts row.inspect } raise $! + # simplecov:enable end end end end def assert_cells_match(cell, expected_cell) - if expected_cell =~ /^\/.*\/$/ - cell.should match(Regexp.new(expected_cell[1..-2])) + if /^\/.*\/$/.match?(expected_cell) + expect(cell).to match(/#{expected_cell[1..-2]}/) else - (cell || "").strip.should == expected_cell + expect((cell || "").strip).to eq expected_cell end end - -end # module TableMatchHelper +end World(TableMatchHelper) - # Usage: # # I should see the "invoices" table: @@ -109,8 +106,8 @@ def assert_cells_match(cell, expected_cell) # | /\d+/ | 27/01/12 | $30.00 | # | /\d+/ | 12/02/12 | $25.00 | # -Then /^I should see the "([^"]*)" table:$/ do |table_id, expected_table| - page.should have_css("table##{table_id}") +Then(/^I should see the "([^"]*)" table:$/) do |table_id, expected_table| + expect(page).to have_css "table##{table_id}" assert_tables_match( HtmlTableToTextHelper.new(page.body, "table##{table_id}").to_array, diff --git a/features/step_definitions/user_steps.rb b/features/step_definitions/user_steps.rb index f3d606f18cb..1c39a6d6025 100644 --- a/features/step_definitions/user_steps.rb +++ b/features/step_definitions/user_steps.rb @@ -1,40 +1,47 @@ +# frozen_string_literal: true def ensure_user_created(email) - user = AdminUser.find_or_create_by_email :email => email, - :password => 'password', :password_confirmation => 'password' - - unless user.persisted? - raise "Could not create user #{email}: #{user.errors.full_messages}" - end - user + AdminUser.create_with(password: "password", password_confirmation: "password").find_or_create_by!(email: email) end -Given /^(?:I am logged|log) out$/ do - click_link 'Logout' if page.all(:css, "a", :text => 'Logout').any? +Given(/^(?:I am logged|log) out$/) do + click_on "Sign out" if page.all(:css, "a", text: "Sign out").any? end -Given /^I am logged in$/ do - step 'log out' - login_as ensure_user_created 'admin@example.com' +Given(/^I am logged in$/) do + logout(:admin_user) + login_as ensure_user_created("admin@example.com"), scope: :admin_user end -# only for @requires-reloading scenario -Given /^I am logged in with capybara$/ do - ensure_user_created 'admin@example.com' - step 'log out' +Given(/^I am logged in with capybara$/) do + ensure_user_created "admin@example.com" + step "log out" visit new_admin_user_session_path - fill_in 'Email', :with => 'admin@example.com' - fill_in 'Password', :with => 'password' - click_button 'Login' + fill_in "Email", with: "admin@example.com" + fill_in "Password", with: "password" + click_on "Sign In" end -Given /^an admin user "([^"]*)" exists$/ do |email| +Given(/^an admin user "([^"]*)" exists$/) do |email| ensure_user_created(email) end -Given /^an admin user "([^"]*)" exists with( expired)? reset password token "(.*?)"$/ do |email, expired, token| - user = ensure_user_created(email) - user.reset_password_token = token - user.reset_password_sent_at = 1.minute.ago unless expired - user.save +Given(/^"([^"]*)" requests a password reset with token "([^"]*)"( but it expires)?$/) do |email, token, expired| + visit new_admin_user_password_path + fill_in "Email", with: email + allow(Devise).to receive(:friendly_token).and_return(token) + click_on "Reset My Password" + + AdminUser.where(email: email).first.update_attribute :reset_password_sent_at, 1.month.ago if expired +end + +Given(/^override locale "([^"]*)" with "([^"]*)"$/) do |path, value| + keys_value = path.split(".") + [value] + locale_hash = keys_value.reverse.inject { |a, n| { n => a } } + I18n.available_locales + I18n.backend.store_translations(I18n.locale, locale_hash) +end + +When(/^I fill in the password field with "([^"]*)"$/) do |password| + fill_in "admin_user_password", with: password end diff --git a/features/step_definitions/web_steps.rb b/features/step_definitions/web_steps.rb index 4ac1163bd74..01a64f12ae3 100644 --- a/features/step_definitions/web_steps.rb +++ b/features/step_definitions/web_steps.rb @@ -1,82 +1,136 @@ -require 'uri' -require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "paths")) -require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "selectors")) +# frozen_string_literal: true +require "uri" +require File.expand_path(File.join(__dir__, "..", "support", "paths")) module WithinHelpers def with_scope(locator) locator ? within(*selector_for(locator)) { yield } : yield end + + private + + def selector_for(locator) + case locator + + # Add more mappings here. + # Here is an example that pulls values out of the Regexp: + # + # when /^the (notice|error|info) flash$/ + # ".flash.#{$1}" + + # You can also return an array to use a different selector + # type, like: + # + # when /the header/ + # [:xpath, "//header"] + + when /^the "([^"]*)" sidebar$/ + [:css, "##{$1.tr(" ", '').underscore}_sidebar_section"] + + # This allows you to provide a quoted selector as the scope + # for "within" steps as was previously the default for the + # web steps: + when /^"(.+)"$/ + $1 + + else + # simplecov:disable + raise "Can't find mapping from \"#{locator}\" to a selector.\n" + + "Now, go and add a mapping in #{__FILE__}" + # simplecov:enable + end + end end World(WithinHelpers) -# Single-line step scoper -When /^(.*) within (.*[^:])$/ do |step_name, parent| +When(/^(.*) within (.*)$/) do |step_name, parent| with_scope(parent) { step step_name } end -# Multi-line step scoper -When /^(.*) within (.*[^:]):$/ do |step_name, parent, table_or_string| - with_scope(parent) { step "#{step_name}:", table_or_string } +Given(/^I am on (.+)$/) do |page_name| + visit path_to(page_name) end -Given /^(?:I )am on (.+)$/ do |page_name| +When(/^I go to (.+)$/) do |page_name| visit path_to(page_name) end -When /^(?:I )go to (.+)$/ do |page_name| - visit path_to(page_name) +When(/^I visit (.+) twice$/) do |page_name| + 2.times { visit path_to(page_name) } end -When /^(?:I )press "([^"]*)"$/ do |button| - click_button(button) +When(/^I press "([^"]*)"$/) do |button| + click_on(button) end -When /^(?:I )follow "([^"]*)"$/ do |link| - click_link(link) +When(/^I follow "([^"]*)"$/) do |link| + first(:link, link).click end -When /^(?:I )fill in "([^"]*)" with "([^"]*)"$/ do |field, value| - fill_in(field, :with => value) +When(/^I click "(.*?)"$/) do |link| + click_on(link) end -When /^(?:I )select "([^"]*)" from "([^"]*)"$/ do |value, field| - select(value, :from => field) +When(/^I fill in "([^"]*)" with "([^"]*)"$/) do |field, value| + fill_in(field, with: value) end -When /^(?:I )(check|uncheck|choose) "([^"]*)"$/ do |action, field| +When(/^I select "([^"]*)" from "([^"]*)"$/) do |value, field| + select(value, from: field) +end + +When(/^I (check|uncheck) "([^"]*)"$/) do |action, field| send action, field end -When /^(?:I )attach the file "([^"]*)" to "([^"]*)"$/ do |path, field| - attach_file(field, File.expand_path(path)) +Then(/^I should( not)? see( the element)? "([^"]*)"$/) do |negate, is_css, text| + should = negate ? :not_to : :to + have = is_css ? have_css(text) : have_text(text) + expect(page).send should, have +end + +Then(/^I should see the select "([^"]*)" with options "([^"]+)"?$/) do |label, with_options| + expect(page).to have_select(label, with_options: with_options.split(", ")) end -Then /^(?:I )should( not)? see( the element)? "([^"]*)"$/ do |negate, is_css, text| - should = negate ? :should_not : :should - have = is_css ? have_css(text) : have_content(text) - page.send should, have +Then(/^I should see the field "([^"]*)" of type "([^"]+)"?$/) do |label, of_type| + expect(page).to have_field(label, type: of_type) end -Then /^the "([^"]*)" field(?: within (.*))? should( not)? contain "([^"]*)"$/ do |field, parent, negate, value| +Then(/^the "([^"]*)" field(?: within (.*))? should contain "([^"]*)"$/) do |field, parent, value| with_scope(parent) do field = find_field(field) - field_value = (field.tag_name == 'textarea') ? field.text : field.value - negate ? field_value.should_not =~ /#{value}/ : field_value.should =~ /#{value}/ + value = field.tag_name == "textarea" ? field.text : field.value + + expect(value).to match(/#{value}/) end end -Then /^the "([^"]*)" checkbox(?: within (.*))? should( not)? be checked$/ do |label, parent, negate| +Then(/^the "([^"]*)" select(?: within (.*))? should have "([^"]+)" selected$/) do |label, parent, option| with_scope(parent) do - field_checked = find_field(label)['checked'] - field_checked.should negate ? be_false : be_true + expect(page).to have_select(label, selected: option) end end -Then /^(?:|I )should be on (.+)$/ do |page_name| - current_path = URI.parse(current_url).path - current_path.should == path_to(page_name) +Then(/^the "([^"]*)" checkbox(?: within (.*))? should( not)? be checked$/) do |label, parent, negate| + with_scope(parent) do + checkbox = find_field(label) + if negate + expect(checkbox).not_to be_checked + else + expect(checkbox).to be_checked + end + end +end + +Then(/^I should be on (.+)$/) do |page_name| + expect(URI.parse(current_url).path).to eq path_to page_name +end + +Then(/^I should see content "(.*?)" above other content "(.*?)"$/) do |top_title, bottom_title| + expect(page).to have_css %Q(div:contains('#{top_title}') + div:contains('#{bottom_title}')) end -Then /^show me the page$/ do - save_and_open_page +Then(/^I should see a flash with "([^"]*)"$/) do |text| + expect(page).to have_text text end diff --git a/features/sti_resource.feature b/features/sti_resource.feature index 3c669c864f6..54d0929c442 100644 --- a/features/sti_resource.feature +++ b/features/sti_resource.feature @@ -6,8 +6,12 @@ Feature: STI Resource Given I am logged in And a configuration of: """ - ActiveAdmin.register Publisher - ActiveAdmin.register User + ActiveAdmin.register Publisher do + permit_params :first_name, :last_name, :username, :age, :reason_of_sign_in + end + ActiveAdmin.register User do + permit_params :first_name, :last_name, :username, :age, :reason_of_sign_in + end """ Scenario: Create, update and delete a child STI resource diff --git a/features/strong_parameters.feature b/features/strong_parameters.feature new file mode 100644 index 00000000000..51e527cf28b --- /dev/null +++ b/features/strong_parameters.feature @@ -0,0 +1,69 @@ +@silent_unpermitted_params_failure +Feature: Strong Params + + Background: + Given a category named "Music" exists + And a user named "John Doe" exists + And a post with the title "Hello World" written by "John Doe" exists + And I am logged in + And a configuration of: + """ + ActiveAdmin.register Post do + end + """ + And I am on the index page for posts + + Scenario: Static permitted parameters + Given a configuration of: + """ + ActiveAdmin.register Post do + permit_params :author, :title, :starred + end + """ + When I follow "Edit" + + And I fill in "Title" with "Hello World from update" + And I check "Starred" + And I press "Update Post" + Then I should see "Post was successfully updated." + And I should see the attribute "Title" with "Hello World from update" + And I should see the attribute "Author" with "John Doe" + And I should see the attribute "Starred" with "Yes" + + Scenario: Dynamic permitted parameters + Given a configuration of: + """ + ActiveAdmin.register Post do + permit_params do + allow = [:author, :title] + allow << :starred + allow + end + end + """ + When I follow "Edit" + + And I fill in "Title" with "Hello World from update" + And I check "Starred" + And I press "Update Post" + Then I should see "Post was successfully updated." + And I should see the attribute "Title" with "Hello World from update" + And I should see the attribute "Author" with "John Doe" + And I should see the attribute "Starred" with "Yes" + + Scenario: Should not update parameters that are not declared as permitted + Given a configuration of: + """ + ActiveAdmin.register Post do + permit_params :author, :title + end + """ + When I follow "Edit" + + And I fill in "Title" with "Hello World from update" + And I check "Starred" + And I press "Update Post" + Then I should see "Post was successfully updated." + And I should see the attribute "Title" with "Hello World from update" + And I should see the attribute "Author" with "John Doe" + And I should see the attribute "Starred" with "Unknown" diff --git a/features/support/env.rb b/features/support/env.rb index d269e7aa0a5..82754c7f448 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -1,118 +1,113 @@ -# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril. -# It is recommended to regenerate this file in the future when you upgrade to a -# newer version of cucumber-rails. Consider adding your own code to a new file -# instead of editing this one. Cucumber will automatically load all features/**/*.rb -# files. +# frozen_string_literal: true +ENV["RAILS_ENV"] = "test" -ENV["RAILS_ENV"] ||= "cucumber" -ENV['BUNDLE_GEMFILE'] = File.expand_path('../../../Gemfile', __FILE__) -require "rubygems" -require "bundler" -Bundler.setup +require_relative "../../spec/support/simplecov_helper" -require File.expand_path('../../../spec/support/detect_rails_version', __FILE__) -ENV["RAILS"] = detect_rails_version +ActiveAdmin::TestSupport::SimpleCovHelper.start("features") -ENV['RAILS_ROOT'] = File.expand_path("../../../spec/rails/rails-#{ENV["RAILS"]}", __FILE__) +Dir["#{File.expand_path('../step_definitions', __dir__)}/*.rb"].each do |f| + require f +end + +require_relative "../../tasks/test_application" + +require "#{ActiveAdmin::TestApplication.new.full_app_dir}/config/environment.rb" + +require_relative "rails" +require_relative "../../spec/support/active_support_deprecation" +require "rspec/mocks" +World(RSpec::Mocks::ExampleMethods) -# Create the test app if it doesn't exists -unless File.exists?(ENV['RAILS_ROOT']) - system 'rake setup' +Around "@mocks" do |scenario, block| + RSpec::Mocks.setup + + block.call + + begin + RSpec::Mocks.verify + ensure + RSpec::Mocks.teardown + end +end + +After "@debug" do |scenario| + # simplecov:disable + save_and_open_page if scenario.failed? + # simplecov:enable end -# Ensure the Active Admin load path is happy -require 'rails' -require 'active_admin' -ActiveAdmin.application.load_paths = [ENV['RAILS_ROOT'] + "/app/admin"] +require "capybara/cuprite" -require ENV['RAILS_ROOT'] + '/config/environment' +Capybara.register_driver(:cuprite) do |app| + Capybara::Cuprite::Driver.new(app, process_timeout: 30, timeout: 30) +end + +Capybara.javascript_driver = :cuprite -# Setup autoloading of ActiveAdmin and the load path -$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) -autoload :ActiveAdmin, 'active_admin' +Capybara.server = :webrick -require 'cucumber/rails' +Capybara.asset_host = "http://localhost:3000" -require 'capybara/rails' -require 'capybara/cucumber' -require 'capybara/session' # Capybara defaults to XPath selectors rather than Webrat's default of CSS3. In # order to ease the transition to Capybara we set the default here. If you'd # prefer to use XPath just remove this line and adjust any selectors in your # steps to use the XPath syntax. Capybara.default_selector = :css -# If you set this to false, any error raised from within your app will bubble -# up to your step definition and out to cucumber unless you catch it somewhere -# on the way. You can make Rails rescue errors and render error pages on a -# per-scenario basis by tagging a scenario or feature with the @allow-rescue tag. -# -# If you set this to true, Rails will rescue all errors and render error -# pages, more or less in the same way your application would behave in the -# default production environment. It's not recommended to do this for all -# of your scenarios, as this makes it hard to discover errors in your application. -ActionController::Base.allow_rescue = false - -# If you set this to true, each scenario will run in a database transaction. -# You can still turn off transactions on a per-scenario basis, simply tagging -# a feature or scenario with the @no-txn tag. If you are using Capybara, -# tagging with @culerity or @javascript will also turn transactions off. -# -# If you set this to false, transactions will be off for all scenarios, -# regardless of whether you use @no-txn or not. -# -# Beware that turning transactions off will leave data in your database -# after each scenario, which can lead to hard-to-debug failures in -# subsequent scenarios. If you do this, we recommend you create a Before -# block that will explicitly put your database in a known state. -Cucumber::Rails::World.use_transactional_fixtures = false -# How to clean your database when transactions are turned off. See -# http://github.com/bmabey/database_cleaner for more info. -if defined?(ActiveRecord::Base) - begin - require 'database_cleaner' - require 'database_cleaner/cucumber' - DatabaseCleaner.strategy = :truncation - rescue LoadError => ignore_if_database_cleaner_not_present - end -end +# Database resetting strategy +DatabaseCleaner.strategy = :truncation +Cucumber::Rails::Database.javascript_strategy = :truncation # Warden helpers to speed up login -# See https://github.com/plataformatec/devise/wiki/How-To:-Test-with-Capybara +# See https://github.com/heartcombo/devise/wiki/How-To:-Test-with-Capybara include Warden::Test::Helpers +Devise.configure_warden! After do Warden.test_reset! +end + +Before do + # Reload Active Admin + ActiveAdmin.unload! + ActiveAdmin.load! +end +# Force deprecations to raise an exception. +ActiveAdmin::DeprecationHelper.behavior = :raise + +After "@authorization" do |scenario, block| # Reset back to the default auth adapter ActiveAdmin.application.namespace(:admin). authorization_adapter = ActiveAdmin::AuthorizationAdapter end -Before do +Around "@silent_unpermitted_params_failure" do |scenario, block| + original = ActionController::Parameters.action_on_unpermitted_parameters begin - # We are caching classes, but need to manually clear references to - # the controllers. If they aren't clear, the router stores references - ActiveSupport::Dependencies.clear - - # Reload Active Admin - ActiveAdmin.unload! - ActiveAdmin.load! - rescue - p $! - raise $! + ActionController::Parameters.action_on_unpermitted_parameters = false + block.call + ensure + ActionController::Parameters.action_on_unpermitted_parameters = original end end -# improve the performance of the specs suite by not logging anything -# see http://blog.plataformatec.com.br/2011/12/three-tips-to-improve-the-performance-of-your-test-suite/ -Rails.logger.level = 4 +Around "@locale_manipulation" do |scenario, block| + I18n.with_locale(:en, &block) +end + +class CustomIndexView < ActiveAdmin::Component + def build(page_presenter, collection) + add_class "custom-index-view" + resource_selection_toggle_panel if active_admin_config.batch_actions.any? + collection.each do |obj| + instance_exec(obj, &page_presenter.block) + end + end -# Improves performance by forcing the garbage collector to run less often. -unless ENV['DEFER_GC'] == '0' || ENV['DEFER_GC'] == 'false' - require File.expand_path('../../../spec/support/deferred_garbage_collection', __FILE__) - Before { DeferredGarbageCollection.start } - After { DeferredGarbageCollection.reconsider } + def self.index_name + "custom" + end end diff --git a/features/support/paths.rb b/features/support/paths.rb index c78de34ff33..e240a512616 100644 --- a/features/support/paths.rb +++ b/features/support/paths.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module NavigationHelpers # Maps a name to a path. Used by the # @@ -8,37 +9,51 @@ module NavigationHelpers def path_to(page_name) case page_name - when /the home\s?page/ - '/' when /the dashboard/ "/admin" when /the new post page/ "/admin/posts/new" - when /the admin password reset form with reset password token "([^"]*)"/ + when /the login page/ + "/admin/login" + when /the first post custom status page/ + "/admin/posts/1/status" + when /the last posts page/ + "/admin/last_posts" + when /the admin password reset form with token "([^"]*)"/ "/admin/password/edit?reset_password_token=#{$1}" - # the index page for posts in the root namespace # the index page for posts in the user_admin namespace when /^the index page for (.*) in the (.*) namespace$/ - if $2 != 'root' - send(:"#{$2}_#{$1}_path") - else - send(:"#{$1}_path") - end + send :"#{$2}_#{$1}_path" # same as above, except defaults to admin namespace when /^the index page for (.*)$/ - send(:"admin_#{$1}_path") + send :"admin_#{$1}_path" + + when /^the (.*) index page for (.*)$/ + send :"admin_#{$2}_path", format: $1 when /^the last author's posts$/ admin_user_posts_path(User.last) + when /^the last author's stories$/ + admin_user_stories_path(User.last) + when /^the last author's last post page$/ - admin_user_post_path(User.last, Post.where(:author_id => User.last.id).last) + admin_user_post_path(User.last, Post.where(author_id: User.last.id).last) + + when /^the last post's show page$/ + admin_post_path(Post.last) + + when /^the post's show page$/ + admin_post_path(Post.last) when /^the last post's edit page$/ edit_admin_post_path(Post.last) + when /^the last author's show page$/ + admin_user_path(User.last) + # Add more mappings here. # Here is an example that pulls values out of the Regexp: # @@ -49,10 +64,12 @@ def path_to(page_name) begin page_name =~ /the (.*) page/ path_components = $1.split(/\s+/) - self.send(path_components.push('path').join('_').to_sym) - rescue Object => e + self.send path_components.push("path").join("_") + # simplecov:disable + rescue Object raise "Can't find mapping from \"#{page_name}\" to a path.\n" + "Now, go and add a mapping in #{__FILE__}" + # simplecov:enable end end end diff --git a/features/support/rails.rb b/features/support/rails.rb new file mode 100644 index 00000000000..2a37e729456 --- /dev/null +++ b/features/support/rails.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true +require "cucumber/rails/application" +require "cucumber/rails/action_dispatch" +require "cucumber/rails/world" +require "cucumber/rails/hooks" +require "cucumber/rails/capybara" +require "cucumber/rails/database" + +MultiTest.disable_autorun diff --git a/features/support/selectors.rb b/features/support/selectors.rb deleted file mode 100644 index 238f4745c99..00000000000 --- a/features/support/selectors.rb +++ /dev/null @@ -1,45 +0,0 @@ -module HtmlSelectorsHelpers - # Maps a name to a selector. Used primarily by the - # - # When /^(.+) within (.+)$/ do |step, scope| - # - # step definitions in web_steps.rb - # - def selector_for(locator) - case locator - - when "the page" - "html > body" - - # Add more mappings here. - # Here is an example that pulls values out of the Regexp: - # - # when /^the (notice|error|info) flash$/ - # ".flash.#{$1}" - - # You can also return an array to use a different selector - # type, like: - # - # when /the header/ - # [:xpath, "//header"] - - when "index grid" - [:css, "table.index_grid"] - - when /^the "([^"]*)" sidebar$/ - [:css, "##{$1.gsub(" ", '').underscore}_sidebar_section"] - - # This allows you to provide a quoted selector as the scope - # for "within" steps as was previously the default for the - # web steps: - when /^"(.+)"$/ - $1 - - else - raise "Can't find mapping from \"#{locator}\" to a selector.\n" + - "Now, go and add a mapping in #{__FILE__}" - end - end -end - -World(HtmlSelectorsHelpers) diff --git a/features/support/simplecov_changes_env.rb b/features/support/simplecov_changes_env.rb new file mode 100644 index 00000000000..26c98e2427c --- /dev/null +++ b/features/support/simplecov_changes_env.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +require_relative "../../spec/support/simplecov_helper" + +ActiveAdmin::TestSupport::SimpleCovHelper.start("filesystem changes features") + +require_relative "env" diff --git a/features/support/simplecov_regular_env.rb b/features/support/simplecov_regular_env.rb new file mode 100644 index 00000000000..0d0f61e2fa4 --- /dev/null +++ b/features/support/simplecov_regular_env.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +require_relative "../../spec/support/simplecov_helper" + +ActiveAdmin::TestSupport::SimpleCovHelper.start(["regular features", ENV["TEST_ENV_NUMBER"]].join(" ").rstrip) + +require_relative "env" diff --git a/features/support/simplecov_reload_env.rb b/features/support/simplecov_reload_env.rb new file mode 100644 index 00000000000..8b147e24c42 --- /dev/null +++ b/features/support/simplecov_reload_env.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +require_relative "../../spec/support/simplecov_helper" + +ActiveAdmin::TestSupport::SimpleCovHelper.start("reload features") + +require_relative "env" diff --git a/features/users/logging_in.feature b/features/users/logging_in.feature index 23416b5ef74..ed90c3e2c04 100644 --- a/features/users/logging_in.feature +++ b/features/users/logging_in.feature @@ -3,32 +3,30 @@ Feature: User Logging In Logging in to the system as an admin user Background: - Given a configuration of: - """ - ActiveAdmin.register Post - """ - And I am logged out + Given I am logged out And an admin user "admin@example.com" exists When I go to the dashboard Scenario: Logging in Successfully When I fill in "Email" with "admin@example.com" And I fill in "Password" with "password" - And I press "Login" + And I press "Sign In" Then I should be on the the dashboard - And I should see "Logout" - And I should see "admin@example.com" + And I should see the element "a[href='/admin/logout' ]:contains('Sign out')" + And I should see the element "a[href='/admin/admin_users/1']:contains('admin@example.com')" Scenario: Attempting to log in with an incorrect email address + Given override locale "devise.failure.not_found_in_database" with "Invalid email or password." When I fill in "Email" with "not-an-admin@example.com" And I fill in "Password" with "not-my-password" - And I press "Login" - Then I should see "Login" + And I press "Sign In" + Then I should see "Sign In" And I should see "Invalid email or password." Scenario: Attempting to log in with an incorrect password + Given override locale "devise.failure.invalid" with "Invalid email or password." When I fill in "Email" with "admin@example.com" And I fill in "Password" with "not-my-password" - And I press "Login" - Then I should see "Login" + And I press "Sign In" + Then I should see "Sign In" And I should see "Invalid email or password." diff --git a/features/users/logging_out.feature b/features/users/logging_out.feature index bf8ef77e190..ed6acbe7e3c 100644 --- a/features/users/logging_out.feature +++ b/features/users/logging_out.feature @@ -3,11 +3,8 @@ Feature: User Logging out Logging out of the system as an admin user Scenario: Logging out successfully - Given a configuration of: - """ - ActiveAdmin.register Post - """ - And I am logged in - When I go to the dashboard - And I follow "Logout" - Then I should see "Login" + When I am logged in + And I go to the dashboard + Then I should see the element "a[data-method='delete']:contains('Sign out')" + And I follow "Sign out" + And I should see "Sign In" diff --git a/features/users/resetting_password.feature b/features/users/resetting_password.feature index 9935fc7bf54..dbad1f8e3de 100644 --- a/features/users/resetting_password.feature +++ b/features/users/resetting_password.feature @@ -3,32 +3,30 @@ Feature: User Resetting Password Resetting my password as an admin user Background: - Given a configuration of: - """ - ActiveAdmin.register Post - """ - And I am logged out + Given I am logged out And an admin user "admin@example.com" exists Scenario: Resetting password successfully When I go to the dashboard And I follow "Forgot your password?" - When I fill in "Email" with "admin@example.com" + And I fill in "Email" with "admin@example.com" And I press "Reset My Password" - Then I should see "You will receive an email with instructions about how to reset your password in a few minutes." + Then I should see "You will receive an email with instructions on how to reset your password in a few minutes." + @mocks Scenario: Changing password after resetting - Given an admin user "admin@example.com" exists with reset password token "123reset" - When I go to the admin password reset form with reset password token "123reset" - And I fill in "Password" with "password" + When "admin@example.com" requests a password reset with token "foobarbaz" + And I go to the admin password reset form with token "foobarbaz" + And I fill in the password field with "password" And I fill in "Password confirmation" with "password" And I press "Change my password" Then I should see "success" + @mocks Scenario: Changing password after resetting with errors - Given an admin user "admin@example.com" exists with expired reset password token "123reset" - When I go to the admin password reset form with reset password token "123reset" - And I fill in "Password" with "password" + When "admin@example.com" requests a password reset with token "foobarbaz" but it expires + And I go to the admin password reset form with token "foobarbaz" + And I fill in the password field with "password" And I fill in "Password confirmation" with "wrong" And I press "Change my password" Then I should see "expired" diff --git a/gemfiles/rails_72/Gemfile b/gemfiles/rails_72/Gemfile new file mode 100644 index 00000000000..1dac1013775 --- /dev/null +++ b/gemfiles/rails_72/Gemfile @@ -0,0 +1,45 @@ +# frozen_string_literal: true +source "https://rubygems.org", cooldown: 5 + +group :development, :test do + gem "rake" + + gem "cancancan" + gem "pundit" + + gem "draper" + gem "devise" + + gem "rails", "~> 7.2.0" + # Rails 7.2.4 caps connection_pool below 3, whose keyword-argument API breaks cache stores + gem "connection_pool", "< 3" # TODO: remove when dropping Rails 7.2 (see rails/rails#56620) + + gem "sprockets-rails" + gem "ransack", ">= 4.2.0" + gem "formtastic", ">= 6.0.0" + + gem "cssbundling-rails" + gem "importmap-rails" +end + +group :test do + gem "cuprite" + gem "capybara" + gem "webrick" + + gem "simplecov", require: false # Test coverage generator. Go to /coverage/ after running tests + gem "simplecov-cobertura", require: false + gem "cucumber-rails", require: false + gem "database_cleaner-active_record" + gem "launchy" + gem "parallel_tests" + gem "rspec-rails" + gem "sqlite3", platform: :mri + + # Translations + gem "i18n-tasks" + gem "i18n-spec" + gem "rails-i18n" # Provides default i18n for many languages +end + +gemspec path: "../.." diff --git a/gemfiles/rails_72/Gemfile.lock b/gemfiles/rails_72/Gemfile.lock new file mode 100644 index 00000000000..5d6a8b085af --- /dev/null +++ b/gemfiles/rails_72/Gemfile.lock @@ -0,0 +1,457 @@ +PATH + remote: ../.. + specs: + activeadmin (4.0.0.beta23) + arbre (~> 2.0) + csv + formtastic (>= 6.0) + formtastic_i18n (>= 0.7) + inherited_resources (~> 2.0) + kaminari (>= 1.2.1) + railties (>= 7.2) + ransack (>= 4.0) + +GEM + remote: https://rubygems.org/ + specs: + actioncable (7.2.4) + actionpack (= 7.2.4) + activesupport (= 7.2.4) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (7.2.4) + actionpack (= 7.2.4) + activejob (= 7.2.4) + activerecord (= 7.2.4) + activestorage (= 7.2.4) + activesupport (= 7.2.4) + mail (>= 2.8.0) + actionmailer (7.2.4) + actionpack (= 7.2.4) + actionview (= 7.2.4) + activejob (= 7.2.4) + activesupport (= 7.2.4) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (7.2.4) + actionview (= 7.2.4) + activesupport (= 7.2.4) + cgi + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4, < 3.3) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (7.2.4) + actionpack (= 7.2.4) + activerecord (= 7.2.4) + activestorage (= 7.2.4) + activesupport (= 7.2.4) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.2.4) + activesupport (= 7.2.4) + builder (~> 3.1) + cgi + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (7.2.4) + activesupport (= 7.2.4) + globalid (>= 0.3.6) + activemodel (7.2.4) + activesupport (= 7.2.4) + activemodel-serializers-xml (1.0.3) + activemodel (>= 5.0.0.a) + activesupport (>= 5.0.0.a) + builder (~> 3.1) + activerecord (7.2.4) + activemodel (= 7.2.4) + activesupport (= 7.2.4) + timeout (>= 0.4.0) + activestorage (7.2.4) + actionpack (= 7.2.4) + activejob (= 7.2.4) + activerecord (= 7.2.4) + activesupport (= 7.2.4) + marcel (~> 1.0) + activesupport (7.2.4) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5, < 3) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1, < 6) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + arbre (2.2.1) + activesupport (>= 7.0) + ast (2.4.3) + base64 (0.3.0) + bcrypt (3.1.22) + benchmark (0.5.0) + bigdecimal (4.1.3) + builder (3.3.0) + cancancan (3.6.1) + capybara (3.40.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + cgi (0.5.2) + childprocess (5.1.0) + logger (~> 1.5) + concurrent-ruby (1.3.8) + connection_pool (2.5.5) + crass (1.0.7) + cssbundling-rails (1.4.3) + railties (>= 6.0.0) + csv (3.3.6) + cucumber (11.1.1) + base64 (~> 0.2) + builder (~> 3.2) + cucumber-ci-environment (> 9, < 12) + cucumber-core (>= 16.2.0, < 17) + cucumber-cucumber-expressions (> 17, < 20) + cucumber-html-formatter (> 21, < 24) + diff-lcs (~> 1.5) + logger (~> 1.6) + mini_mime (~> 1.1) + multi_test (~> 1.1) + sys-uname (~> 1.5) + cucumber-ci-environment (11.0.0) + cucumber-core (16.2.0) + cucumber-gherkin (> 36, < 40) + cucumber-messages (> 31, < 33) + cucumber-tag-expressions (> 6, < 9) + cucumber-cucumber-expressions (19.0.1) + bigdecimal + cucumber-gherkin (39.1.0) + cucumber-messages (>= 31, < 33) + cucumber-html-formatter (23.1.0) + cucumber-messages (> 23, < 33) + cucumber-messages (32.3.1) + cucumber-rails (4.1.0) + capybara (>= 3.25, < 4) + cucumber (>= 7, < 12) + railties (>= 6.1, < 9) + cucumber-tag-expressions (8.1.0) + cuprite (0.18) + capybara (~> 3.0) + ferrum (~> 0.18.0) + database_cleaner-active_record (2.2.2) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0) + database_cleaner-core (2.1.0) + date (3.5.1) + devise (5.0.4) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 7.0) + responders + warden (~> 1.2.3) + diff-lcs (1.6.2) + draper (4.0.6) + actionpack (>= 5.0) + activemodel (>= 5.0) + activemodel-serializers-xml (>= 1.0) + activesupport (>= 5.0) + request_store (>= 1.0) + ruby2_keywords + drb (2.2.3) + erb (6.0.7) + erubi (1.13.1) + ferrum (0.18.0) + addressable (~> 2.5) + base64 (~> 0.2) + concurrent-ruby (~> 1.1) + websocket-driver (~> 0.7) + ffi (1.17.4-aarch64-linux-gnu) + ffi (1.17.4-arm64-darwin) + ffi (1.17.4-x86_64-darwin) + ffi (1.17.4-x86_64-linux-gnu) + formtastic (6.0.0) + actionpack (>= 7.2.0) + formtastic_i18n (0.7.0) + globalid (1.4.0) + activesupport (>= 6.1) + has_scope (0.9.0) + actionpack (>= 7.0) + activesupport (>= 7.0) + highline (3.1.2) + reline + i18n (1.15.2) + concurrent-ruby (~> 1.0) + i18n-spec (0.6.0) + iso + i18n-tasks (1.1.2) + activesupport (>= 4.0.2) + ast (>= 2.1.0) + erubi + highline (>= 3.0.0) + i18n + parser (>= 3.2.2.1) + prism + rails-i18n + rainbow (>= 2.2.2, < 4.0) + ruby-progressbar (~> 1.8, >= 1.8.1) + terminal-table (>= 1.5.1) + importmap-rails (2.2.3) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + inherited_resources (2.1.0) + actionpack (>= 7.0) + has_scope (>= 0.6) + railties (>= 7.0) + responders (>= 2) + io-console (0.9.4) + irb (1.18.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + iso (0.4.0) + i18n + kaminari (1.2.2) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.2.2) + kaminari-activerecord (= 1.2.2) + kaminari-core (= 1.2.2) + kaminari-actionview (1.2.2) + actionview + kaminari-core (= 1.2.2) + kaminari-activerecord (1.2.2) + activerecord + kaminari-core (= 1.2.2) + kaminari-core (1.2.2) + launchy (3.1.1) + addressable (~> 2.8) + childprocess (~> 5.0) + logger (~> 1.6) + logger (1.7.0) + loofah (2.25.2) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.9.1) + logger + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.2.1) + matrix (0.4.3) + memoist3 (1.0.0) + mini_mime (1.1.5) + minitest (5.27.0) + multi_test (1.1.0) + net-imap (0.6.7) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.4.0) + timeout + net-smtp (0.5.2) + net-protocol + nio4r (2.7.5) + nokogiri (1.19.4-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.4-arm64-darwin) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-linux-gnu) + racc (~> 1.4) + orm_adapter (0.5.0) + parallel (2.3.0) + parallel_tests (5.8.0) + parallel + parser (3.3.12.0) + ast (~> 2.4.1) + racc + pp (0.6.4) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + public_suffix (7.0.5) + pundit (2.5.2) + activesupport (>= 3.0.0) + racc (1.8.1) + rack (3.2.7) + rack-session (2.1.2) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.3.1) + rack (>= 3) + rails (7.2.4) + actioncable (= 7.2.4) + actionmailbox (= 7.2.4) + actionmailer (= 7.2.4) + actionpack (= 7.2.4) + actiontext (= 7.2.4) + actionview (= 7.2.4) + activejob (= 7.2.4) + activemodel (= 7.2.4) + activerecord (= 7.2.4) + activestorage (= 7.2.4) + activesupport (= 7.2.4) + bundler (>= 1.15.0) + railties (= 7.2.4) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.7.1) + loofah (~> 2.25, >= 2.25.2) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + rails-i18n (7.0.10) + i18n (>= 0.7, < 2) + railties (>= 6.0.0, < 8) + railties (7.2.4) + actionpack (= 7.2.4) + activesupport (= 7.2.4) + cgi + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.4.2) + ransack (5.0.2) + activerecord (>= 7.2) + activesupport (>= 7.2) + i18n + rbs (4.2.0) + logger + prism (>= 1.6.0) + tsort + rdoc (8.1.0) + erb + prism (>= 1.6.0) + rbs (>= 4.0.0) + tsort + regexp_parser (2.13.1) + reline (0.7.0) + io-console (~> 0.5) + request_store (1.7.0) + rack (>= 1.4) + responders (3.2.1) + actionpack (>= 7.0) + railties (>= 7.0) + rexml (3.4.4) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.8) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (8.0.4) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) + rspec-core (>= 3.13.0, < 5.0.0) + rspec-expectations (>= 3.13.0, < 5.0.0) + rspec-mocks (>= 3.13.0, < 5.0.0) + rspec-support (>= 3.13.0, < 5.0.0) + rspec-support (3.13.7) + ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) + securerandom (0.4.1) + simplecov (1.3.2) + simplecov-cobertura (4.0.0) + rexml + simplecov (~> 1.0) + sprockets (4.4.1) + concurrent-ruby (~> 1.1) + logger + rack (>= 2.2.4, < 4) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) + sprockets (>= 3.0.0) + sqlite3 (2.9.6-aarch64-linux-gnu) + sqlite3 (2.9.6-arm64-darwin) + sqlite3 (2.9.6-x86_64-darwin) + sqlite3 (2.9.6-x86_64-linux-gnu) + sys-uname (1.5.1) + ffi (~> 1.1) + memoist3 (~> 1.0.0) + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) + thor (1.5.0) + timeout (0.6.1) + tsort (0.2.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.3.0) + unicode-emoji (~> 4.3) + unicode-emoji (4.3.0) + useragent (0.16.11) + warden (1.2.9) + rack (>= 2.0.9) + webrick (1.9.2) + websocket-driver (0.8.2) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.8.3) + +PLATFORMS + aarch64-linux + arm64-darwin + x86_64-darwin + x86_64-linux + +DEPENDENCIES + activeadmin! + cancancan + capybara + connection_pool (< 3) + cssbundling-rails + cucumber-rails + cuprite + database_cleaner-active_record + devise + draper + formtastic (>= 6.0.0) + i18n-spec + i18n-tasks + importmap-rails + launchy + parallel_tests + pundit + rails (~> 7.2.0) + rails-i18n + rake + ransack (>= 4.2.0) + rspec-rails + simplecov + simplecov-cobertura + sprockets-rails + sqlite3 + webrick + +BUNDLED WITH + 4.0.22 diff --git a/gemfiles/rails_80/Gemfile b/gemfiles/rails_80/Gemfile new file mode 100644 index 00000000000..278719f6a40 --- /dev/null +++ b/gemfiles/rails_80/Gemfile @@ -0,0 +1,43 @@ +# frozen_string_literal: true +source "https://rubygems.org", cooldown: 5 + +group :development, :test do + gem "rake" + + gem "cancancan" + gem "pundit" + + gem "draper" + gem "devise" + + gem "rails", "~> 8.0.0" + + gem "sprockets-rails" + gem "ransack", ">= 4.2.0" + gem "formtastic", ">= 6.0.0" + + gem "cssbundling-rails" + gem "importmap-rails" +end + +group :test do + gem "cuprite" + gem "capybara" + gem "webrick" + + gem "simplecov", require: false # Test coverage generator. Go to /coverage/ after running tests + gem "simplecov-cobertura", require: false + gem "cucumber-rails", require: false + gem "database_cleaner-active_record" + gem "launchy" + gem "parallel_tests" + gem "rspec-rails" + gem "sqlite3", platform: :mri + + # Translations + gem "i18n-tasks" + gem "i18n-spec" + gem "rails-i18n" # Provides default i18n for many languages +end + +gemspec path: "../.." diff --git a/gemfiles/rails_80/Gemfile.lock b/gemfiles/rails_80/Gemfile.lock new file mode 100644 index 00000000000..472f0b966ea --- /dev/null +++ b/gemfiles/rails_80/Gemfile.lock @@ -0,0 +1,455 @@ +PATH + remote: ../.. + specs: + activeadmin (4.0.0.beta23) + arbre (~> 2.0) + csv + formtastic (>= 6.0) + formtastic_i18n (>= 0.7) + inherited_resources (~> 2.0) + kaminari (>= 1.2.1) + railties (>= 7.2) + ransack (>= 4.0) + +GEM + remote: https://rubygems.org/ + specs: + actioncable (8.0.5.1) + actionpack (= 8.0.5.1) + activesupport (= 8.0.5.1) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (8.0.5.1) + actionpack (= 8.0.5.1) + activejob (= 8.0.5.1) + activerecord (= 8.0.5.1) + activestorage (= 8.0.5.1) + activesupport (= 8.0.5.1) + mail (>= 2.8.0) + actionmailer (8.0.5.1) + actionpack (= 8.0.5.1) + actionview (= 8.0.5.1) + activejob (= 8.0.5.1) + activesupport (= 8.0.5.1) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.0.5.1) + actionview (= 8.0.5.1) + activesupport (= 8.0.5.1) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.0.5.1) + actionpack (= 8.0.5.1) + activerecord (= 8.0.5.1) + activestorage (= 8.0.5.1) + activesupport (= 8.0.5.1) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.0.5.1) + activesupport (= 8.0.5.1) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.0.5.1) + activesupport (= 8.0.5.1) + globalid (>= 0.3.6) + activemodel (8.0.5.1) + activesupport (= 8.0.5.1) + activemodel-serializers-xml (1.0.3) + activemodel (>= 5.0.0.a) + activesupport (>= 5.0.0.a) + builder (~> 3.1) + activerecord (8.0.5.1) + activemodel (= 8.0.5.1) + activesupport (= 8.0.5.1) + timeout (>= 0.4.0) + activestorage (8.0.5.1) + actionpack (= 8.0.5.1) + activejob (= 8.0.5.1) + activerecord (= 8.0.5.1) + activesupport (= 8.0.5.1) + marcel (~> 1.0) + activesupport (8.0.5.1) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + arbre (2.2.1) + activesupport (>= 7.0) + ast (2.4.3) + base64 (0.3.0) + bcrypt (3.1.22) + benchmark (0.5.0) + bigdecimal (4.1.3) + builder (3.3.0) + cancancan (3.6.1) + capybara (3.40.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + childprocess (5.1.0) + logger (~> 1.5) + concurrent-ruby (1.3.8) + connection_pool (3.0.2) + crass (1.0.7) + cssbundling-rails (1.4.3) + railties (>= 6.0.0) + csv (3.3.6) + cucumber (11.1.1) + base64 (~> 0.2) + builder (~> 3.2) + cucumber-ci-environment (> 9, < 12) + cucumber-core (>= 16.2.0, < 17) + cucumber-cucumber-expressions (> 17, < 20) + cucumber-html-formatter (> 21, < 24) + diff-lcs (~> 1.5) + logger (~> 1.6) + mini_mime (~> 1.1) + multi_test (~> 1.1) + sys-uname (~> 1.5) + cucumber-ci-environment (11.0.0) + cucumber-core (16.2.0) + cucumber-gherkin (> 36, < 40) + cucumber-messages (> 31, < 33) + cucumber-tag-expressions (> 6, < 9) + cucumber-cucumber-expressions (19.0.1) + bigdecimal + cucumber-gherkin (39.1.0) + cucumber-messages (>= 31, < 33) + cucumber-html-formatter (23.1.0) + cucumber-messages (> 23, < 33) + cucumber-messages (32.3.1) + cucumber-rails (4.1.0) + capybara (>= 3.25, < 4) + cucumber (>= 7, < 12) + railties (>= 6.1, < 9) + cucumber-tag-expressions (8.1.0) + cuprite (0.18) + capybara (~> 3.0) + ferrum (~> 0.18.0) + database_cleaner-active_record (2.2.2) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0) + database_cleaner-core (2.1.0) + date (3.5.1) + devise (5.0.4) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 7.0) + responders + warden (~> 1.2.3) + diff-lcs (1.6.2) + draper (4.0.6) + actionpack (>= 5.0) + activemodel (>= 5.0) + activemodel-serializers-xml (>= 1.0) + activesupport (>= 5.0) + request_store (>= 1.0) + ruby2_keywords + drb (2.2.3) + erb (6.0.7) + erubi (1.13.1) + ferrum (0.18.0) + addressable (~> 2.5) + base64 (~> 0.2) + concurrent-ruby (~> 1.1) + websocket-driver (~> 0.7) + ffi (1.17.4-aarch64-linux-gnu) + ffi (1.17.4-arm64-darwin) + ffi (1.17.4-x86_64-darwin) + ffi (1.17.4-x86_64-linux-gnu) + formtastic (6.0.0) + actionpack (>= 7.2.0) + formtastic_i18n (0.7.0) + globalid (1.4.0) + activesupport (>= 6.1) + has_scope (0.9.0) + actionpack (>= 7.0) + activesupport (>= 7.0) + highline (3.1.2) + reline + i18n (1.15.2) + concurrent-ruby (~> 1.0) + i18n-spec (0.6.0) + iso + i18n-tasks (1.1.2) + activesupport (>= 4.0.2) + ast (>= 2.1.0) + erubi + highline (>= 3.0.0) + i18n + parser (>= 3.2.2.1) + prism + rails-i18n + rainbow (>= 2.2.2, < 4.0) + ruby-progressbar (~> 1.8, >= 1.8.1) + terminal-table (>= 1.5.1) + importmap-rails (2.2.3) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + inherited_resources (2.1.0) + actionpack (>= 7.0) + has_scope (>= 0.6) + railties (>= 7.0) + responders (>= 2) + io-console (0.9.4) + irb (1.18.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + iso (0.4.0) + i18n + kaminari (1.2.2) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.2.2) + kaminari-activerecord (= 1.2.2) + kaminari-core (= 1.2.2) + kaminari-actionview (1.2.2) + actionview + kaminari-core (= 1.2.2) + kaminari-activerecord (1.2.2) + activerecord + kaminari-core (= 1.2.2) + kaminari-core (1.2.2) + launchy (3.1.1) + addressable (~> 2.8) + childprocess (~> 5.0) + logger (~> 1.6) + logger (1.7.0) + loofah (2.25.2) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.9.1) + logger + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.2.1) + matrix (0.4.3) + memoist3 (1.0.0) + mini_mime (1.1.5) + minitest (6.0.6) + drb (~> 2.0) + prism (~> 1.5) + multi_test (1.1.0) + net-imap (0.6.7) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.4.0) + timeout + net-smtp (0.5.2) + net-protocol + nio4r (2.7.5) + nokogiri (1.19.4-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.19.4-arm64-darwin) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-linux-gnu) + racc (~> 1.4) + orm_adapter (0.5.0) + parallel (2.3.0) + parallel_tests (5.8.0) + parallel + parser (3.3.12.0) + ast (~> 2.4.1) + racc + pp (0.6.4) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + public_suffix (7.0.5) + pundit (2.5.2) + activesupport (>= 3.0.0) + racc (1.8.1) + rack (3.2.7) + rack-session (2.1.2) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.3.1) + rack (>= 3) + rails (8.0.5.1) + actioncable (= 8.0.5.1) + actionmailbox (= 8.0.5.1) + actionmailer (= 8.0.5.1) + actionpack (= 8.0.5.1) + actiontext (= 8.0.5.1) + actionview (= 8.0.5.1) + activejob (= 8.0.5.1) + activemodel (= 8.0.5.1) + activerecord (= 8.0.5.1) + activestorage (= 8.0.5.1) + activesupport (= 8.0.5.1) + bundler (>= 1.15.0) + railties (= 8.0.5.1) + rails-dom-testing (2.3.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.7.1) + loofah (~> 2.25, >= 2.25.2) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + rails-i18n (8.1.0) + i18n (>= 0.7, < 2) + railties (>= 8.0.0, < 9) + railties (8.0.5.1) + actionpack (= 8.0.5.1) + activesupport (= 8.0.5.1) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.4.2) + ransack (5.0.2) + activerecord (>= 7.2) + activesupport (>= 7.2) + i18n + rbs (4.2.0) + logger + prism (>= 1.6.0) + tsort + rdoc (8.1.0) + erb + prism (>= 1.6.0) + rbs (>= 4.0.0) + tsort + regexp_parser (2.13.1) + reline (0.7.0) + io-console (~> 0.5) + request_store (1.7.0) + rack (>= 1.4) + responders (3.2.1) + actionpack (>= 7.0) + railties (>= 7.0) + rexml (3.4.4) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.8) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (8.0.4) + actionpack (>= 7.2) + activesupport (>= 7.2) + railties (>= 7.2) + rspec-core (>= 3.13.0, < 5.0.0) + rspec-expectations (>= 3.13.0, < 5.0.0) + rspec-mocks (>= 3.13.0, < 5.0.0) + rspec-support (>= 3.13.0, < 5.0.0) + rspec-support (3.13.7) + ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) + securerandom (0.4.1) + simplecov (1.3.2) + simplecov-cobertura (4.0.0) + rexml + simplecov (~> 1.0) + sprockets (4.4.1) + concurrent-ruby (~> 1.1) + logger + rack (>= 2.2.4, < 4) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) + sprockets (>= 3.0.0) + sqlite3 (2.9.6-aarch64-linux-gnu) + sqlite3 (2.9.6-arm64-darwin) + sqlite3 (2.9.6-x86_64-darwin) + sqlite3 (2.9.6-x86_64-linux-gnu) + sys-uname (1.5.1) + ffi (~> 1.1) + memoist3 (~> 1.0.0) + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) + thor (1.5.0) + timeout (0.6.1) + tsort (0.2.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.3.0) + unicode-emoji (~> 4.3) + unicode-emoji (4.3.0) + uri (1.1.1) + useragent (0.16.11) + warden (1.2.9) + rack (>= 2.0.9) + webrick (1.9.2) + websocket-driver (0.8.2) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.8.3) + +PLATFORMS + aarch64-linux + arm64-darwin + x86_64-darwin + x86_64-linux + +DEPENDENCIES + activeadmin! + cancancan + capybara + cssbundling-rails + cucumber-rails + cuprite + database_cleaner-active_record + devise + draper + formtastic (>= 6.0.0) + i18n-spec + i18n-tasks + importmap-rails + launchy + parallel_tests + pundit + rails (~> 8.0.0) + rails-i18n + rake + ransack (>= 4.2.0) + rspec-rails + simplecov + simplecov-cobertura + sprockets-rails + sqlite3 + webrick + +BUNDLED WITH + 4.0.22 diff --git a/lib/active_admin.rb b/lib/active_admin.rb index 05734224f2c..148f34468e2 100644 --- a/lib/active_admin.rb +++ b/lib/active_admin.rb @@ -1,65 +1,71 @@ -require 'meta_search' -require 'bourbon' -require 'devise' -require 'kaminari' -require 'formtastic' -require 'sass' -require 'inherited_resources' -require 'jquery-rails' -require 'arbre' -require 'active_admin/dependency_checker' -require 'active_admin/sass/helpers' -require 'active_admin/engine' +# frozen_string_literal: true +require "active_support/core_ext" +require "set" + +require "ransack" +require "kaminari" +require "formtastic" +require "formtastic_i18n" +require "inherited_resources" +require "arbre" + +begin + require "importmap-rails" +rescue LoadError + # importmap-rails is optional +end module ActiveAdmin - autoload :VERSION, 'active_admin/version' - autoload :Application, 'active_admin/application' - autoload :AssetRegistration, 'active_admin/asset_registration' - autoload :Authorization, 'active_admin/authorization_adapter' - autoload :AuthorizationAdapter, 'active_admin/authorization_adapter' - autoload :Breadcrumbs, 'active_admin/breadcrumbs' - autoload :CanCanAdapter, 'active_admin/cancan_adapter' - autoload :Callbacks, 'active_admin/callbacks' - autoload :Component, 'active_admin/component' - autoload :BaseController, 'active_admin/base_controller' - autoload :ControllerAction, 'active_admin/controller_action' - autoload :CSVBuilder, 'active_admin/csv_builder' - autoload :Deprecation, 'active_admin/deprecation' - autoload :Devise, 'active_admin/devise' - autoload :DSL, 'active_admin/dsl' - autoload :Event, 'active_admin/event' - autoload :FormBuilder, 'active_admin/form_builder' - autoload :Inputs, 'active_admin/inputs' - autoload :Iconic, 'active_admin/iconic' - autoload :Menu, 'active_admin/menu' - autoload :MenuCollection, 'active_admin/menu_collection' - autoload :MenuItem, 'active_admin/menu_item' - autoload :Namespace, 'active_admin/namespace' - autoload :Page, 'active_admin/page' - autoload :PagePresenter, 'active_admin/page_presenter' - autoload :PageController, 'active_admin/page_controller' - autoload :PageDSL, 'active_admin/page_dsl' - autoload :Reloader, 'active_admin/reloader' - autoload :Resource, 'active_admin/resource' - autoload :ResourceController, 'active_admin/resource_controller' - autoload :ResourceDSL, 'active_admin/resource_dsl' - autoload :Scope, 'active_admin/scope' - autoload :ScopeChain, 'active_admin/helpers/scope_chain' - autoload :SidebarSection, 'active_admin/sidebar_section' - autoload :TableBuilder, 'active_admin/table_builder' - autoload :ViewFactory, 'active_admin/view_factory' - autoload :ViewHelpers, 'active_admin/view_helpers' - autoload :Views, 'active_admin/views' + autoload :VERSION, "active_admin/version" + autoload :Application, "active_admin/application" + autoload :Authorization, "active_admin/authorization_adapter" + autoload :AuthorizationAdapter, "active_admin/authorization_adapter" + autoload :Callbacks, "active_admin/callbacks" + autoload :Component, "active_admin/component" + autoload :CanCanAdapter, "active_admin/cancan_adapter" + autoload :ControllerAction, "active_admin/controller_action" + autoload :CSVBuilder, "active_admin/csv_builder" + autoload :Dependency, "active_admin/dependency" + autoload :Devise, "active_admin/devise" + autoload :DSL, "active_admin/dsl" + autoload :FormBuilder, "active_admin/form_builder" + autoload :Inputs, "active_admin/inputs" + autoload :Localizers, "active_admin/localizers" + autoload :Menu, "active_admin/menu" + autoload :MenuCollection, "active_admin/menu_collection" + autoload :MenuItem, "active_admin/menu_item" + autoload :Namespace, "active_admin/namespace" + autoload :OrderClause, "active_admin/order_clause" + autoload :Page, "active_admin/page" + autoload :PagePresenter, "active_admin/page_presenter" + autoload :PageDSL, "active_admin/page_dsl" + autoload :PunditAdapter, "active_admin/pundit_adapter" + autoload :Resource, "active_admin/resource" + autoload :ResourceDSL, "active_admin/resource_dsl" + autoload :Scope, "active_admin/scope" + autoload :ScopeChain, "active_admin/helpers/scope_chain" + autoload :SidebarSection, "active_admin/sidebar_section" + autoload :TableBuilder, "active_admin/table_builder" + autoload :ViewHelpers, "active_admin/view_helpers" + autoload :Views, "active_admin/views" class << self - attr_accessor :application + attr_accessor :application, :importmap def application @application ||= ::ActiveAdmin::Application.new end + def deprecator + @deprecator ||= ActiveSupport::Deprecation.new("4.1", "active-admin") + end + + def importmap + @importmap ||= Importmap::Map.new + end + # Gets called within the initializer def setup application.setup! @@ -67,17 +73,11 @@ def setup application.prepare! end - delegate :register, :to => :application - delegate :register_page, :to => :application - delegate :unload!, :to => :application - delegate :load!, :to => :application - delegate :routes, :to => :application - - # Returns true if this rails application has the asset - # pipeline enabled. - def use_asset_pipeline? - DependencyChecker.rails_3_1? && Rails.application.config.try(:assets).try(:enabled) - end + delegate :register, to: :application + delegate :register_page, to: :application + delegate :unload!, to: :application + delegate :load!, to: :application + delegate :routes, to: :application # A callback is triggered each time (before) Active Admin loads the configuration files. # In development mode, this will happen whenever the user changes files. In production @@ -93,7 +93,7 @@ def use_asset_pipeline? # # @param [Block] block A block to call each time (before) AA loads resources def before_load(&block) - ActiveAdmin::Event.subscribe ActiveAdmin::Application::BeforeLoadEvent, &block + ActiveSupport::Notifications.subscribe ActiveAdmin::Application::BeforeLoadEvent, &wrap_block_for_active_support_notifications(block) end # A callback is triggered each time (after) Active Admin loads the configuration files. This @@ -111,16 +111,26 @@ def before_load(&block) # # @param [Block] block A block to call each time (after) AA loads resources def after_load(&block) - ActiveAdmin::Event.subscribe ActiveAdmin::Application::AfterLoadEvent, &block + ActiveSupport::Notifications.subscribe ActiveAdmin::Application::AfterLoadEvent, &wrap_block_for_active_support_notifications(block) + end + + private + + def wrap_block_for_active_support_notifications block + proc { |_name, _start, _finish, _id, payload| block.call payload[:active_admin_application] } end end end -ActiveAdmin::DependencyChecker.check! +# Require things that don't support autoload +require_relative "active_admin/engine" +require_relative "active_admin/error" + +# Require internal plugins +require_relative "active_admin/batch_actions" +require_relative "active_admin/filters" -# Require internal Plugins -require 'active_admin/comments' -require 'active_admin/batch_actions' -require 'active_admin/filters' +# Require ORM-specific plugins +require_relative "active_admin/orm/active_record" if defined? ActiveRecord diff --git a/lib/active_admin/abstract_view_factory.rb b/lib/active_admin/abstract_view_factory.rb deleted file mode 100644 index e5875cae94a..00000000000 --- a/lib/active_admin/abstract_view_factory.rb +++ /dev/null @@ -1,95 +0,0 @@ -module ActiveAdmin - class AbstractViewFactory - @@default_views = {} - - def self.register(view_hash) - view_hash.each do |view_key, view_class| - @@default_views[view_key] = view_class - end - end - - def initialize - @views = {} - end - - # Register a new view key with the view factory - # - # eg: - # - # factory = AbstractViewFactory.new - # factory.register :my_view => SomeViewClass - # - # You can setup many at the same time: - # - # factory.register :my_view => SomeClass, - # :another_view => OtherViewClass - # - def register(view_hash) - view_hash.each do |view_key, view_class| - @views[view_key] = view_class - end - end - - def default_for(key) - @@default_views[key.to_sym] - end - - def has_key?(key) - @views.has_key?(key.to_sym) || @@default_views.has_key?(key.to_sym) - end - - def [](key) - get_view_for_key(key) - end - - def []=(key, value) - set_view_for_key(key, value) - end - - # Override respond to to include keys - def respond_to?(method) - key = key_from_method_name(method) - if has_key?(key) - true - else - super - end - end - - private - - def method_missing(method, *args) - key = key_from_method_name(method) - if has_key?(key) - if method.to_s.include?('=') - self.class_eval <<-EOS - def #{key}=(value) - set_view_for_key(:#{key}, value) - end - EOS - else - self.class_eval <<-EOS - def #{key} - get_view_for_key(:#{key}) - end - EOS - end - self.send(method, *args) - else - super - end - end - - def key_from_method_name(method) - method.to_s.gsub('=', '').to_sym - end - - def get_view_for_key(key) - @views[key.to_sym] || @@default_views[key.to_sym] - end - - def set_view_for_key(key, view) - @views[key.to_sym] = view - end - end -end diff --git a/lib/active_admin/application.rb b/lib/active_admin/application.rb index 5be537e652e..8be729c29bc 100644 --- a/lib/active_admin/application.rb +++ b/lib/active_admin/application.rb @@ -1,131 +1,80 @@ -require 'active_admin/router' -require 'active_admin/helpers/settings' +# frozen_string_literal: true +require_relative "router" +require_relative "application_settings" +require_relative "namespace_settings" module ActiveAdmin class Application - include Settings - include Settings::Inheritance - settings_inherited_by Namespace - - # The default namespace to put controllers and routes inside. Set this - # in config/initializers/active_admin.rb using: - # - # config.default_namespace = :super_admin - # - setting :default_namespace, :admin + class << self + def setting(name, default) + ApplicationSettings.register name, default + end - attr_reader :namespaces - def initialize - @namespaces = {} + def inheritable_setting(name, default) + NamespaceSettings.register name, default + end end - # Load paths for admin configurations. Add folders to this load path - # to load up other resources for administration. External gems can - # include their paths in this load path to provide active_admin UIs - setting :load_paths, [File.expand_path('app/admin', Rails.root)] - - # The default number of resources to display on index pages - inheritable_setting :default_per_page, 30 - - # The title which gets displayed in the main layout - inheritable_setting :site_title, "" - - # Set the site title link href (defaults to AA dashboard) - inheritable_setting :site_title_link, "" - - # Set the site title image displayed in the main layout (has precendence over :site_title) - inheritable_setting :site_title_image, "" - - # The view factory to use to generate all the view classes. Take - # a look at ActiveAdmin::ViewFactory - inheritable_setting :view_factory, ActiveAdmin::ViewFactory.new - - # The method to call in controllers to get the current user - inheritable_setting :current_user_method, false - - # The method to call in the controllers to ensure that there - # is a currently authenticated admin user - inheritable_setting :authentication_method, false - - # The path to log user's out with. If set to a symbol, we assume - # that it's a method to call which returns the path - inheritable_setting :logout_link_path, :destroy_admin_user_session_path - - # The method to use when generating the link for user logout - inheritable_setting :logout_link_method, :get - - # Whether the batch actions are enabled or not - inheritable_setting :batch_actions, false - - # Whether filters are enabled - inheritable_setting :filters, true - - # The namespace root. - inheritable_setting :root_to, 'dashboard#index' - - # Default CSV options - inheritable_setting :csv_options, {:col_sep => ','} - - # Default Download Links options - inheritable_setting :download_links, true - - # The authorization adapter to use - inheritable_setting :authorization_adapter, ActiveAdmin::AuthorizationAdapter - - # A proc to be used when a user is not authorized to view the current resource - inheritable_setting :on_unauthorized_access, :rescue_active_admin_access_denied + def settings + @settings ||= SettingsNode.build(ApplicationSettings) + end - # Active Admin makes educated guesses when displaying objects, this is - # the list of methods it tries calling in order - setting :display_name_methods, [ :display_name, - :full_name, - :name, - :username, - :login, - :title, - :email, - :to_s ] + def namespace_settings + @namespace_settings ||= SettingsNode.build(NamespaceSettings) + end - # == Deprecated Settings + def respond_to_missing?(method, include_private = false) + [settings, namespace_settings].any? { |sets| sets.respond_to?(method) } || super + end - # (none currently) + def method_missing(method, *args) + if settings.respond_to?(method) + settings.send(method, *args) + elsif namespace_settings.respond_to?(method) + namespace_settings.send(method, *args) + else + super + end + end - include AssetRegistration + attr_reader :namespaces + def initialize + @namespaces = Namespace::Store.new + end # Event that gets triggered on load of Active Admin - BeforeLoadEvent = 'active_admin.application.before_load'.freeze - AfterLoadEvent = 'active_admin.application.after_load'.freeze + BeforeLoadEvent = "active_admin.application.before_load".freeze + AfterLoadEvent = "active_admin.application.after_load".freeze + # Runs before the app's AA initializer def setup! - register_default_assets end + # Runs after the app's AA initializer def prepare! remove_active_admin_load_paths_from_rails_autoload_and_eager_load attach_reloader - generate_stylesheets end # Registers a brand new configuration for the given resource. def register(resource, options = {}, &block) - ns_name = namespace_name(options) - namespace(ns_name).register resource, options, &block + ns = options.fetch(:namespace) { default_namespace } + namespace(ns).register resource, options, &block end # Creates a namespace for the given name # # Yields the namespace if a block is given # - # @returns [Namespace] the new or existing namespace + # @return [Namespace] the new or existing namespace def namespace(name) name ||= :root - if namespaces[name] - namespace = namespaces[name] - else - namespace = namespaces[name] = Namespace.new(self, name) - ActiveAdmin::Event.dispatch ActiveAdmin::Namespace::RegisterEvent, namespace + namespace = namespaces[name.to_sym] ||= begin + namespace = Namespace.new(self, name) + ActiveSupport::Notifications.instrument ActiveAdmin::Namespace::RegisterEvent, { active_admin_namespace: namespace } + namespace end yield(namespace) if block_given? @@ -136,12 +85,12 @@ def namespace(name) # Register a page # # @param name [String] The page name - # @options [Hash] Accepts option :namespace. + # @option [Hash] Accepts option :namespace. # @&block The registration block. # def register_page(name, options = {}, &block) - ns_name = namespace_name(options) - namespace(ns_name).register_page name, options, &block + ns = options.fetch(:namespace) { default_namespace } + namespace(ns).register_page name, options, &block end # Whether all configuration files have been loaded @@ -152,7 +101,7 @@ def loaded? # Removes all defined controllers from memory. Useful in # development, where they are reloaded on each request. def unload! - namespaces.values.each{ |namespace| namespace.unload! } + namespaces.each(&:unload!) @@loaded = false end @@ -160,91 +109,117 @@ def unload! # To reload everything simply call `ActiveAdmin.unload!` def load! unless loaded? - ActiveAdmin::Event.dispatch BeforeLoadEvent, self # before_load hook - files.each{ |file| load file } # load files - namespace(default_namespace) # init AA resources - ActiveAdmin::Event.dispatch AfterLoadEvent, self # after_load hook + ActiveSupport::Notifications.instrument BeforeLoadEvent, { active_admin_application: self } # before_load hook + files.each { |file| load file } # load files + namespace(default_namespace) # init AA resources + ActiveSupport::Notifications.instrument AfterLoadEvent, { active_admin_application: self } # after_load hook @@loaded = true end end - # Returns ALL the files to be loaded - def files - load_paths.flatten.compact.uniq.map{ |path| Dir["#{path}/**/*.rb"] }.flatten + def load(file) + DatabaseHitDuringLoad.capture { super } end - def router - @router ||= Router.new(self) + # Returns ALL the files to be loaded + def files + load_paths.flatten.compact.uniq.flat_map { |path| Dir["#{path}/**/*.rb"].sort } end - # One-liner called by user's config/routes.rb file + # Creates all the necessary routes for the ActiveAdmin configurations + # + # Use this within the routes.rb file: + # + # Application.routes.draw do |map| + # ActiveAdmin.routes(self) + # end + # + # @param rails_router [ActionDispatch::Routing::Mapper] def routes(rails_router) load! - router.apply(rails_router) + Router.new(router: rails_router, namespaces: namespaces).apply end # Adds before, around and after filters to all controllers. # Example usage: - # ActiveAdmin.before_filter :authenticate_admin! + # ActiveAdmin.before_action :authenticate_admin! # - %w(before_filter skip_before_filter after_filter around_filter).each do |name| + AbstractController::Callbacks::ClassMethods.public_instance_methods. + select { |m| m.end_with?('_action') }.each do |name| define_method name do |*args, &block| - ActiveAdmin::BaseController.send name, *args, &block - ActiveAdmin::Devise::PasswordsController.send name, *args, &block - ActiveAdmin::Devise::SessionsController.send name, *args, &block - ActiveAdmin::Devise::UnlocksController.send name, *args, &block + ActiveSupport.on_load(:active_admin_controller) do + public_send name, *args, &block + end end end - # Helper method to add a dashboard section - def dashboard_section(name, options = {}, &block) - ActiveAdmin::Dashboards.add_section(name, options, &block) + def controllers_for_filters + controllers = [BaseController] + controllers.push(*Devise.controllers_for_filters) if Dependency.devise? + controllers end private - # Return either the passed in namespace or the default - def namespace_name(options) - options.fetch(:namespace){ default_namespace } - end - - def register_default_assets - register_stylesheet 'active_admin.css', :media => 'screen' - register_stylesheet 'active_admin/print.css', :media => 'print' - - unless ActiveAdmin.use_asset_pipeline? - register_javascript 'jquery.min.js' - register_javascript 'jquery-ui.min.js' - register_javascript 'jquery_ujs.js' - end - - register_javascript 'active_admin.js' - end - - # Since we're dealing with all our own file loading, we need - # to remove our paths from the ActiveSupport autoload paths. - # If not, file naming becomes very important and can cause clashes. + # Since app/admin is alphabetically before app/models, we have to remove it + # from the host app's +autoload_paths+ to prevent missing constant errors. + # + # As well, we have to remove it from +eager_load_paths+ to prevent the + # files from being loaded twice in production. def remove_active_admin_load_paths_from_rails_autoload_and_eager_load - ActiveSupport::Dependencies.autoload_paths.reject!{|path| load_paths.include?(path) } - # Don't eagerload our configs, we'll deal with them ourselves - Rails.application.config.eager_load_paths = Rails.application.config.eager_load_paths.reject do |path| - load_paths.include?(path) - end + ActiveSupport::Dependencies.autoload_paths -= load_paths + Rails.application.config.eager_load_paths -= load_paths end + # Hook into the Rails code reloading mechanism so that things are reloaded + # properly in development mode. + # + # If any of the app files (e.g. models) has changed, we need to reload all + # the admin files. If the admin files themselves has changed, we need to + # regenerate the routes as well. def attach_reloader - ActiveAdmin::Reloader.build(Rails.application, self, Rails.version).attach! - end - - def generate_stylesheets - # Create our own asset pipeline in Rails 3.0 - if ActiveAdmin.use_asset_pipeline? - # Add our mixins to the load path for SASS - ::Sass::Engine::DEFAULT_OPTIONS[:load_paths] << File.expand_path("../../../app/assets/stylesheets", __FILE__) - else - require 'active_admin/sass/css_loader' - ::Sass::Plugin.add_template_location(File.expand_path("../../../app/assets/stylesheets", __FILE__)) - ::Sass::Plugin.add_template_location(File.expand_path("../sass", __FILE__)) + Rails.application.config.after_initialize do |app| + unload_active_admin = -> { ActiveAdmin.application.unload! } + + if app.config.reload_classes_only_on_change + # Rails is about to unload all the app files (e.g. models), so we + # should first unload the classes generated by Active Admin, otherwise + # they will contain references to the stale (unloaded) classes. + ActiveSupport::Reloader.to_prepare(prepend: true, &unload_active_admin) + else + # If the user has configured the app to always reload app files after + # each request, so we should unload the generated classes too. + ActiveSupport::Reloader.to_complete(&unload_active_admin) + end + + admin_dirs = {} + + load_paths.each do |path| + admin_dirs[path] = [:rb] + end + + routes_reloader = app.config.file_watcher.new([], admin_dirs) do + app.reload_routes! + end + + app.reloaders << routes_reloader + + ActiveSupport::Reloader.to_prepare do + # Rails might have reloaded the routes for other reasons (e.g. + # routes.rb has changed), in which case Active Admin would have been + # loaded via the `ActiveAdmin.routes` call in `routes.rb`. + # + # Otherwise, we should check if any of the admin files are changed + # and force the routes to reload if necessary. This would again causes + # Active Admin to load via `ActiveAdmin.routes`. + # + # Finally, if Active Admin is still not loaded at this point, then we + # would need to load it manually. + unless ActiveAdmin.application.loaded? + routes_reloader.execute_if_updated + ActiveAdmin.application.load! + end + end end end end diff --git a/lib/active_admin/application_settings.rb b/lib/active_admin/application_settings.rb new file mode 100644 index 00000000000..362705b21fd --- /dev/null +++ b/lib/active_admin/application_settings.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true +require_relative "settings_node" + +module ActiveAdmin + class ApplicationSettings < SettingsNode + + # The default namespace to put controllers and routes inside. Set this + # in config/initializers/active_admin.rb using: + # + # config.default_namespace = :super_admin + # + register :default_namespace, :admin + + register :app_path, Rails.root + + # Load paths for admin configurations. Add folders to this load path + # to load up other resources for administration. External gems can + # include their paths in this load path to provide active_admin UIs + register :load_paths, [File.expand_path("app/admin", Rails.root)] + + # Set default localize format for Date/Time values + register :localize_format, :long + + # Active Admin makes educated guesses when displaying objects, this is + # the list of methods it tries calling in order + # Note that Formtastic also has 'collection_label_methods' similar to this + # used by auto generated dropdowns in filter or belongs_to field of Active Admin + register :display_name_methods, [ :display_name, + :full_name, + :name, + :username, + :login, + :title, + :email, + :to_s ] + + # To make debugging easier, by default don't stream in development + register :disable_streaming_in, ["development"] + + # Remove sensitive attributes from being displayed, made editable, or exported by default + register :filter_attributes, [:encrypted_password, :password, :password_confirmation] + end +end diff --git a/lib/active_admin/asset_registration.rb b/lib/active_admin/asset_registration.rb deleted file mode 100644 index 15dd90cce5a..00000000000 --- a/lib/active_admin/asset_registration.rb +++ /dev/null @@ -1,29 +0,0 @@ -module ActiveAdmin - module AssetRegistration - - def register_stylesheet(path, options = {}) - stylesheets[path] = options - end - - def stylesheets - @stylesheets ||= {} - end - - def clear_stylesheets! - @stylesheets = {} - end - - def register_javascript(name) - javascripts.add name - end - - def javascripts - @javascripts ||= Set.new - end - - def clear_javascripts! - @javascripts = Set.new - end - - end -end diff --git a/lib/active_admin/async_count.rb b/lib/active_admin/async_count.rb new file mode 100644 index 00000000000..6becc62fcd9 --- /dev/null +++ b/lib/active_admin/async_count.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true +module ActiveAdmin + class AsyncCount + class NotSupportedError < RuntimeError; end + + def initialize(collection) + raise NotSupportedError, "#{collection.inspect} does not support :async_count" unless collection.respond_to?(:async_count) + + @collection = collection.except(:select, :order) + @promise = @collection.async_count + end + + def count + @promise.value + end + + alias size count + + delegate :except, :group_values, :length, :limit_value, to: :@collection + end +end diff --git a/lib/active_admin/authorization_adapter.rb b/lib/active_admin/authorization_adapter.rb index 5d59798ff43..8a2e4d9772e 100644 --- a/lib/active_admin/authorization_adapter.rb +++ b/lib/active_admin/authorization_adapter.rb @@ -1,16 +1,18 @@ +# frozen_string_literal: true module ActiveAdmin # Default Authorization permissions for Active Admin module Authorization - READ = :read - CREATE = :create - UPDATE = :update + READ = :read + NEW = :new + CREATE = :create + EDIT = :edit + UPDATE = :update DESTROY = :destroy end Auth = Authorization - # Active Admin's default authorization adapter. This adapter returns true # for all requests to `#authorized?`. It should be the starting point for # implementing your own authorization adapter. @@ -19,7 +21,6 @@ module Authorization class AuthorizationAdapter attr_reader :resource, :user - # Initialize a new authorization adapter. This happens on each and # every request to a controller. # @@ -47,14 +48,13 @@ def initialize(resource, user) # the class of the subject also. For example, Active Admin uses the class # of the resource to decide if the resource should be displayed in the # global navigation. To deal with this nicely in a case statement, take - # a look at `#normalized(klasss)` + # a look at `#normalized(klass)` # - # @returns [Boolean] + # @return [Boolean] def authorized?(action, subject = nil) true end - # A hook method for authorization libraries to scope the collection. By # default, we just return the same collection. The returned scope is used # as the starting point for all queries to the db in the controller. @@ -66,7 +66,7 @@ def authorized?(action, subject = nil) # one of the `ActiveAdmin::Auth::*` symbols. Defaults to `Auth::READ` if # no action passed in. # - # @returns [ActiveRecord::Relation] A new collection, scoped to the + # @return [ActiveRecord::Relation] A new collection, scoped to the # objects that the current user has access to. def scope_collection(collection, action = Auth::READ) collection diff --git a/lib/active_admin/base_controller/authorization.rb b/lib/active_admin/base_controller/authorization.rb deleted file mode 100644 index 9c81facb1c6..00000000000 --- a/lib/active_admin/base_controller/authorization.rb +++ /dev/null @@ -1,153 +0,0 @@ -module ActiveAdmin - - # Exception class to raise when there is an authorized access - # exception thrown. The exception has a few goodies that may - # be useful for capturing / recognizing security issues. - class AccessDenied < StandardError - attr_reader :user, :action, :subject - - def initialize(user, action, subject) - @user, @action, @subject = user, action, subject - - super() - end - - def message - I18n.t("active_admin.access_denied.message") - end - end - - - class BaseController < ::InheritedResources::Base - module Authorization - include MethodOrProcHelper - extend ActiveSupport::Concern - - ACTIONS_DICTIONARY = { - :index => ActiveAdmin::Authorization::READ, - :show => ActiveAdmin::Authorization::READ, - :new => ActiveAdmin::Authorization::CREATE, - :create => ActiveAdmin::Authorization::CREATE, - :edit => ActiveAdmin::Authorization::UPDATE, - :update => ActiveAdmin::Authorization::UPDATE, - :destroy => ActiveAdmin::Authorization::DESTROY - } - - included do - rescue_from ActiveAdmin::AccessDenied, :with => :dispatch_active_admin_access_denied - - helper_method :authorized? - helper_method :authorize! - end - - protected - - # Authorize the action and subject. Available in the controller - # as well as all the views. - # - # @param [Symbol] action The action to check if the user has permission - # to perform on the subject. - # - # @param [any] subject The subject that the user is trying to perform - # the action on. - # - # @returns [Boolean] - # - def authorized?(action, subject = nil) - active_admin_authorization.authorized?(action, subject) - end - - - # Authorize the action and subject. Available in the controller - # as well as all the views. If the action is not allowd, it raises - # an ActiveAdmin::AccessDenied exception. - # - # @param [Symbol] action The action to check if the user has permission - # to perform on the subject. - # - # @param [any] subject The subject that the user is trying to perform - # the action on. - # - # @returns [Boolean] True if authorized, otherwise raises - # an ActiveAdmin::AccessDenied. - def authorize!(action, subject = nil) - unless authorized? action, subject - raise ActiveAdmin::AccessDenied.new(current_active_admin_user, - action, - subject) - end - end - - # Performs authorization on the resource using the current controller - # action as the permission action. - # - def authorize_resource!(resource) - permission = action_to_permission(params[:action]) - authorize! permission, resource - end - - # Retrieve or instantiate the authorization instance for this resource - # - # @returns [ActiveAdmin::AuthorizationAdapter] - def active_admin_authorization - @active_admin_authorization ||= active_admin_authorization_adapter.new(active_admin_config, current_active_admin_user) - end - - # Returns the class to be used as the authorization adapter - # - # @returns [Class] - def active_admin_authorization_adapter - if active_admin_namespace.authorization_adapter.is_a?(String) - ActiveSupport::Dependencies.constantize(active_admin_namespace.authorization_adapter) - else - active_admin_namespace.authorization_adapter - end - end - - # Converts a controller action into one of the correct Active Admin - # authorization names. Uses the ACTIONS_DICTIONARY to convert the - # action name to permission. - # - # @param [String, Symbol] action The controller action name. - # - # @returns [Symbol] The permission name to use. - def action_to_permission(action) - return nil unless action - - action = action.to_sym - - if Authorization::ACTIONS_DICTIONARY.has_key?(action) - Authorization::ACTIONS_DICTIONARY[action] - else - action - end - end - - def dispatch_active_admin_access_denied(exception) - call_method_or_exec_proc active_admin_namespace.on_unauthorized_access, exception - end - - def rescue_active_admin_access_denied(exception) - error_message = exception.message - - respond_to do |format| - format.html do - flash[:error] = error_message - - if request.headers.keys.include?("HTTP_REFERER") - redirect_to :back - else - controller, action = active_admin_namespace.root_to.split("#") - redirect_to :controller => controller, :action => action - end - end - - format.csv { render :text => error_message, :status => :unauthorized} - format.json { render :json => { :error => error_message }, :status => :unauthorized} - format.xml { render :xml => "#{error_message}", :status => :unauthorized} - end - end - - end - end -end diff --git a/lib/active_admin/base_controller/menu.rb b/lib/active_admin/base_controller/menu.rb deleted file mode 100644 index 52b7a49b0a2..00000000000 --- a/lib/active_admin/base_controller/menu.rb +++ /dev/null @@ -1,34 +0,0 @@ -module ActiveAdmin - class BaseController < ::InheritedResources::Base - module Menu - extend ActiveSupport::Concern - - included do - before_filter :set_current_tab - helper_method :current_menu - end - - protected - - def current_menu - active_admin_config.navigation_menu - end - - # Set's @current_tab to be name of the tab to mark as current - # Get's called through a before filter - def set_current_tab - @current_tab = if current_menu && active_admin_config.belongs_to? && parent? - parent_item = active_admin_config.belongs_to_config.target.menu_item - if current_menu.include? parent_item - parent_item - else - active_admin_config.menu_item - end - else - active_admin_config.menu_item - end - end - - end - end -end diff --git a/lib/active_admin/batch_actions.rb b/lib/active_admin/batch_actions.rb index a6864da1097..91a362da38b 100644 --- a/lib/active_admin/batch_actions.rb +++ b/lib/active_admin/batch_actions.rb @@ -1,17 +1,12 @@ +# frozen_string_literal: true ActiveAdmin.before_load do |app| - require "active_admin/batch_actions/resource_extension" - require "active_admin/batch_actions/controller" + require_relative "batch_actions/resource_extension" + require_relative "batch_actions/controller" # Add our Extensions ActiveAdmin::Resource.send :include, ActiveAdmin::BatchActions::ResourceExtension ActiveAdmin::ResourceController.send :include, ActiveAdmin::BatchActions::Controller - # Require all the views - require "active_admin/batch_actions/views/batch_action_form" - require "active_admin/batch_actions/views/batch_action_popover" - require "active_admin/batch_actions/views/selection_cells" - require "active_admin/batch_actions/views/batch_action_selector" - - # Register the views with the view factory - app.view_factory.register :batch_action_selector => ActiveAdmin::BatchActions::BatchActionSelector + require_relative "batch_actions/views/batch_action_form" + require_relative "batch_actions/views/selection_cells" end diff --git a/lib/active_admin/batch_actions/controller.rb b/lib/active_admin/batch_actions/controller.rb index ee9685a0ad0..e3cb588ecc9 100644 --- a/lib/active_admin/batch_actions/controller.rb +++ b/lib/active_admin/batch_actions/controller.rb @@ -1,13 +1,13 @@ +# frozen_string_literal: true module ActiveAdmin module BatchActions module Controller - - # Controller Action that get's called when submitting the batch action form + # Controller action that is called when submitting the batch action form def batch_action - if selected_batch_action - selected_ids = params[:collection_selection] - selected_ids ||= [] - instance_exec selected_ids, &selected_batch_action.block + if action_present? + selection = params[:collection_selection] || [] + inputs = JSON.parse(params[:batch_action_inputs].presence || "{}") + instance_exec selection, inputs, ¤t_batch_action.block else raise "Couldn't find batch action \"#{params[:batch_action]}\"" end @@ -15,11 +15,24 @@ def batch_action protected - def selected_batch_action - return unless params[:batch_action].present? - active_admin_config.batch_actions.select { |action| action.sym == params[:batch_action].to_sym }.first + def action_present? + params[:batch_action].present? && current_batch_action + end + + def current_batch_action + active_admin_config.batch_actions.detect { |action| action.sym.to_s == params[:batch_action] } end + COLLECTION_APPLIES = [ + :authorization_scope, + :filtering, + :scoping, + :includes, + ].freeze + + def batch_action_collection(only = COLLECTION_APPLIES) + find_collection(only: only) + end end end end diff --git a/lib/active_admin/batch_actions/resource_extension.rb b/lib/active_admin/batch_actions/resource_extension.rb index a1724276c2a..a4b9b8f6636 100644 --- a/lib/active_admin/batch_actions/resource_extension.rb +++ b/lib/active_admin/batch_actions/resource_extension.rb @@ -1,11 +1,11 @@ +# frozen_string_literal: true module ActiveAdmin - module BatchActions module ResourceExtension def initialize(*) super @batch_actions = {} - add_default_batch_actions + add_default_batch_action end # @return [Array] The set of batch actions for this resource @@ -31,7 +31,8 @@ def batch_actions=(bool) # @param [Hash] options # => :if is a proc that will be called to determine if the BatchAction should be displayed # => :sort_order is used to sort the batch actions ascending - # => :confirm is a string which the user will have to accept in order to process the action + # => :confirm is a string to prompt the user with (or a boolean to use the default message) + # => :form is a Hash of form fields you want the user to fill out # def add_batch_action(sym, title, options = {}, &block) @batch_actions[sym] = ActiveAdmin::BatchAction.new(sym, title, options, &block) @@ -39,7 +40,7 @@ def add_batch_action(sym, title, options = {}, &block) # Remove a batch action # @param [Symbol] sym - # @returns [ActiveAdmin::BatchAction] the batch action, if it was present + # @return [ActiveAdmin::BatchAction] the batch action, if it was present # def remove_batch_action(sym) @batch_actions.delete(sym.to_sym) @@ -50,29 +51,28 @@ def clear_batch_actions! @batch_actions = {} end - # Path to the batch action itself - def batch_action_path(params = {}) - [route_collection_path(params), "batch_action"].join("/") - end - private # @return [ActiveAdmin::BatchAction] The default "delete" action - def add_default_batch_actions + def add_default_batch_action destroy_options = { - :priority => 100, - :confirm => proc { I18n.t('active_admin.batch_actions.delete_confirmation', :plural_model => active_admin_config.plural_resource_label.downcase) }, - :if => proc{ controller.action_methods.include?('destroy') && authorized?(ActiveAdmin::Auth::DESTROY, active_admin_config.resource_class) } + priority: 100, + confirm: proc { I18n.t("active_admin.batch_actions.delete_confirmation", plural_model: active_admin_config.plural_resource_label.downcase) }, + if: proc { destroy_action_authorized?(active_admin_config.resource_class) } } - add_batch_action :destroy, proc { I18n.t('active_admin.delete') }, destroy_options do |selected_ids| - active_admin_config.resource_class.find(selected_ids).each { |r| r.destroy } + add_batch_action :destroy, proc { I18n.t("active_admin.delete") }, destroy_options do |selected_ids| + batch_action_collection.find(selected_ids).each do |record| + authorize! ActiveAdmin::Auth::DESTROY, record + destroy_resource(record) + end redirect_to active_admin_config.route_collection_path(params), - :notice => I18n.t("active_admin.batch_actions.succesfully_destroyed", - :count => selected_ids.count, - :model => active_admin_config.resource_label.downcase, - :plural_model => active_admin_config.plural_resource_label(:count => selected_ids.count).downcase) + notice: I18n.t( + "active_admin.batch_actions.successfully_destroyed", + count: selected_ids.count, + model: active_admin_config.resource_label.downcase, + plural_model: active_admin_config.plural_resource_label(count: selected_ids.count).downcase) end end @@ -83,7 +83,9 @@ class BatchAction include Comparable - attr_reader :block, :title, :sym, :confirm + attr_reader :block, :title, :sym, :partial, :link_html_options + + DEFAULT_CONFIRM_MESSAGE = proc { I18n.t "active_admin.batch_actions.default_confirmation" } # Create a Batch Action # @@ -92,20 +94,43 @@ class BatchAction # BatchAction.new :flag # => Will create an action that appears in the action list popover # - # BatchAction.new( :flag ) { |selection| redirect_to collection_path, :notice => "#{selection.length} users flagged" } - # => Will create an action that uses a block to process the request (which receives one paramater of the selected objects) + # BatchAction.new(:flag) { |selection| redirect_to collection_path, notice: "#{selection.length} users flagged" } + # => Will create an action that uses a block to process the request (which receives one parameter of the selected objects) # - # BatchAction.new( "Perform Long Operation on the" ) { |selection| } + # BatchAction.new("Perform Long Operation on") { |selection| } # => You can create batch actions with a title instead of a Symbol # - # BatchAction.new( :flag, :if => proc { can? :flag, AdminUser } ) { |selection| } - # => You can provide an optional :if proc to optionally display the batch action + # BatchAction.new(:flag, if: proc{ can? :flag, AdminUser }) { |selection| } + # => You can provide an `:if` proc to choose when the batch action should be displayed + # + # BatchAction.new :flag, confirm: true + # => You can pass `true` to `:confirm` to use the default confirm message. + # + # BatchAction.new(:flag, confirm: "Are you sure?") { |selection| } + # => You can pass a custom confirmation message through `:confirm` + # + # BatchAction.new(:flag, partial: "flag_form", link_html_options: { "data-modal-target": "modal-id", "data-modal-show": "modal-id" }) { |selection, inputs| } + # => Pass a partial that contains a modal and with a data attribute that opens the modal with the form for the user to fill out. # def initialize(sym, title, options = {}, &block) - @sym, @title, @options, @block, @confirm = sym, title, options, block, options[:confirm] + @sym = sym + @title = title + @options = options + @block = block + @confirm = options[:confirm] + @partial = options[:partial] + @link_html_options = options[:link_html_options] || {} @block ||= proc {} end + def confirm + if @confirm == true + DEFAULT_CONFIRM_MESSAGE + else + @confirm + end + end + # Returns the display if block. If the block was not explicitly defined # a default block always returning true will be returned. def display_if_block @@ -121,7 +146,5 @@ def priority def <=>(other) self.priority <=> other.priority end - end - end diff --git a/lib/active_admin/batch_actions/views/batch_action_form.rb b/lib/active_admin/batch_actions/views/batch_action_form.rb index 54531bf77ec..6dbf41f131b 100644 --- a/lib/active_admin/batch_actions/views/batch_action_form.rb +++ b/lib/active_admin/batch_actions/views/batch_action_form.rb @@ -1,20 +1,19 @@ -require 'active_admin/component' +# frozen_string_literal: true +require_relative "../../component" module ActiveAdmin module BatchActions - - # Build an BatchActionForm + # Build a BatchActionForm class BatchActionForm < ActiveAdmin::Component - builder_method :batch_action_form - - attr_reader :prefix_html - def build(options = {}, &block) options[:id] ||= "collection_selection" - # Open a form - text_node form_tag(active_admin_config.batch_action_path(params), :id => options[:id]) - input(:name => :batch_action, :id => :batch_action, :type => :hidden) + # Open a form with two hidden input fields: + # batch_action => name of the specific action called + # batch_action_inputs => a JSON string of any requested confirmation values + text_node form_tag active_admin_config.route_batch_action_path(params, url_options), id: options[:id] + input name: :batch_action, id: :batch_action, type: :hidden + input name: :batch_action_inputs, id: :batch_action_inputs, type: :hidden super(options) end @@ -27,9 +26,8 @@ def to_s private def closing_form_tag - ''.html_safe + "".html_safe end - end end end diff --git a/lib/active_admin/batch_actions/views/batch_action_popover.rb b/lib/active_admin/batch_actions/views/batch_action_popover.rb deleted file mode 100644 index d7ec0ece4b0..00000000000 --- a/lib/active_admin/batch_actions/views/batch_action_popover.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'active_admin/views/components/action_list_popover' - -module ActiveAdmin - module BatchActions - # Build an BatchActionPopover - class BatchActionPopover < ActiveAdmin::Views::ActionListPopover - builder_method :batch_action_popover - - def build(options = {}, &block) - options[:id] ||= "batch_actions_popover" - super(options) - end - - def action(batch_action, options = {}) - options[:class] ||= [] - options[:class] += %w(batch_action) - options.merge! "data-action" => batch_action.sym, - "data-confirm" => batch_action.confirm - - title = I18n.t("active_admin.batch_actions.labels.#{batch_action.sym}", :default => batch_action.title) - label = I18n.t("active_admin.batch_actions.action_label", :title => title) - - super(label, "#", options) - end - - end - end -end diff --git a/lib/active_admin/batch_actions/views/batch_action_selector.rb b/lib/active_admin/batch_actions/views/batch_action_selector.rb deleted file mode 100644 index 246ced1b2a2..00000000000 --- a/lib/active_admin/batch_actions/views/batch_action_selector.rb +++ /dev/null @@ -1,66 +0,0 @@ -require 'active_admin/component' - -module ActiveAdmin - module BatchActions - - class BatchActionSelector < ActiveAdmin::Component - builder_method :batch_action_selector - - # Build a new batch actions selector - # - # @param [Array] batch_actions An array of batch actions - def build(batch_actions) - @batch_actions = Array(batch_actions) - @drop_down = build_drop_down - end - - # We don't want to wrap the action list (or any other children) in - # an unecessary div, so instead we just return the children - def to_s - children.to_s - end - - private - - def build_drop_down - dropdown_menu I18n.t("active_admin.batch_actions.button_label"), - :id => "batch_actions_selector", - :button => { :class => "disabled" } do - batch_actions_to_display.each do |batch_action| - confirmation_text = render_or_call_method_or_proc_on(self, batch_action.confirm) - - options = { - :class => "batch_action", - "data-action" => batch_action.sym, - "data-confirm" => confirmation_text - } - - default_title = render_or_call_method_or_proc_on(self, batch_action.title) - title = I18n.t("active_admin.batch_actions.labels.#{batch_action.sym}", :default => default_title) - label = I18n.t("active_admin.batch_actions.action_label", :title => title) - - item label, "#", options - end - end - end - - # Return the set of batch actions that should be displayed - def batch_actions_to_display - @batch_actions.select do |batch_action| - call_method_or_proc_on(self, batch_action.display_if_block) - end - end - - # def build_batch_action_button - # a :class => 'table_tools_button dropdown_button disabled', :href => "#batch_actions_popover", :id => "batch_actions_button" do - # text_node I18n.t("active_admin.batch_actions.button_label") - # end - # end - - # def build_batch_action_popover - # end - - end - - end -end diff --git a/lib/active_admin/batch_actions/views/selection_cells.rb b/lib/active_admin/batch_actions/views/selection_cells.rb index 4fccaade5ff..57d1afeb4cd 100644 --- a/lib/active_admin/batch_actions/views/selection_cells.rb +++ b/lib/active_admin/batch_actions/views/selection_cells.rb @@ -1,4 +1,5 @@ -require 'active_admin/component' +# frozen_string_literal: true +require_relative "../../component" module ActiveAdmin module BatchActions @@ -7,8 +8,11 @@ module BatchActions class ResourceSelectionToggleCell < ActiveAdmin::Component builder_method :resource_selection_toggle_cell - def build - input( :type => "checkbox", :id => "collection_selection_toggle_all", :name => "collection_selection_toggle_all", :class => "toggle_all" ) + def build(label_text = "") + label do + input type: "checkbox", id: "collection_selection_toggle_all", name: "collection_selection_toggle_all", class: "batch-actions-toggle-all" + text_node label_text if label_text.present? + end end end @@ -17,7 +21,7 @@ class ResourceSelectionCell < ActiveAdmin::Component builder_method :resource_selection_cell def build(resource) - input :type => "checkbox", :id => "batch_action_item_#{resource.id}", :value => resource.id, :class => "collection_selection", :name => "collection_selection[]" + input type: "checkbox", id: "batch_action_item_#{resource.id}", value: resource.id, class: "batch-actions-resource-selection", name: "collection_selection[]" end end @@ -26,11 +30,9 @@ class ResourceSelectionTogglePanel < ActiveAdmin::Component builder_method :resource_selection_toggle_panel def build - super(:id => "collection_selection_toggle_panel") - resource_selection_toggle_cell - div(:id => "collection_selection_toggle_explaination" ) { I18n.t('active_admin.batch_actions.selection_toggle_explanation', :default => "(Toggle Selection)") } + super(id: "collection_selection_toggle_panel") + resource_selection_toggle_cell(I18n.t("active_admin.batch_actions.selection_toggle_explanation", default: "(Toggle Selection)")) end - end end diff --git a/lib/active_admin/callbacks.rb b/lib/active_admin/callbacks.rb index 903f5b6c989..3ea5811cb37 100644 --- a/lib/active_admin/callbacks.rb +++ b/lib/active_admin/callbacks.rb @@ -1,17 +1,20 @@ +# frozen_string_literal: true module ActiveAdmin module Callbacks extend ActiveSupport::Concern - protected + CALLBACK_TYPES = %i[before after].freeze + + private # Simple callback system. Implements before and after callbacks for # use within the controllers. # - # We didn't use the ActiveSupport callbacks becuase they do not support + # We didn't use the ActiveSupport callbacks because they do not support # passing in any arbitrary object into the callback method (which we # need to do) - def call_callback_with(method, *args) + def run_callback(method, *args) case method when Symbol send(method, *args) @@ -24,6 +27,8 @@ def call_callback_with(method, *args) module ClassMethods + private + # Define a new callback. # # Example: @@ -55,33 +60,32 @@ module ClassMethods # def define_active_admin_callbacks(*names) names.each do |name| - [:before, :after].each do |type| + CALLBACK_TYPES.each do |type| + callback_name = "#{type}_#{name}_callbacks" + callback_ivar = "@#{callback_name}" - # Define a method to set the callback - class_eval(<<-EOS, __FILE__, __LINE__ + 1) - # def self.before_create_callbacks - def self.#{type}_#{name}_callbacks - @#{type}_#{name}_callbacks ||= [] - end + # def self.before_create_callbacks + singleton_class.send :define_method, callback_name do + instance_variable_get(callback_ivar) || instance_variable_set(callback_ivar, []) + end + singleton_class.send :private, callback_name - # def self.before_create - def self.#{type}_#{name}(method = nil, &block) - #{type}_#{name}_callbacks << (method || block) - end - EOS + # def self.before_create + singleton_class.send :define_method, "#{type}_#{name}" do |method = nil, &block| + send(callback_name).push method || block + end end - # Define a method to run the callbacks - class_eval(<<-EOS, __FILE__, __LINE__ + 1) - def run_#{name}_callbacks(*args) - self.class.before_#{name}_callbacks.each{|callback| call_callback_with(callback, *args)} - value = yield if block_given? - self.class.after_#{name}_callbacks.each{|callback| call_callback_with(callback, *args)} - return value - end - EOS - end - end + # def run_create_callbacks + define_method :"run_#{name}_callbacks" do |*args, &block| + self.class.send(:"before_#{name}_callbacks").each { |cbk| run_callback(cbk, *args) } + value = block.try :call + self.class.send(:"after_#{name}_callbacks").each { |cbk| run_callback(cbk, *args) } + return value + end + send :private, "run_#{name}_callbacks" + end + end end end end diff --git a/lib/active_admin/cancan_adapter.rb b/lib/active_admin/cancan_adapter.rb index f25720acd46..51ce4cc2bf0 100644 --- a/lib/active_admin/cancan_adapter.rb +++ b/lib/active_admin/cancan_adapter.rb @@ -1,4 +1,9 @@ -require 'cancan' +# frozen_string_literal: true +unless ActiveAdmin::Dependency.cancan? || ActiveAdmin::Dependency.cancancan? + ActiveAdmin::Dependency.cancan! +end + +require "cancan" # Add a setting to the application to configure the ability ActiveAdmin::Application.inheritable_setting :cancan_ability_class, "Ability" @@ -21,18 +26,10 @@ def scope_collection(collection, action = ActiveAdmin::Auth::READ) private - # The setting allows the class to be stored as a string - # to enable reloading in development. def initialize_cancan_ability - ability_class_name = resource.namespace.cancan_ability_class - - if ability_class_name.is_a?(String) - ability_class = ActiveSupport::Dependencies.constantize(ability_class_name) - else - ability_class = ability_class_name - end - - ability_class.new(user) + klass = resource.namespace.cancan_ability_class + klass = klass.constantize if klass.is_a? String + klass.new user end end diff --git a/lib/active_admin/collection_decorator.rb b/lib/active_admin/collection_decorator.rb new file mode 100644 index 00000000000..964622d8f44 --- /dev/null +++ b/lib/active_admin/collection_decorator.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true +module ActiveAdmin + # This class decorates a collection of objects delegating + # methods to behave like an Array. It's used to decouple ActiveAdmin + # from Draper and thus being able to use PORO decorators as well. + # + # It's implementation is heavily based on the Draper::CollectionDecorator + # https://github.com/drapergem/draper/blob/aaa06bd2f1e219838b241a5534e7ca513edd1fe2/lib/draper/collection_decorator.rb + class CollectionDecorator + # @return the collection being decorated. + attr_reader :object + + # @return [Class] the decorator class used to decorate each item, as set by {#initialize}. + attr_reader :decorator_class + + array_methods = Array.instance_methods - Object.instance_methods + delegate :==, :as_json, *array_methods, to: :decorated_collection + + def initialize(object, with:) + @object = object + @decorator_class = with + end + + class << self + alias_method :decorate, :new + end + + def decorated_collection + @decorated_collection ||= object.map { |item| decorator_class.new(item) } + end + end +end diff --git a/lib/active_admin/comments.rb b/lib/active_admin/comments.rb deleted file mode 100644 index d34fc5ceca7..00000000000 --- a/lib/active_admin/comments.rb +++ /dev/null @@ -1,79 +0,0 @@ -require 'active_admin/comments/comment' -require 'active_admin/comments/views' -require 'active_admin/comments/show_page_helper' -require 'active_admin/comments/namespace_helper' -require 'active_admin/comments/resource_helper' - -# Add the comments configuration -ActiveAdmin::Application.inheritable_setting :allow_comments, true -ActiveAdmin::Application.inheritable_setting :show_comments_in_menu, true -ActiveAdmin::Application.inheritable_setting :comments_registration_name, 'Comment' - -# Add the comments module to ActiveAdmin::Namespace -ActiveAdmin::Namespace.send :include, ActiveAdmin::Comments::NamespaceHelper - -# Add the comments module to ActiveAdmin::Resource -ActiveAdmin::Resource.send :include, ActiveAdmin::Comments::ResourceHelper - -# Add the module to the show page -ActiveAdmin.application.view_factory.show_page.send :include, ActiveAdmin::Comments::ShowPageHelper - -# Walk through all the loaded resources after they are loaded -ActiveAdmin.after_load do |app| - app.namespaces.values.each do |namespace| - if namespace.comments? - namespace.register ActiveAdmin::Comment, :as => namespace.comments_registration_name do - actions :index, :show, :create - - menu false unless namespace.show_comments_in_menu - - config.comments = false # Don't allow comments on comments - config.batch_actions = false # The default destroy batch action isn't showing up anyway... - - scope :all, :show_count => false - # Register a scope for every namespace that exists. - # The current namespace will be the default scope. - app.namespaces.values.map(&:name).each do |name| - scope name, :default => namespace.name == name do |scope| - scope.where :namespace => name.to_s - end - end - - # Store the author and namespace - before_save do |comment| - comment.namespace = active_admin_config.namespace.name - comment.author = current_active_admin_user - end - - # Redirect to the resource show page after comment creation - controller do - # Prevent N+1 queries - def scoped_collection - resource_class.includes :author, :resource - end unless Rails::VERSION::MAJOR == 3 && Rails::VERSION::MINOR == 0 - def create - create! do |success, failure| - # FYI: below we call `resource.resource`. First is the comment, second is the associated resource. - resource_config = active_admin_config.namespace.resource_for resource.resource.class - resource_url = resource_config.route_instance_path resource.resource - success.html{ redirect_to resource_url } - failure.html do - flash[:error] = I18n.t('active_admin.comments.errors.empty_text') - redirect_to resource_url - end - end - end - end - - index do - column I18n.t('active_admin.comments.resource_type'), :resource_type - column I18n.t('active_admin.comments.author_type'), :author_type - column I18n.t('active_admin.comments.resource'), :resource - column I18n.t('active_admin.comments.author'), :author - column I18n.t('active_admin.comments.body'), :body - actions - end - end - end - end -end diff --git a/lib/active_admin/comments/comment.rb b/lib/active_admin/comments/comment.rb deleted file mode 100644 index e0c1065a6e9..00000000000 --- a/lib/active_admin/comments/comment.rb +++ /dev/null @@ -1,47 +0,0 @@ -require 'kaminari/models/active_record_extension' - -module ActiveAdmin - - # manually initialize kaminari for this model - ::ActiveRecord::Base.send :include, Kaminari::ActiveRecordExtension - - class Comment < ActiveRecord::Base - belongs_to :resource, :polymorphic => true - belongs_to :author, :polymorphic => true - - attr_accessible :resource, :resource_id, :resource_type, :body, :namespace - - validates_presence_of :body, :namespace, :resource - - # @returns [String] The name of the record to use for the polymorphic relationship - def self.resource_type(record) - record.class.base_class.name.to_s - end - - def self.resource_id_cast(record) - # Postgres adapters won't compare strings to numbers (issue 34) - if resource_id_type == :string - record.id.to_s - else - record.id - end - end - - def self.find_for_resource_in_namespace(resource, namespace) - where :resource_type => resource_type(resource), - :resource_id => resource_id_cast(resource), - :namespace => namespace.to_s - end - - def self.resource_id_type - columns.detect{ |i| i.name == "resource_id" }.type - end - - def self.table_name - @table_name ||= ActiveRecord::Migrator.proper_table_name("active_admin_comments") - end - - end - -end - diff --git a/lib/active_admin/comments/show_page_helper.rb b/lib/active_admin/comments/show_page_helper.rb deleted file mode 100644 index 3f8529569bf..00000000000 --- a/lib/active_admin/comments/show_page_helper.rb +++ /dev/null @@ -1,23 +0,0 @@ -module ActiveAdmin - module Comments - - # Adds #active_admin_comments to the show page for use - # and sets it up on the default main content - module ShowPageHelper - - # Add admin comments to the main content if they are - # turned on for the current resource - def default_main_content - super - active_admin_comments if active_admin_config.comments? - end - - # Display the comments for the resource. Same as calling - # #active_admin_comments_for with the current resource - def active_admin_comments(*args, &block) - active_admin_comments_for(resource, *args, &block) - end - end - - end -end diff --git a/lib/active_admin/comments/views.rb b/lib/active_admin/comments/views.rb deleted file mode 100644 index f4b811164da..00000000000 --- a/lib/active_admin/comments/views.rb +++ /dev/null @@ -1,2 +0,0 @@ -require 'active_admin/views' -require 'active_admin/comments/views/active_admin_comments' diff --git a/lib/active_admin/comments/views/active_admin_comments.rb b/lib/active_admin/comments/views/active_admin_comments.rb deleted file mode 100644 index cd5978a6648..00000000000 --- a/lib/active_admin/comments/views/active_admin_comments.rb +++ /dev/null @@ -1,75 +0,0 @@ -require 'active_admin/views' -require 'active_admin/views/components/panel' - -module ActiveAdmin - module Comments - module Views - - class Comments < ActiveAdmin::Views::Panel - builder_method :active_admin_comments_for - - def build(resource) - @resource = resource - @comments = ActiveAdmin::Comment.find_for_resource_in_namespace @resource, active_admin_namespace.name - super(title, :for => resource) - build_comments - end - - protected - - def title - I18n.t 'active_admin.comments.title_content', :count => @comments.count - end - - def build_comments - @comments.any? ? @comments.each(&method(:build_comment)) : build_empty_message - build_comment_form - end - - def build_comment(comment) - div :for => comment do - div :class => 'active_admin_comment_meta' do - h4 :class => 'active_admin_comment_author' do - comment.author ? auto_link(comment.author) : 'Anonymous' - end - span pretty_format comment.created_at - end - div :class => 'active_admin_comment_body' do - simple_format comment.body - end - end - end - - def build_empty_message - span I18n.t('active_admin.comments.no_comments_yet'), :class => 'empty' - end - - def comment_form_url - parts = [] - parts << active_admin_namespace.name unless active_admin_namespace.root? - parts << active_admin_namespace.comments_registration_name.underscore.pluralize - parts << 'path' - send parts.join '_' - end - - def build_comment_form - self << active_admin_form_for(ActiveAdmin::Comment.new, :url => comment_form_url) do |f| - f.inputs do - f.input :resource_type, :as => :hidden, :input_html => { :value => ActiveAdmin::Comment.resource_type(@resource) } - f.input :resource_id, :as => :hidden, :input_html => { :value => @resource.id } - f.input :body, :label => false, :input_html => { :size => '80x8' } - end - f.actions do - f.action :submit, :label => I18n.t('active_admin.comments.add') - end - end - end - - def default_id_for_prefix - 'active_admin_comments_for' - end - end - - end - end -end diff --git a/lib/active_admin/component.rb b/lib/active_admin/component.rb index 8343552f612..784aa048041 100644 --- a/lib/active_admin/component.rb +++ b/lib/active_admin/component.rb @@ -1,5 +1,5 @@ +# frozen_string_literal: true module ActiveAdmin class Component < Arbre::Component - end end diff --git a/lib/active_admin/controller_action.rb b/lib/active_admin/controller_action.rb index bd657926197..4e92d6903dd 100644 --- a/lib/active_admin/controller_action.rb +++ b/lib/active_admin/controller_action.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true module ActiveAdmin class ControllerAction attr_reader :name def initialize(name, options = {}) - @name, @options = name, options + @name = name + @options = options end def http_verb diff --git a/lib/active_admin/csv_builder.rb b/lib/active_admin/csv_builder.rb index 20c56a60c3c..4122e34b944 100644 --- a/lib/active_admin/csv_builder.rb +++ b/lib/active_admin/csv_builder.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin # CSVBuilder stores CSV configuration # @@ -6,8 +7,11 @@ module ActiveAdmin # csv_builder = CSVBuilder.new # csv_builder.column :id # csv_builder.column("Name") { |resource| resource.full_name } + # csv_builder.column(:name, humanize_name: false) + # csv_builder.column("name", humanize_name: false) { |resource| resource.full_name } # - # csv_builder = CSVBuilder.new :col_sep => ";" + # csv_builder = CSVBuilder.new col_sep: ";" + # csv_builder = CSVBuilder.new humanize_name: false # csv_builder.column :id # # @@ -17,33 +21,124 @@ class CSVBuilder # The CSVBuilder's columns would be Id followed by this # resource's content columns def self.default_for_resource(resource) - new.tap do |csv_builder| - csv_builder.column(:id) - resource.content_columns.each do |content_column| - csv_builder.column(content_column.name.to_sym) - end + new resource: resource do + column :id + resource.content_columns.each { |c| column c } + end + end + + attr_reader :columns, :options, :view_context + + COLUMN_TRANSITIVE_OPTIONS = [:humanize_name].freeze + + def initialize(options = {}, &block) + @resource = options.delete(:resource) + @columns = [] + @options = ActiveAdmin.application.csv_options.merge options + @block = block + end + + def column(name, options = {}, &block) + @columns << Column.new(name, @resource, column_transitive_options.merge(options), block) + end + + def build(controller, csv) + columns = exec_columns controller.view_context + bom = options[:byte_order_mark] + column_names = options.delete(:column_names) { true } + csv_options = options.except :encoding_options, :humanize_name, :byte_order_mark + + csv << bom if bom + + if column_names + csv << CSV.generate_line(columns.map { |c| sanitize(encode(c.name, options)) }, **csv_options) end + + controller.send(:in_paginated_batches) do |resource| + csv << CSV.generate_line(build_row(resource, columns, options), **csv_options) + end + + csv end - attr_reader :columns, :options + def exec_columns(view_context = nil) + @view_context = view_context + @columns = [] # we want to re-render these every instance + instance_exec(&@block) if @block.present? + columns + end - def initialize(options={}, &block) - @columns, @options = [], options - instance_eval &block if block_given? + def build_row(resource, columns, options) + columns.map do |column| + sanitize(encode(call_method_or_proc_on(resource, column.data), options)) + end end - # Add a column - def column(name, &block) - @columns << Column.new(name, block) + def encode(content, options) + if options[:encoding] + if options[:encoding_options] + content.to_s.encode options[:encoding], **options[:encoding_options] + else + content.to_s.encode options[:encoding] + end + else + content + end + end + + def sanitize(content) + Sanitizer.sanitize(content) + end + + def method_missing(method, *args, &block) + if @view_context.respond_to? method + @view_context.public_send method, *args, &block + else + super + end end class Column - attr_reader :name, :data + attr_reader :name, :data, :options + + DEFAULT_OPTIONS = { humanize_name: true } - def initialize(name, block = nil) - @name = name.is_a?(Symbol) ? name.to_s.titleize : name + def initialize(name, resource = nil, options = {}, block = nil) + @options = options.reverse_merge(DEFAULT_OPTIONS) + @name = humanize_name(name, resource, @options[:humanize_name]) @data = block || name.to_sym end + + def humanize_name(name, resource, humanize_name_option) + if humanize_name_option + name.is_a?(Symbol) && resource ? resource.resource_class.human_attribute_name(name) : name.to_s.humanize + else + name.to_s + end + end + end + + private + + def column_transitive_options + @column_transitive_options ||= @options.slice(*COLUMN_TRANSITIVE_OPTIONS) + end + end + + # Prevents CSV Injection according to https://owasp.org/www-community/attacks/CSV_Injection + module Sanitizer + extend self + + ATTACK_CHARACTERS = ['=', '+', '-', '@', "\t", "\r"].freeze + + def sanitize(value) + return "'#{value}" if require_sanitization?(value) + + value + end + + def require_sanitization?(value) + value.is_a?(String) && value.starts_with?(*ATTACK_CHARACTERS) end end end diff --git a/lib/active_admin/dependency.rb b/lib/active_admin/dependency.rb new file mode 100644 index 00000000000..9cb44d3306d --- /dev/null +++ b/lib/active_admin/dependency.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true +module ActiveAdmin + module Dependency + module Requirements + DEVISE = ">= 4.0", "< 6" + end + + # Provides a clean interface to check for gem dependencies at runtime. + # + # ActiveAdmin::Dependency.rails + # => # + # + # ActiveAdmin::Dependency.rails? + # => true + # + # ActiveAdmin::Dependency.rails? '>= 6.1' + # => false + # + # ActiveAdmin::Dependency.rails? '= 6.0.3.2' + # => true + # + # ActiveAdmin::Dependency.rails? '~> 6.0.3' + # => true + # + # ActiveAdmin::Dependency.rails? '>= 6.0.3', '<= 6.1.0' + # => true + # + # ActiveAdmin::Dependency.rails! '5' + # -> ActiveAdmin::DependencyError: You provided rails 4.2.7 but we need: 5. + # + # ActiveAdmin::Dependency.devise! + # -> ActiveAdmin::DependencyError: To use devise you need to specify it in your Gemfile. + # + # + # All but the pessimistic operator (~>) can also be run using Ruby's comparison syntax. + # + # ActiveAdmin::Dependency.rails >= '4.2.7' + # => true + # + # Which is especially useful if you're looking up a gem with dashes in the name. + # + # ActiveAdmin::Dependency['jquery-rails'] < 5 + # => false + # + def self.method_missing(name, *args) + if name[-1] == "?" + Matcher.new(name[0..-2]).match? args + elsif name[-1] == "!" + Matcher.new(name[0..-2]).match! args + else + Matcher.new name.to_s + end + end + + def self.[](name) + Matcher.new name.to_s + end + + class Matcher + attr_reader :name + + def initialize(name) + @name = name + end + + def spec + @spec ||= Gem.loaded_specs[name] + end + + def spec! + spec || raise(DependencyError, "To use #{name} you need to specify it in your Gemfile.") + end + + def match?(*reqs) + !!spec && Gem::Requirement.create(reqs).satisfied_by?(spec.version) + end + + def match!(*reqs) + unless match? reqs + raise DependencyError, "You provided #{spec!.name} #{spec!.version} but we need: #{reqs.join ', '}." + end + end + + include Comparable + + def <=>(other) + spec!.version <=> Gem::Version.create(other) + end + + def inspect + info = spec ? "#{spec.name} #{spec.version}" : "(missing)" + "" + end + end + + end +end diff --git a/lib/active_admin/dependency_checker.rb b/lib/active_admin/dependency_checker.rb deleted file mode 100644 index cb6d8166dfe..00000000000 --- a/lib/active_admin/dependency_checker.rb +++ /dev/null @@ -1,42 +0,0 @@ -require 'rails/version' - -module ActiveAdmin - module DependencyChecker - class << self - def check! - if rails_3_1? - unless meta_search_1_1? && sass_rails_3_1? - warn "ActiveAdmin requires meta_search >= 1.1.0.pre and sass-rails ~> 3.1.0.rc to work with rails >= 3.1.0" - end - end - - if pry_rails_before_0_1_6? - warn "ActiveAdmin is not compatible with pry-rails < 0.1.6. Please upgrade pry-rails." - end - end - - def rails_3_1? - Rails::VERSION::MAJOR == 3 && Rails::VERSION::MINOR >= 1 - end - - def meta_search_1_1? - Gem.loaded_specs['meta_search'].version.to_s >= "1.1" - end - - def sass_rails_3_1? - require 'sass/rails/version' - ::Sass::Rails::VERSION >= "3.1" - rescue LoadError - false - end - - def pry_rails_before_0_1_6? - begin - PryRails::VERSION < "0.1.6" - rescue NameError - false - end - end - end - end -end diff --git a/lib/active_admin/deprecation.rb b/lib/active_admin/deprecation.rb deleted file mode 100644 index 9ab6e06c2b4..00000000000 --- a/lib/active_admin/deprecation.rb +++ /dev/null @@ -1,36 +0,0 @@ -module ActiveAdmin - module Deprecation - module_function - - def warn(message, callstack = caller) - ActiveSupport::Deprecation.warn "Active Admin: #{message}", callstack - end - - # Deprecate a method. - # - # @param [Module] klass the Class or Module to deprecate the method on - # @param [Symbol] method the method to deprecate - # @param [String] message the message to display to the end user - # - # Example: - # - # class MyClass - # def my_method - # # ... - # end - # ActiveAdmin::Deprecation.deprecate self, :my_method, - # "MyClass#my_method is being removed in the next release" - # end - # - def deprecate(klass, method, message) - klass.class_eval <<-EOC, __FILE__, __LINE__ + 1 - alias_method :deprecated_#{method}, :#{method} - def #{method}(*args) - ActiveAdmin::Deprecation.warn('#{message}', caller) - send(:deprecated_#{method}, *args) - end - EOC - end - - end -end diff --git a/lib/active_admin/devise.rb b/lib/active_admin/devise.rb index 472cc753343..8de2fd29603 100644 --- a/lib/active_admin/devise.rb +++ b/lib/active_admin/devise.rb @@ -1,69 +1,90 @@ -require 'devise' +# frozen_string_literal: true +ActiveAdmin::Dependency.devise! ActiveAdmin::Dependency::Requirements::DEVISE + +require "devise" module ActiveAdmin module Devise def self.config - config = { - :path => ActiveAdmin.application.default_namespace, - :controllers => ActiveAdmin::Devise.controllers, - :path_names => { :sign_in => 'login', :sign_out => "logout" } + { + path: ActiveAdmin.application.default_namespace || "/", + controllers: ActiveAdmin::Devise.controllers, + path_names: { sign_in: "login", sign_out: "logout" } } - - if ::Devise.respond_to?(:sign_out_via) - logout_methods = [::Devise.sign_out_via, ActiveAdmin.application.logout_link_method].flatten.uniq - config.merge!( :sign_out_via => logout_methods) - end - - config end def self.controllers { - :sessions => "active_admin/devise/sessions", - :passwords => "active_admin/devise/passwords", - :unlocks => "active_admin/devise/unlocks" + sessions: "active_admin/devise/sessions", + passwords: "active_admin/devise/passwords", + unlocks: "active_admin/devise/unlocks", + registrations: "active_admin/devise/registrations", + confirmations: "active_admin/devise/confirmations" } end module Controller extend ::ActiveSupport::Concern included do - layout 'active_admin_logged_out' - helper ::ActiveAdmin::ViewHelpers + layout "active_admin_logged_out" + helper ::ActiveAdmin::LayoutHelper + helper ::ActiveAdmin::FormHelper end # Redirect to the default namespace on logout def root_path namespace = ActiveAdmin.application.default_namespace.presence - root_path_method = [namespace, :root_path].compact.join('_') + root_path_method = [namespace, :root_path].compact.join("_") - url_helpers = Rails.application.routes.url_helpers - - path = if url_helpers.respond_to? root_path_method - url_helpers.send root_path_method + path = if Helpers::Routes.respond_to? root_path_method + Helpers::Routes.send root_path_method else # Guess a root_path when url_helpers not helpful "/#{namespace}" end - # NOTE: `relative_url_root` is deprecated by rails. + # NOTE: `relative_url_root` is deprecated by Rails. # Remove prefix here if it is removed completely. - prefix = Rails.configuration.action_controller[:relative_url_root] || '' + prefix = Rails.configuration.action_controller[:relative_url_root] || "" prefix + path end end class SessionsController < ::Devise::SessionsController include ::ActiveAdmin::Devise::Controller + + ActiveSupport.run_load_hooks(:active_admin_controller, self) end class PasswordsController < ::Devise::PasswordsController include ::ActiveAdmin::Devise::Controller + + ActiveSupport.run_load_hooks(:active_admin_controller, self) end class UnlocksController < ::Devise::UnlocksController include ::ActiveAdmin::Devise::Controller + + ActiveSupport.run_load_hooks(:active_admin_controller, self) + end + + class RegistrationsController < ::Devise::RegistrationsController + include ::ActiveAdmin::Devise::Controller + + ActiveSupport.run_load_hooks(:active_admin_controller, self) + end + + class ConfirmationsController < ::Devise::ConfirmationsController + include ::ActiveAdmin::Devise::Controller + + ActiveSupport.run_load_hooks(:active_admin_controller, self) + end + + def self.controllers_for_filters + [SessionsController, PasswordsController, UnlocksController, + RegistrationsController, ConfirmationsController + ] end end diff --git a/lib/active_admin/dsl.rb b/lib/active_admin/dsl.rb index d5747b4dd89..cbe6a9de6c9 100644 --- a/lib/active_admin/dsl.rb +++ b/lib/active_admin/dsl.rb @@ -1,9 +1,9 @@ +# frozen_string_literal: true module ActiveAdmin - # # The Active Admin DSL. This class is where all the registration blocks - # are instance eval'd. This is the central place for the API given to - # users of Active Admin + # are evaluated. This is the central place for the API given to + # users of Active Admin. # class DSL @@ -13,10 +13,10 @@ def initialize(config) # Runs the registration block inside this object def run_registration_block(&block) - instance_eval &block if block_given? + instance_exec(&block) if block end - # The instance of ActiveAdmin::Config that's being registered + # The instance of ActiveAdmin::Resource that's being registered # currently. You can use this within your registration blocks to # modify options: # @@ -30,7 +30,7 @@ def config @config end - # Include a module with this resource. The modules's `included` method + # Include a module with this resource. The modules' `included` method # is called with the instance of the `ActiveAdmin::DSL` passed into it. # # eg: @@ -51,13 +51,13 @@ def config # # @param [Module] mod A module to include # - # @returns [Nil] + # @return [Nil] def include(mod) mod.included(self) end # Returns the controller for this resource. If you pass a - # block, it will be eval'd in the controller + # block, it will be evaluated in the controller. # # Example: # @@ -72,19 +72,20 @@ def include(mod) # end # def controller(&block) - @config.controller.class_eval(&block) if block_given? + @config.controller.class_exec(&block) if block @config.controller end # Add a new action item to the resource # + # @param [Symbol] name # @param [Hash] options valid keys include: # :only: A single or array of controller actions to display # this action item on. # :except: A single or array of controller actions not to # display this action item on. - def action_item(options = {}, &block) - config.add_action_item(options, &block) + def action_item(name, options = {}, &block) + config.add_action_item(name, options, &block) end # Add a new batch action item to the resource @@ -101,7 +102,7 @@ def action_item(options = {}, &block) def batch_action(title, options = {}, &block) # Create symbol & title information if title.is_a? String - sym = title.titleize.gsub(' ', '').underscore.to_sym + sym = title.titleize.tr(" ", "").underscore.to_sym else sym = title title = sym.to_s.titleize @@ -109,7 +110,7 @@ def batch_action(title, options = {}, &block) # Either add/remove the batch action unless options == false - config.add_batch_action( sym, title, options, &block ) + config.add_batch_action(sym, title, options, &block) else config.remove_batch_action sym end @@ -121,7 +122,7 @@ def menu(options = {}) config.menu_item_options = options end - # Set the name of the navigation menu to display. This is mainly used in conjuction with the + # Set the name of the navigation menu to display. This is mainly used in conjunction with the # `#belongs_to` functionality. # # @param [Symbol] menu_name The name of the menu to display as the global navigation @@ -130,7 +131,7 @@ def menu(options = {}) # Pass a block returning the name of a menu you want rendered for the request, being # executed in the context of the controller # - def navigation_menu(menu_name=nil, &block) + def navigation_menu(menu_name = nil, &block) config.navigation_menu_name = menu_name || block end @@ -156,11 +157,5 @@ def sidebar(name, options = {}, &block) config.sidebar_sections << ActiveAdmin::SidebarSection.new(name, options, &block) end - def decorate_with(decorator_class) - # Force storage as a string. This will help us with reloading issues. - # Assuming decorator_class.to_s will return the name of the class allows - # us to handle a string or a class. - config.decorator_class_name = "::#{ decorator_class }" - end end end diff --git a/lib/active_admin/dynamic_setting.rb b/lib/active_admin/dynamic_setting.rb new file mode 100644 index 00000000000..69461379f11 --- /dev/null +++ b/lib/active_admin/dynamic_setting.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true +module ActiveAdmin + + class DynamicSetting + def self.build(setting, type) + (type ? klass(type) : self).new(setting) + end + + def self.klass(type) + klass = "#{type.to_s.camelcase}Setting" + raise ArgumentError, "Unknown type: #{type}" unless ActiveAdmin.const_defined?(klass) + ActiveAdmin.const_get(klass) + end + + def initialize(setting) + @setting = setting + end + + def value(*_args) + @setting + end + end + + # Many configuration options (Ex: site_title, title_image) could either be + # static (String), methods (Symbol) or procs (Proc). This wrapper takes care of + # returning the content when String or using instance_eval when Symbol or Proc. + # + class StringSymbolOrProcSetting < DynamicSetting + def value(context = self) + case @setting + when Symbol, Proc + context.instance_eval(&@setting) + else + @setting + end + end + end + +end diff --git a/lib/active_admin/dynamic_settings_node.rb b/lib/active_admin/dynamic_settings_node.rb new file mode 100644 index 00000000000..9227ea2db9e --- /dev/null +++ b/lib/active_admin/dynamic_settings_node.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true +require_relative "dynamic_setting" +require_relative "settings_node" + +module ActiveAdmin + + class DynamicSettingsNode < SettingsNode + class << self + def register(name, value, type = nil) + class_attribute "#{name}_setting" + add_reader(name) + add_writer(name, type) + send :"#{name}=", value + end + + def add_reader(name) + define_singleton_method(name) do |*args| + send(:"#{name}_setting").value(*args) + end + end + + def add_writer(name, type) + define_singleton_method(:"#{name}=") do |value| + send(:"#{name}_setting=", DynamicSetting.build(value, type)) + end + end + end + end +end diff --git a/lib/active_admin/engine.rb b/lib/active_admin/engine.rb index cee896738ea..0d1930bc2bc 100644 --- a/lib/active_admin/engine.rb +++ b/lib/active_admin/engine.rb @@ -1,9 +1,45 @@ +# frozen_string_literal: true module ActiveAdmin class Engine < ::Rails::Engine - if Rails.version > "3.1" - initializer "ActiveAdmin precompile hook", :group => :all do |app| - app.config.assets.precompile += %w(active_admin.js active_admin.css active_admin/print.css) + isolate_namespace ActiveAdmin + + # Set default values for app_path and load_paths before running initializers + initializer "active_admin.load_app_path", before: :load_config_initializers do |app| + ActiveAdmin::Application.setting :app_path, app.root + ActiveAdmin::Application.setting :load_paths, [File.expand_path("app/admin", app.root)] + end + + initializer "active_admin.precompile", group: :all do |app| + if app.config.respond_to?(:assets) + app.config.assets.precompile += %w(active_admin.js active_admin.css active_admin_manifest.js) + end + end + + initializer "active_admin.importmap", after: "importmap" do |app| + # Skip if importmap-rails is not installed + next unless app.config.respond_to?(:importmap) + + ActiveAdmin.importmap.draw(Engine.root.join("config", "importmap.rb")) + package_path = Engine.root.join("app/javascript") + if app.config.respond_to?(:assets) + app.config.assets.paths << package_path + app.config.assets.paths << Engine.root.join("vendor/javascript") end + + if app.config.importmap.sweep_cache + ActiveAdmin.importmap.cache_sweeper(watches: package_path) + ActiveSupport.on_load(:action_controller_base) do + before_action { ActiveAdmin.importmap.cache_sweeper.execute_if_updated } + end + end + end + + initializer "active_admin.routes" do + require_relative "helpers/routes/url_helpers" + end + + initializer "active_admin.deprecator" do |app| + app.deprecators[:activeadmin] = ActiveAdmin.deprecator if app.respond_to?(:deprecators) end end end diff --git a/lib/active_admin/error.rb b/lib/active_admin/error.rb new file mode 100644 index 00000000000..824a7fe79f6 --- /dev/null +++ b/lib/active_admin/error.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true +module ActiveAdmin + # Exception class to raise when there is an authorized access + # exception thrown. The exception has a few goodies that may + # be useful for capturing / recognizing security issues. + class AccessDenied < StandardError + attr_reader :user, :action, :subject + + def initialize(user, action, subject = nil) + @user = user + @action = action + @subject = subject + + super() + end + + def message + I18n.t("active_admin.access_denied.message") + end + end + + class Error < RuntimeError + end + + class ErrorLoading < Error + # Locates the most recent file and line from the caught exception's backtrace. + def find_cause(folder, backtrace) + backtrace.grep(/\/(#{folder}\/.*\.rb):(\d+)/) { [$1, $2] }.first + end + end + + class DatabaseHitDuringLoad < ErrorLoading + def initialize(exception) + file, line = find_cause(:app, exception.backtrace) + + super "Your file, #{file} (line #{line}), caused a database error while Active Admin was loading. This " + + "is most common when your database is missing or doesn't have the latest migrations applied. To " + + "prevent this error, move the code to a place where it will only be run when a page is rendered. " + + "One solution can be, to wrap the query in a Proc. " + + "Original error message: #{exception.message}" + end + + def self.capture + yield + rescue *database_error_classes => exception + raise new exception + end + + def self.database_error_classes + @classes ||= [] + end + end + + class DependencyError < ErrorLoading + end + + class NoMenuError < KeyError + end + + class GeneratorError < Error + end + +end diff --git a/lib/active_admin/event.rb b/lib/active_admin/event.rb deleted file mode 100644 index 68caf336398..00000000000 --- a/lib/active_admin/event.rb +++ /dev/null @@ -1,33 +0,0 @@ -module ActiveAdmin - - class EventDispatcher - def initialize - @events = {} - end - - def clear_all_subscribers! - @events = {} - end - - def subscribe(*event_names, &block) - event_names.each do |event| - @events[event] ||= [] - @events[event] << block - end - end - - def subscribers(event) - @events[event] || [] - end - - def dispatch(event, *args) - subscribers(event).each do |subscriber| - subscriber.call(*args) - end - end - end - - # ActiveAdmin::Event is set to a dispatcher - Event = EventDispatcher.new - -end diff --git a/lib/active_admin/filters.rb b/lib/active_admin/filters.rb index 13485d79232..c69094f5d99 100644 --- a/lib/active_admin/filters.rb +++ b/lib/active_admin/filters.rb @@ -1,9 +1,10 @@ -require 'active_admin/filters/dsl' -require 'active_admin/filters/resource_extension' -require 'active_admin/filters/formtastic_addons' -require 'active_admin/filters/forms' +# frozen_string_literal: true +require_relative "filters/dsl" +require_relative "filters/resource_extension" +require_relative "filters/formtastic_addons" +require_relative "filters/forms" +require_relative "helpers/optional_display" # Add our Extensions ActiveAdmin::ResourceDSL.send :include, ActiveAdmin::Filters::DSL -ActiveAdmin::Resource.send :include, ActiveAdmin::Filters::ResourceExtension -ActiveAdmin::ViewHelpers.send :include, ActiveAdmin::Filters::ViewHelper +ActiveAdmin::Resource.send :include, ActiveAdmin::Filters::ResourceExtension diff --git a/lib/active_admin/filters/active.rb b/lib/active_admin/filters/active.rb new file mode 100644 index 00000000000..a68dd718a1d --- /dev/null +++ b/lib/active_admin/filters/active.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true +require_relative "active_filter" + +module ActiveAdmin + module Filters + class Active + attr_reader :filters, :resource, :scopes + + # Instantiate a `Active` object containing collection of current active filters + + # @param resource [ActiveAdmin::Resource] current resource + # @param search [Ransack::Search] search object + # + # @see ActiveAdmin::ResourceController::DataAccess#apply_filtering + def initialize(resource, search) + @resource = resource + # Ransack::Search uses method_missing for #conditions, so gate on the class. + @filters = search.is_a?(::Ransack::Search) ? build_filters(search.conditions) : [] + @scopes = search.instance_variable_get(:@scope_args) + end + + def all_blank? + filters.blank? && scopes.blank? + end + + private + + def build_filters(conditions) + conditions.map { |condition| ActiveFilter.new(resource, condition.dup) } + end + end + end +end diff --git a/lib/active_admin/filters/active_filter.rb b/lib/active_admin/filters/active_filter.rb new file mode 100644 index 00000000000..a0e971f8f53 --- /dev/null +++ b/lib/active_admin/filters/active_filter.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true +module ActiveAdmin + module Filters + + class ActiveFilter + attr_reader :resource, :condition, :related_class + + # Instantiate a `ActiveFilter` + # + # @param resource [ActiveAdmin::Resource] current resource + # @param condition [Ransack::Nodes::Condition] applied ransack condition + def initialize(resource, condition) + @resource = resource + @condition = condition + @related_class = find_class if find_class? + end + + def values + condition_values = condition.values.map(&:value) + if related_class + related_class.where(related_primary_key => condition_values) + else + condition_values + end + end + + def label + translated_predicate = predicate_name.downcase + if filter_label && filter_label.is_a?(Proc) + "#{filter_label.call} #{translated_predicate}" + elsif filter_label + "#{filter_label} #{translated_predicate}" + elsif related_class + "#{related_class_name} #{translated_predicate}" + else + "#{attribute_name} #{translated_predicate}" + end.strip + end + + def predicate_name + Ransack::Translate.predicate(condition.predicate.name) + end + + def html_options + { "data-filter": condition.key } + end + + private + + def resource_class + resource.resource_class + end + + def attribute_name + resource_class.human_attribute_name(name) + end + + def related_class_name + return unless related_class + + related_class.model_name.human + end + + def filter_label + return unless filter + + filter[:label] || I18n.t(name, scope: ["formtastic", "labels"], default: nil) + end + + #@return Ransack::Nodes::Attribute + def condition_attribute + condition.attributes[0] + end + + def name + condition_attribute.attr_name + end + + def find_class? + ["eq", "in"].include? condition.predicate.arel_predicate + end + + # detect related class for Ransack::Nodes::Attribute + def find_class + if condition_attribute.klass != resource_class && condition_attribute.klass.primary_key == name.to_s + condition_attribute.klass + elsif predicate_association + predicate_association.klass + end + end + + def filter + resource.filters[name.to_sym] || resource.filters[condition.key.to_sym] + end + + def related_primary_key + if predicate_association + predicate_association.association_primary_key + elsif related_class + related_class.primary_key + end + end + + def predicate_association + @predicate_association = find_predicate_association unless defined?(@predicate_association) + @predicate_association + end + + def find_predicate_association + condition_attribute.klass.reflect_on_all_associations. + reject { |r| r.options[:polymorphic] }. #skip polymorphic + detect { |r| r.foreign_key.to_s == name.to_s } + end + end + end +end diff --git a/lib/active_admin/filters/dsl.rb b/lib/active_admin/filters/dsl.rb index 207c2c872e9..56a40473e9f 100644 --- a/lib/active_admin/filters/dsl.rb +++ b/lib/active_admin/filters/dsl.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin module Filters module DSL @@ -8,8 +9,8 @@ def filter(attribute, options = {}) end # For docs, please see ActiveAdmin::Filters::ResourceExtension#remove_filter - def remove_filter(attribute) - config.remove_filter(attribute) + def remove_filter(*attributes) + config.remove_filter(*attributes) end # For docs, please see ActiveAdmin::Filters::ResourceExtension#preserve_default_filters! diff --git a/lib/active_admin/filters/forms.rb b/lib/active_admin/filters/forms.rb index cd7da1575a6..7c71fdf74a4 100644 --- a/lib/active_admin/filters/forms.rb +++ b/lib/active_admin/filters/forms.rb @@ -1,35 +1,34 @@ +# frozen_string_literal: true module ActiveAdmin module Filters - # This form builder defines methods to build filter forms such # as the one found in the sidebar of the index page of a standard resource. class FormBuilder < ::ActiveAdmin::FormBuilder include ::ActiveAdmin::Filters::FormtasticAddons - - def initialize(*args) - @use_form_buffer = true # force ActiveAdmin::FormBuilder to use the form buffer - super - end + self.input_namespaces = [::Object, ::ActiveAdmin::Inputs::Filters, ::ActiveAdmin::Inputs, ::Formtastic::Inputs] def filter(method, options = {}) if method.present? && options[:as] ||= default_input_type(method) - input(method, options) + template.concat input(method, options) end end protected - # Returns the default filter type for a given attribute + # Returns the default filter type for a given attribute. If you want + # to use a custom search method, you have to specify the type yourself. def default_input_type(method, options = {}) - if method =~ /_(contains|starts_with|ends_with)\z/ + if /_(eq|cont|start|end)\z/.match?(method) :string + elsif klass._ransackers.key?(method.to_s) + klass._ransackers[method.to_s].type elsif reflection_for(method) || polymorphic_foreign_type?(method) :select elsif column = column_for(method) case column.type when :date, :datetime :date_range - when :string, :text + when :string, :text, :citext :string when :integer, :float, :decimal :numeric @@ -38,52 +37,6 @@ def default_input_type(method, options = {}) end end end - - def custom_input_class_name(as) - "Filter#{as.to_s.camelize}Input" - end - - def active_admin_input_class_name(as) - "ActiveAdmin::Inputs::Filter#{as.to_s.camelize}Input" - end - - end - - - # This module is included into the view - module ViewHelper - - # Helper method to render a filter form - def active_admin_filters_form_for(search, filters, options = {}) - defaults = { :builder => ActiveAdmin::Filters::FormBuilder, - :url => collection_path, - :html => {:class => 'filter_form'} } - required = { :html => {:method => :get}, - :as => :q } - options = defaults.deep_merge(options).deep_merge(required) - - form_for search, options do |f| - filters.group_by{ |o| o[:attribute] }.each do |attribute, array| - opts = array.last # grab last-defined `filter` call from DSL - should = opts.delete(:if) || proc{ true } - shouldnt = opts.delete(:unless) || proc{ false } - - if call_method_or_proc_on(self, should) && !call_method_or_proc_on(self, shouldnt) - f.filter attribute, opts - end - end - - buttons = content_tag :div, :class => "buttons" do - f.submit(I18n.t('active_admin.filters.buttons.filter')) + - link_to(I18n.t('active_admin.filters.buttons.clear'), '#', :class => 'clear_filters_btn') + - hidden_field_tags_for(params, :except => [:q, :page]) - end - - f.form_buffers.last + buttons - end - end - end - end end diff --git a/lib/active_admin/filters/formtastic_addons.rb b/lib/active_admin/filters/formtastic_addons.rb index ac5a1f8ba2a..7df3f0571e1 100644 --- a/lib/active_admin/filters/formtastic_addons.rb +++ b/lib/active_admin/filters/formtastic_addons.rb @@ -1,43 +1,79 @@ +# frozen_string_literal: true module ActiveAdmin module Filters module FormtasticAddons # - # The below are Formtastic overrides to use `base` instead of `class` for MetaSearch. + # The below are Formtastic method overrides that jump inside of the Ransack + # search object to get at the object being searched upon. # - # Returns the default label for a given attribute. Uses ActiveModel I18n if available. def humanized_method_name - if object.base.respond_to?(:human_attribute_name) - object.base.human_attribute_name(method) + if klass.respond_to?(:human_attribute_name) + klass.human_attribute_name(method) else - method.to_s.send(builder.label_str_method) + method.to_s.public_send(builder.label_str_method) end end - # Returns the association reflection for the method if it exists def reflection_for(method) - @object.base.reflect_on_association(method) if @object.base.respond_to?(:reflect_on_association) + klass.reflect_on_association(method) if klass.respond_to? :reflect_on_association end - # Returns the column for an attribute on the object being searched if it exists. def column_for(method) - @object.base.columns_hash[method.to_s] if @object.base.respond_to?(:columns_hash) + klass.columns_hash[method.to_s] if klass.respond_to? :columns_hash + end + + def column + column_for method end # # The below are custom methods that Formtastic does not provide. # - def foreign_key?(method) - @object.base.reflections.select{ |_,r| r.macro == :belongs_to }.values - .map(&:foreign_key).include? method.to_s + # The resource class, unwrapped from Ransack or `nil` for non-Ransack search objects. + def klass + @object.object.klass if @object.respond_to?(:object) && @object.object.respond_to?(:klass) end def polymorphic_foreign_type?(method) - type = Rails::VERSION::MAJOR == 3 && Rails::VERSION::MINOR == 0 ? proc{ |r| r.options[:foreign_type] } : :foreign_type - @object.base.reflections.values.select{ |r| r.macro == :belongs_to && r.options[:polymorphic] } - .map(&type).include? method.to_s + return false if klass.nil? + + klass.reflect_on_all_associations.select { |r| r.macro == :belongs_to && r.options[:polymorphic] } + .map(&:foreign_type).include? method.to_s + end + + # + # These help figure out whether the given method or association will be recognized by Ransack. + # + + def searchable_has_many_through? + return false if klass.nil? + + if klass.ransackable_associations.include?(method.to_s) && reflection && reflection.options[:through] + reflection.through_reflection.klass.ransackable_attributes.include? reflection.foreign_key + else + false + end + end + + def seems_searchable? + column_for(method).nil? && (has_predicate? || scope?) + end + + # If the given method has a predicate (like _eq or _lteq), it's pretty + # likely we're dealing with a valid search method. + def has_predicate? + !!Ransack::Predicate.detect_from_string(method.to_s) + end + + # Ransack supports exposing selected scopes on your model for advanced searches. + def scope? + return false if klass.nil? + + context = Ransack::Context.for klass + context.respond_to?(:ransackable_scope?) && context.ransackable_scope?(method.to_s, klass) end end diff --git a/lib/active_admin/filters/resource_extension.rb b/lib/active_admin/filters/resource_extension.rb index ba400054b51..1e73d0af880 100644 --- a/lib/active_admin/filters/resource_extension.rb +++ b/lib/active_admin/filters/resource_extension.rb @@ -1,20 +1,27 @@ +# frozen_string_literal: true +require_relative "active" + module ActiveAdmin module Filters + class Disabled < RuntimeError + def initialize(action) + super "Cannot #{action} a filter when filters are disabled. Enable filters with 'config.filters = true'" + end + end module ResourceExtension - def initialize(*) super add_filters_sidebar_section + add_active_search_sidebar_section end # Returns the filters for this resource. If filters are not enabled, - # it will always return an empty array. + # it will always return an empty hash. # - # @return [Array] Filters that apply for this resource + # @return [Hash] Filters that apply for this resource def filters - return [] unless filters_enabled? - filter_lookup + filters_enabled? ? filter_lookup : {} end # Setter to enable / disable filters on this resource. @@ -24,11 +31,23 @@ def filters=(bool) @filters_enabled = bool end + # Setter to enable/disable showing current filters on this resource. + # + # Set to `nil` to inherit the setting from the namespace + def current_filters=(bool) + @current_filters_enabled = bool + end + # @return [Boolean] If filters are enabled for this resource def filters_enabled? @filters_enabled.nil? ? namespace.filters : @filters_enabled end + # @return [Boolean] If show current filters are enabled for this resource + def current_filters_enabled? + @current_filters_enabled.nil? ? namespace.current_filters : @current_filters_enabled + end + def preserve_default_filters! @preserve_default_filters = true end @@ -40,14 +59,11 @@ def preserve_default_filters? # Remove a filter for this resource. If filters are not enabled, this method # will raise a RuntimeError # - # @param [Symbol] attribute The attribute to not filter on - def remove_filter(attribute) - unless filters_enabled? - raise RuntimeError, "Can't remove a filter when filters are disabled. Enable filters with 'config.filters = true'" - end + # @param [Symbol] attributes The attributes to not filter on + def remove_filter(*attributes) + raise Disabled, "remove" unless filters_enabled? - @filters_to_remove ||= [] - @filters_to_remove << attribute + attributes.each { |attribute| (@filters_to_remove ||= []) << attribute.to_sym } end # Add a filter for this resource. If filters are not enabled, this method @@ -55,14 +71,11 @@ def remove_filter(attribute) # # @param [Symbol] attribute The attribute to filter on # @param [Hash] options The set of options that are passed through to - # metasearch for the field definition. + # ransack for the field definition. def add_filter(attribute, options = {}) - unless filters_enabled? - raise RuntimeError, "Can't add a filter when filters are disabled. Enable filters with 'config.filters = true'" - end + raise Disabled, "add" unless filters_enabled? - @filters ||= [] - @filters << options.merge({ :attribute => attribute }) + (@filters ||= {})[attribute.to_sym] = options end # Reset the filters to use defaults @@ -76,47 +89,75 @@ def reset_filters! # Collapses the waveform, if you will, of which filters should be displayed. # Removes filters and adds in default filters as desired. def filter_lookup - filters = @filters.try(:dup) || [] - filters.push *default_filters if filters.empty? || preserve_default_filters? + filters = @filters.try(:dup) || {} - if @filters_to_remove - @filters_to_remove.each do |attr| - filters.delete_if{ |f| f.fetch(:attribute) == attr } + if filters.empty? || preserve_default_filters? + default_filters.each do |f| + filters[f] ||= {} end end + if @filters_to_remove + @filters_to_remove.each { |filter| filters.delete(filter) } + end + filters end # @return [Array] The array of default filters for this resource def default_filters - default_association_filters + default_content_filters + result = [] + result.concat default_association_filters if namespace.include_default_association_filters + result.concat content_columns + result.concat custom_ransack_filters + result + end + + def custom_ransack_filters + if resource_class.respond_to?(:_ransackers) + resource_class._ransackers.keys.map(&:to_sym) + else + [] + end end # Returns a default set of filters for the associations def default_association_filters - if resource_class.respond_to?(:reflections) - block = if Rails::VERSION::MAJOR == 3 && Rails::VERSION::MINOR == 0 - proc{ |_,r| [r.options[:foreign_type], r.primary_key_name] } - else - proc{ |_,r| [r.foreign_type, r.foreign_key] } + if resource_class.respond_to?(:reflect_on_all_associations) + poly, not_poly = resource_class.reflect_on_all_associations.partition { |r| r.macro == :belongs_to && r.options[:polymorphic] } + + # remove deeply nested associations + not_poly.reject! { |r| r.chain.length > 2 } + + filters = poly.map(&:foreign_type) + not_poly.map(&:name) + + # Check high-arity associations for filterable columns + max = namespace.maximum_association_filter_arity + if max != :unlimited + high_arity, low_arity = not_poly.partition do |r| + r.klass.reorder(nil).limit(max + 1).count > max + end + + # Remove high-arity associations with no searchable column + high_arity = high_arity.select { |r| searchable_column_for(r) } + + high_arity = high_arity.map { |r| r.name.to_s + "_" + searchable_column_for(r) + namespace.filter_method_for_large_association } + + filters = poly.map(&:foreign_type) + low_arity.map(&:name) + high_arity end - poly, not_poly = resource_class.reflections.partition{ |_,r| r.macro == :belongs_to && r.options[:polymorphic] } - filters = poly.map(&block).flatten + not_poly.map(&:first) - filters.collect{ |name| { :attribute => name.to_sym } } + filters.map(&:to_sym) else [] end end - # Returns a default set of filters for the content columns - def default_content_filters - if resource_class.respond_to?(:content_columns) - resource_class.content_columns.collect{|c| { :attribute => c.name.to_sym } } - else - [] - end + def search_columns + @search_columns ||= namespace.filter_columns_for_large_association.map(&:to_s) + end + + def searchable_column_for(relation) + relation.klass.column_names.find { |name| search_columns.include?(name) } end def add_filters_sidebar_section @@ -124,12 +165,24 @@ def add_filters_sidebar_section end def filters_sidebar_section - ActiveAdmin::SidebarSection.new(:filters, :only => :index, :if => proc{ active_admin_config.filters.any? } ) do - active_admin_filters_form_for assigns[:search], active_admin_config.filters + name = :filters + ActiveAdmin::SidebarSection.new name, only: :index, if: -> { active_admin_config.filters.any? } do + h3 I18n.t("active_admin.sidebars.#{name}", default: name.to_s.titlecase), class: "filters-form-title" + active_admin_filters_form_for assigns[:search], **active_admin_config.filters end end - end + def add_active_search_sidebar_section + self.sidebar_sections << active_search_sidebar_section + end + def active_search_sidebar_section + name = :active_search + ActiveAdmin::SidebarSection.new name, only: :index, if: -> { active_admin_config.current_filters_enabled? && (params[:q] || params[:scope]) } do + filters = ActiveAdmin::Filters::Active.new(active_admin_config, assigns[:search]) + render "active_filters", active_filters: filters + end + end + end end end diff --git a/lib/active_admin/form_builder.rb b/lib/active_admin/form_builder.rb index 07519a75d5e..5b0e0525fea 100644 --- a/lib/active_admin/form_builder.rb +++ b/lib/active_admin/form_builder.rb @@ -1,202 +1,183 @@ +# frozen_string_literal: true +# Provides an intuitive way to build has_many associated records in the same form. +module Formtastic + module Inputs + module Base + def input_wrapping(&block) + html = super + template.concat(html) if template.output_buffer && template.assigns[:has_many_block] + html + end + end + end +end + module ActiveAdmin class FormBuilder < ::Formtastic::FormBuilder - attr_reader :form_buffers + self.input_namespaces = [::Object, ::ActiveAdmin::Inputs, ::Formtastic::Inputs] - def initialize(*args) - @form_buffers = ["".html_safe] - super + def cancel_link(url = { action: "index" }, html_options = {}, li_attrs = {}) + li_attrs[:class] ||= "action cancel" + html_options[:class] ||= "cancel-link" + li_content = template.link_to I18n.t("active_admin.cancel"), url, html_options + template.content_tag(:li, li_content, li_attrs) end - def inputs(*args, &block) - @use_form_buffer = block_given? - form_buffers.last << with_new_form_buffer{ super } - end - - # If this `input` call is inside a `inputs` block, add the content - # to the form buffer. Else, return it directly. - def input(method, *args) - content = with_new_form_buffer{ super } - @use_form_buffer ? form_buffers.last << content : content - end + attr_accessor :already_in_an_inputs_block - def cancel_link(url = {:action => "index"}, html_options = {}, li_attrs = {}) - li_attrs[:class] ||= "cancel" - li_content = template.link_to I18n.t('active_admin.cancel'), url, html_options - form_buffers.last << template.content_tag(:li, li_content, li_attrs) + def has_many(assoc, options = {}, &block) + HasManyBuilder.new(self, assoc, options).render(&block) end + end - def actions(*args, &block) - form_buffers.last << with_new_form_buffer do - block_given? ? super : super{ commit_action_with_cancel_link } + # Decorates a FormBuilder with the additional attributes and methods + # to build a has_many block. Nested has_many blocks are handled by + # nested decorators. + class HasManyBuilder < SimpleDelegator + attr_reader :assoc + attr_reader :options + attr_reader :heading, :sortable_column, :sortable_start + attr_reader :new_record, :destroy_option, :remove_record + + def initialize(has_many_form, assoc, options) + super has_many_form + @assoc = assoc + @options = extract_custom_settings!(options.dup) + @options.reverse_merge!(for: assoc) + @options[:class] = [options[:class], "inputs has-many-fields"].compact.join(" ") + + if sortable_column + @options[:for] = [assoc, sorted_children(sortable_column)] end end - def action(*args) - form_buffers.last << with_new_form_buffer{ super } + def render(&block) + html = "".html_safe + html << template.content_tag(:h3, class: "has-many-fields-title") { heading } if heading.present? + html << template.capture { content_has_many(&block) } + html = wrap_div_or_li(html) + template.concat(html) if template.output_buffer + html end - def commit_action_with_cancel_link - action(:submit) - cancel_link - end - - def has_many(association, options = {}, &block) - options = { :for => association, :new_record => true }.merge(options) - options[:class] ||= "" - options[:class] << "inputs has_many_fields" + protected - # Add Delete Links - form_block = proc do |has_many_form| - # @see https://github.com/justinfrench/formtastic/blob/2.2.1/lib/formtastic/helpers/inputs_helper.rb#L373 - contents = if block.arity == 1 # for backwards compatibility with REE & Ruby 1.8.x - block.call(has_many_form) - else - index = parent_child_index(options[:parent]) if options[:parent] - block.call(has_many_form, index) - end + # remove options that should not render as attributes + def extract_custom_settings!(options) + @heading = options.key?(:heading) ? options.delete(:heading) : default_heading + @sortable_column = options.delete(:sortable) + @sortable_start = options.delete(:sortable_start) || 0 + @new_record = options.key?(:new_record) ? options.delete(:new_record) : true + @destroy_option = options.delete(:allow_destroy) + @remove_record = options.delete(:remove_record) + options + end - if has_many_form.object.new_record? - contents += template.content_tag(:li, :class => 'has_many_delete') do - template.link_to I18n.t('active_admin.has_many_delete'), "#", :onclick => "$(this).closest('.has_many_fields').remove(); return false;", :class => "button" - end - elsif options[:allow_destroy] - has_many_form.input :_destroy, :as => :boolean, :wrapper_html => {:class => "has_many_remove"}, - :label => I18n.t('active_admin.has_many_remove') + def default_heading + assoc_klass.model_name.human(count: 2.1) + end - end + def assoc_klass + @assoc_klass ||= __getobj__.object.class.reflect_on_association(assoc).klass + end - contents + def content_has_many(&block) + form_block = proc do |form_builder| + render_has_many_form(form_builder, options[:parent], &block) end - form_buffers.last << with_new_form_buffer do - template.content_tag :div, :class => "has_many #{association}" do - # Allow customization of the nested form heading - unless options.key?(:heading) && !options[:heading] - form_heading = options[:heading] || - object.class.reflect_on_association(association).klass.model_name.human(:count => 1.1) - form_buffers.last << template.content_tag(:h3, form_heading) - end + template.assigns[:has_many_block] = true + contents = without_wrapper { inputs(options, &form_block) } + contents ||= "".html_safe - inputs options, &form_block - - js = options[:new_record] ? js_for_has_many(association, form_block, template) : "" - form_buffers.last << js.html_safe - end - end + js = new_record ? js_for_has_many(&form_block) : "" + contents << js end - def semantic_errors(*args) - form_buffers.last << with_new_form_buffer{ super } + # Renders the Formtastic inputs then appends ActiveAdmin delete and sort actions. + def render_has_many_form(form_builder, parent, &block) + index = parent && form_builder.send(:parent_child_index, parent) + template.concat template.capture { yield(form_builder, index) } + template.concat has_many_actions(form_builder, "".html_safe) end - # These methods are deprecated and removed from Formtastic, however are - # supported here to help with transition. - module DeprecatedMethods - - # Formtastic has depreciated #commit_button in favor of #action(:submit) - def commit_button(*args) - ::ActiveSupport::Deprecation.warn("f.commit_button is deprecated in favour of f.action(:submit)") - - options = args.extract_options! - if String === args.first - options[:label] = args.first unless options.has_key?(:label) + def has_many_actions(form_builder, contents) + if form_builder.object.new_record? + contents << template.content_tag(:li, class: "input") do + remove_text = remove_record.is_a?(String) ? remove_record : I18n.t("active_admin.has_many_remove") + template.link_to remove_text, "#", class: "has-many-remove" end - - action(:submit, options) + elsif allow_destroy?(form_builder.object) + form_builder.input( + :_destroy, as: :boolean, + wrapper_html: { class: "has-many-delete" }, + label: I18n.t("active_admin.has_many_delete")) end - def commit_button_with_cancel_link - # Formtastic has depreciated #buttons in favor of #actions - ::ActiveSupport::Deprecation.warn("f.commit_button_with_cancel_link is deprecated in favour of f.commit_action_with_cancel_link") + if sortable_column + form_builder.input sortable_column, as: :hidden - commit_action_with_cancel_link + # contents << template.content_tag(:li, class: "handle") do + # I18n.t("active_admin.move") + # end end - # The buttons method always needs to be wrapped in a new buffer - def buttons(*args, &block) - # Formtastic has depreciated #buttons in favor of #actions - ::ActiveSupport::Deprecation.warn("f.buttons is deprecated in favour of f.actions") - - actions args, &block - end - - end - include DeprecatedMethods - - protected - - def active_admin_input_class_name(as) - "ActiveAdmin::Inputs::#{as.to_s.camelize}Input" + contents end - # Overrides Formtastic's version to include ActiveAdmin::Inputs::* - def input_class_with_const_defined(as) - input_class_name = custom_input_class_name(as) - - if ::Object.const_defined?(input_class_name) - input_class_name.constantize - elsif ActiveAdmin::Inputs.const_defined?(input_class_name) - active_admin_input_class_name(as).constantize - elsif Formtastic::Inputs.const_defined?(input_class_name) - standard_input_class_name(as).constantize - else - raise Formtastic::UnknownInputError, "Unable to find input class #{input_class_name}" - end + def allow_destroy?(form_object) + !! case destroy_option + when Symbol, String + form_object.public_send destroy_option + when Proc + destroy_option.call form_object + else + destroy_option + end end - # use auto-loading in development environment - def input_class_by_trying(as) - begin - custom_input_class_name(as).constantize - rescue NameError - begin - active_admin_input_class_name(as).constantize - rescue NameError - standard_input_class_name(as).constantize - end + def sorted_children(column) + __getobj__.object.public_send(assoc).sort_by do |o| + attribute = o.public_send column + [attribute.nil? ? Float::INFINITY : attribute, o.id || Float::INFINITY] end - rescue NameError - raise Formtastic::UnknownInputError end - # This method calls the block it's passed (in our case, the `f.inputs` block) - # and wraps the resulting HTML in a fieldset. If your block doesn't have a - # valid return value but it was otherwise built correctly, we instead use - # the most recent part of the Active Admin form buffer. - def field_set_and_list_wrapping(*args, &block) - block_given? ? super{ - (val = yield).is_a?(String) ? val : form_buffers.last - } : super - end + def without_wrapper + is_being_wrapped = already_in_an_inputs_block + self.already_in_an_inputs_block = false - private + html = yield - def with_new_form_buffer - form_buffers << ''.html_safe - return_value = (yield || '').html_safe - form_buffers.pop - return_value + self.already_in_an_inputs_block = is_being_wrapped + html end # Capture the ADD JS - def js_for_has_many(association, form_block, template) - assoc_reflection = object.class.reflect_on_association(association) - assoc_name = assoc_reflection.klass.model_name - placeholder = "NEW_#{assoc_name.to_s.upcase.split(' ').join('_')}_RECORD" - opts = { - :for => [association, assoc_reflection.klass.new], - :class => "inputs has_many_fields", - :for_options => { :child_index => placeholder } + def js_for_has_many(&form_block) + assoc_name = assoc_klass.model_name + placeholder = "NEW_#{assoc_name.to_s.underscore.upcase.tr('/', '_')}_RECORD" + opts = options.merge( + for: [assoc, assoc_klass.new], + for_options: { child_index: placeholder } + ) + html = template.capture { __getobj__.send(:inputs_for_nested_attributes, opts, &form_block) } + text = new_record.is_a?(String) ? new_record : I18n.t("active_admin.has_many_new", model: assoc_name.human) + + template.link_to text, "#", class: "has-many-add", data: { + html: CGI.escapeHTML(html).html_safe, placeholder: placeholder } - js = with_new_form_buffer{ inputs_for_nested_attributes opts, &form_block } - js = template.escape_javascript js - - onclick = "$(this).before('#{js}'.replace(/#{placeholder}/g, new Date().getTime())); return false;" - text = I18n.t 'active_admin.has_many_new', :model => assoc_name.human - - template.link_to(text, "#", :onclick => onclick, :class => "button").html_safe end + def wrap_div_or_li(html) + template.content_tag( + already_in_an_inputs_block ? :li : :div, + html, + class: "has-many-container", + "data-has-many-association" => assoc, + "data-sortable" => sortable_column, + "data-sortable-start" => sortable_start) + end end end diff --git a/lib/active_admin/helpers/collection.rb b/lib/active_admin/helpers/collection.rb deleted file mode 100644 index c3f2a77377b..00000000000 --- a/lib/active_admin/helpers/collection.rb +++ /dev/null @@ -1,23 +0,0 @@ -module ActiveAdmin - module Helpers - module Collection - # Works around this issue: https://github.com/rails/rails/issues/7121 - # - # GROUP BY + COUNT drops SELECT statement. This leads to SQL error when - # the ORDER statement mentions a column defined in the SELECT statement. - # - # We remove the ORDER statement to work around this issue. - def collection_size(collection=collection) - size = collection.reorder("").count - # when GROUP BY is used, AR returns Hash instead of Fixnum for .size - size = size.size if size.kind_of?(Hash) - - size - end - - def collection_is_empty?(collection=collection) - collection_size(collection) == 0 - end - end - end -end diff --git a/lib/active_admin/helpers/optional_display.rb b/lib/active_admin/helpers/optional_display.rb index 01d04ebd0c9..92f2446b49f 100644 --- a/lib/active_admin/helpers/optional_display.rb +++ b/lib/active_admin/helpers/optional_display.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin # Shareable module to give a #display_on?(action) method @@ -5,24 +6,24 @@ module ActiveAdmin # # The options hash accepts: # - # :only => :index - # :only => [:index, :show] - # :except => :index - # :except => [:index, :show] + # only: :index + # only: [:index, :show] + # except: :index + # except: [:index, :show] # # call #normalize_display_options! after @options has been set # to ensure that the display options are setup correctly module OptionalDisplay def display_on?(action, render_context = self) - return false if @options[:only] && !@options[:only].include?(action.to_sym) + return false if @options[:only] && !@options[:only].include?(action.to_sym) return false if @options[:except] && @options[:except].include?(action.to_sym) case condition = @options[:if] when Symbol, String - render_context.send condition + render_context.public_send condition when Proc - render_context.instance_exec &condition + render_context.instance_exec(&condition) else true end @@ -31,7 +32,7 @@ def display_on?(action, render_context = self) private def normalize_display_options! - @options[:only] = Array(@options[:only]) if @options[:only] + @options[:only] = Array(@options[:only]) if @options[:only] @options[:except] = Array(@options[:except]) if @options[:except] end end diff --git a/lib/active_admin/helpers/routes/url_helpers.rb b/lib/active_admin/helpers/routes/url_helpers.rb new file mode 100644 index 00000000000..bacbe721d21 --- /dev/null +++ b/lib/active_admin/helpers/routes/url_helpers.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +module ActiveAdmin + module Helpers + module Routes + module UrlHelpers + include Rails.application.routes.url_helpers + end + + extend UrlHelpers + + def self.default_url_options + Rails.application.routes.default_url_options || {} + end + end + end +end diff --git a/lib/active_admin/helpers/scope_chain.rb b/lib/active_admin/helpers/scope_chain.rb index d27d33d347d..d585fd804d3 100644 --- a/lib/active_admin/helpers/scope_chain.rb +++ b/lib/active_admin/helpers/scope_chain.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true module ActiveAdmin module ScopeChain + private # Scope an ActiveRecord::Relation chain # # Example: @@ -12,7 +14,7 @@ module ScopeChain # def scope_chain(scope, chain) if scope.scope_method - chain.send(scope.scope_method) + chain.public_send scope.scope_method elsif scope.scope_block instance_exec chain, &scope.scope_block else diff --git a/lib/active_admin/helpers/settings.rb b/lib/active_admin/helpers/settings.rb deleted file mode 100644 index f2406791deb..00000000000 --- a/lib/active_admin/helpers/settings.rb +++ /dev/null @@ -1,108 +0,0 @@ -module ActiveAdmin - - # Adds a class method to a class to create settings with default values. - # - # Example: - # - # class Configuration - # include ActiveAdmin::Settings - # - # setting :site_title, "Default Site Title" - # end - # - # conf = Configuration.new - # conf.site_title #=> "Default Site Title" - # conf.site_title = "Override Default" - # conf.site_title #=> "Override Default" - # - module Settings - - def self.included(base) - base.extend ClassMethods - end - - def read_default_setting(name) - default_settings[name] - end - - private - - def default_settings - self.class.default_settings - end - - module ClassMethods - - def setting(name, default) - default_settings[name] = default - attr_writer name - - # Creates a reader that will grab the default if no value has been set. - class_eval <<-EOC, __FILE__, __LINE__ + 1 - def #{name} - if instance_variable_defined? :@#{name} - @#{name} - else - read_default_setting(:#{name}) - end - end - EOC - end - - def deprecated_setting(name, default, message = nil) - setting(name, default) - - message ||= "The #{name} setting is deprecated and will be removed." - ActiveAdmin::Deprecation.deprecate self, name, message - ActiveAdmin::Deprecation.deprecate self, :"#{name}=", message - end - - def default_settings - @default_settings ||= {} - end - - end - - - # Allows you to define child classes that should receive the same - # settings, as well as the same default values. - # - # Example from the codebase: - # - # class Application - # include Settings - # include Settings::Inheritance - # - # settings_inherited_by :Namespace - # - # inheritable_setting :root_to, 'dashboard#index' - # end - # - module Inheritance - - def self.included(base) - base.extend ClassMethods - end - - module ClassMethods - - def settings_inherited_by(heir) - (@setting_heirs ||= []) << heir - heir.send :include, ActiveAdmin::Settings - end - - def inheritable_setting(name, default) - setting name, default - @setting_heirs.each{ |c| c.setting name, default } - end - - def deprecated_inheritable_setting(name, default) - deprecated_setting name, default - @setting_heirs.each{ |c| c.deprecated_setting name, default } - end - - end - end - - end -end diff --git a/lib/active_admin/iconic.rb b/lib/active_admin/iconic.rb deleted file mode 100644 index 4c4bf557681..00000000000 --- a/lib/active_admin/iconic.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'active_admin/iconic/icons' - -module ActiveAdmin - module Iconic - - # Default color to use for icons - @@default_color = "#5E6469" - mattr_accessor :default_color - - # Default width to use for icons - @@default_width = 15 - mattr_accessor :default_width - - # Default height to use for icons - @@default_height = 15 - mattr_accessor :default_height - - # Render an icon: - # Iconic.icon :loop - def self.icon(name, options = {}) - options = { - :color => default_color, - :width => default_width, - :height => default_height, - :id => "" - }.merge(options) - - - options[:style] = "fill:#{options[:color]};" - options[:fill] = options.delete(:color) - - # Convert to strings representations of pixels - [:width, :height].each do |key| - options[key] = "#{options[key]}px" unless options[key].is_a?(String) - end - - template = ICONS[name.to_sym] - - if template - svg = template.dup - options.each do |key, value| - svg.gsub!("{#{key}}", value) - end - "#{svg}".html_safe - else - raise "Could not find the icon named #{name}" - end - end - - end -end diff --git a/lib/active_admin/iconic/icons.rb b/lib/active_admin/iconic/icons.rb deleted file mode 100644 index bee44720a87..00000000000 --- a/lib/active_admin/iconic/icons.rb +++ /dev/null @@ -1,142 +0,0 @@ -module ActiveAdmin - module Iconic - ICONS = { - :arrow_down => '', - :arrow_down_alt1 => '', - :arrow_down_alt2 => ' ', - :arrow_left => '', - :arrow_left_alt1 => '', - :arrow_left_alt2 => ' ', - :arrow_right => '', - :arrow_right_alt1 => '', - :arrow_right_alt2 => ' ', - :arrow_up => '', - :arrow_up_alt1 => '', - :arrow_up_alt2 => ' ', - :article => ' ', - :at => ' ', - :battery_charging => '', - :battery_empty => '', - :battery_full => '', - :battery_half => '', - :beaker => '', - :beaker_alt => ' ', - :bolt => ' ', - :book => '', - :book_alt => ' ', - :box => ' ', - :calendar => ' ', - :calendar_alt_fill => ' ', - :calendar_alt_stroke => ' ', - :cd => ' ', - :chat => ' ', - :chat_alt_fill => ' ', - :chat_alt_stroke => ' ', - :check => ' ', - :check_alt => '', - :clock => ' ', - :cloud => '', - :cog => '', - :cog_alt => ' ', - :comment_alt1_fill => ' ', - :comment_alt1_stroke => '', - :comment_alt2_fill => ' ', - :comment_alt2_stroke => ' ', - :comment_fill => ' ', - :comment_stroke => '', - :compass => ' ', - :cursor => ' ', - :denied => '', - :denied_alt => '', - :dial => ' ', - :document_fill => '', - :document_stroke => '', - :eject => ' ', - :equalizer => ' ', - :eyedropper => ' ', - :first => ' ', - :folder_fill => ' ', - :folder_stroke => '', - :fork => ' ', - :fullscreen => ' ', - :fullscreen_alt => ' ', - :fullscreen_exit => ' ', - :fullscreen_exit_alt => ' ', - :headphones => '', - :heart_fill => ' ', - :heart_stroke => ' ', - :home => ' ', - :image => ' ', - :info => ' ', - :iphone => '', - :key_fill => ' ', - :key_stroke => ' ', - :last => ' ', - :left_quote => ' ', - :left_quote_alt => ' ', - :lightbulb => ' ', - :link => ' ', - :lock_fill => '', - :lock_stroke => ' ', - :loop => ' ', - :loop_alt1 => ' ', - :loop_alt2 => ' ', - :loop_alt3 => ' ', - :loop_alt4 => ' ', - :magnifying_glass => '', - :magnifying_glass_alt => ' ', - :mail => '', - :mail_alt => ' ', - :map_pin_fill => ' ', - :map_pin_stroke => ' ', - :minus => '', - :minus_alt => '', - :moon_fill => ' ', - :moon_stroke => ' ', - :move => ' ', - :move_alt1 => ' ', - :move_alt2 => ' ', - :move_horizontal => ' ', - :move_horizontal_alt1 => ' ', - :move_horizontal_alt2 => ' ', - :move_vertical => ' ', - :move_vertical_alt1 => ' ', - :move_vertical_alt2 => ' ', - :movie => '', - :new_window => '', - :pause => ' ', - :pen => '', - :pen_alt_fill => ' ', - :pen_alt_stroke => '', - :pin => ' ', - :play => '', - :play_alt => '', - :plus => '', - :plus_alt => '', - :read_more => ' ', - :reload => ' ', - :reload_alt => ' ', - :right_quote => ' ', - :right_quote_alt => ' ', - :rss => ' ', - :rss_alt => ' ', - :spin => ' ', - :spin_alt => ' ', - :star => ' ', - :stop => '', - :sun => ' ', - :tag_fill => '', - :tag_stroke => ' ', - :trash_fill => '', - :trash_stroke => ' ', - :undo => ' ', - :unlock_fill => '', - :unlock_stroke => ' ', - :user => ' ', - :volume => ' ', - :volume_mute => ' ', - :x => ' ', - :x_alt => '', - } - end -end diff --git a/lib/active_admin/inputs.rb b/lib/active_admin/inputs.rb index 1ca41d81160..110eb0a437d 100644 --- a/lib/active_admin/inputs.rb +++ b/lib/active_admin/inputs.rb @@ -1,15 +1,19 @@ +# frozen_string_literal: true module ActiveAdmin module Inputs extend ActiveSupport::Autoload - autoload :DatepickerInput + module Filters + extend ActiveSupport::Autoload - autoload :FilterBase - autoload :FilterStringInput - autoload :FilterDateRangeInput - autoload :FilterNumericInput - autoload :FilterSelectInput - autoload :FilterCheckBoxesInput - autoload :FilterBooleanInput + autoload :Base + autoload :StringInput + autoload :TextInput + autoload :DateRangeInput + autoload :NumericInput + autoload :SelectInput + autoload :CheckBoxesInput + autoload :BooleanInput + end end end diff --git a/lib/active_admin/inputs/datepicker_input.rb b/lib/active_admin/inputs/datepicker_input.rb deleted file mode 100644 index baba4bc0d0b..00000000000 --- a/lib/active_admin/inputs/datepicker_input.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ActiveAdmin - module Inputs - class DatepickerInput < ::Formtastic::Inputs::StringInput - def input_html_options - options = super - options[:class] = [options[:class], "datepicker"].compact.join(' ') - options - end - end - end -end diff --git a/lib/active_admin/inputs/filter_base.rb b/lib/active_admin/inputs/filter_base.rb deleted file mode 100644 index 29862229d5c..00000000000 --- a/lib/active_admin/inputs/filter_base.rb +++ /dev/null @@ -1,33 +0,0 @@ -module ActiveAdmin - module Inputs - module FilterBase - include ::Formtastic::Inputs::Base - include ::ActiveAdmin::Filters::FormtasticAddons - - extend ::ActiveSupport::Autoload - autoload :SearchMethodSelect - - def input_wrapping(&block) - template.content_tag :div, template.capture(&block), wrapper_html_options - end - - def required? - false - end - - def wrapper_html_options - { :class => "filter_form_field #{as}" } - end - - # Override the standard finder to accept a proc - def collection_from_options - if options[:collection].is_a?(Proc) - template.instance_eval(&options[:collection]) - else - super - end - end - - end - end -end diff --git a/lib/active_admin/inputs/filter_base/search_method_select.rb b/lib/active_admin/inputs/filter_base/search_method_select.rb deleted file mode 100644 index 68384aaa185..00000000000 --- a/lib/active_admin/inputs/filter_base/search_method_select.rb +++ /dev/null @@ -1,73 +0,0 @@ -# This is a common set of Formtastic overrides needed to build a filter form -# that lets you select from a set of search methods for a given attribute. -# -# Your class must declare available filters for this module to work. -# Those filters must be recognizable by Metasearch. For example: -# -# class FilterNumericInput < ::Formtastic::Inputs::NumberInput -# include FilterBase -# include FilterBase::SearchMethodSelect -# -# filter :equals, :greater_than, :less_than -# end -# -module ActiveAdmin - module Inputs - module FilterBase - module SearchMethodSelect - - def self.included(base) - base.extend ClassMethods - end - - module ClassMethods - attr_reader :filters - - def filter(*filters) - (@filters ||= []).push *filters - end - end - - def wrapper_html_options - opts = super - (opts[:class] ||= '') << ' select_and_search' - opts - end - - def to_html - input_wrapping do - label_html << # your label - select_html << # the dropdown that holds the available search methods - input_html # your input field - end - end - - def input_html - builder.text_field current_filter, input_html_options - end - - def select_html - template.select_tag '', template.options_for_select(filter_options, current_filter) - end - - def filters - options[:filters] || self.class.filters - end - - def current_filter - @current_filter ||= begin - methods = filters.map{ |f| "#{method}_#{f}" } - methods.detect{ |m| @object.send m } || methods.first - end - end - - def filter_options - filters.collect do |filter| - [I18n.t("active_admin.filters.predicates.#{filter}"), "#{method}_#{filter}"] - end - end - - end - end - end -end diff --git a/lib/active_admin/inputs/filter_boolean_input.rb b/lib/active_admin/inputs/filter_boolean_input.rb deleted file mode 100644 index 0e71c6355ca..00000000000 --- a/lib/active_admin/inputs/filter_boolean_input.rb +++ /dev/null @@ -1,32 +0,0 @@ -module ActiveAdmin - module Inputs - class FilterBooleanInput < ::Formtastic::Inputs::BooleanInput - include FilterBase - - def to_html - input_wrapping do - [ label_html, - check_box_html - ].join("\n").html_safe - end - end - - def search_method - method.to_s.match(search_conditions) ? method : "#{method}_eq" - end - - def checked? - object && boolean_checked?(object.send(search_method), checked_value) - end - - def input_html_options - { :name => "q[#{ search_method }]" } - end - - def search_conditions - /(is_true|is_false|is_present|is_blank|is_null|is_not_null)\z/ - end - - end - end -end diff --git a/lib/active_admin/inputs/filter_check_boxes_input.rb b/lib/active_admin/inputs/filter_check_boxes_input.rb deleted file mode 100644 index 464a2c0bcbf..00000000000 --- a/lib/active_admin/inputs/filter_check_boxes_input.rb +++ /dev/null @@ -1,40 +0,0 @@ -module ActiveAdmin - module Inputs - class FilterCheckBoxesInput < ::Formtastic::Inputs::CheckBoxesInput - include FilterBase - - def input_name - "#{object_name}[#{association_primary_key || method}_in][]" - end - - def selected_values - @object.send("#{association_primary_key || method}_in") || [] - end - - # Add whitespace before label - def choice_label(choice) - ' ' + super - end - - # Don't wrap in UL tag - def choices_group_wrapping(&block) - template.capture(&block) - end - - # Don't wrap in LI tag - def choice_wrapping(html_options, &block) - template.capture(&block) - end - - # Don't render hidden fields - def hidden_field_for_all - "" - end - - # Don't render hidden fields - def hidden_fields? - false - end - end - end -end diff --git a/lib/active_admin/inputs/filter_date_range_input.rb b/lib/active_admin/inputs/filter_date_range_input.rb deleted file mode 100644 index 94f80cbdf97..00000000000 --- a/lib/active_admin/inputs/filter_date_range_input.rb +++ /dev/null @@ -1,34 +0,0 @@ -module ActiveAdmin - module Inputs - class FilterDateRangeInput < ::Formtastic::Inputs::StringInput - include FilterBase - - def to_html - input_wrapping do - [ label_html, - builder.text_field(gt_input_name, input_html_options(gt_input_name)), - template.content_tag(:span, "-", :class => "seperator"), - builder.text_field(lt_input_name, input_html_options(lt_input_name)), - ].join("\n").html_safe - end - end - - def gt_input_name - "#{method}_gte" - end - alias :input_name :gt_input_name - - def lt_input_name - "#{method}_lte" - end - - def input_html_options(input_name = gt_input_name) - current_value = @object.send(input_name) - { :size => 12, - :class => "datepicker", - :max => 10, - :value => current_value.respond_to?(:strftime) ? current_value.strftime("%Y-%m-%d") : "" } - end - end - end -end diff --git a/lib/active_admin/inputs/filter_numeric_input.rb b/lib/active_admin/inputs/filter_numeric_input.rb deleted file mode 100644 index 0342293512f..00000000000 --- a/lib/active_admin/inputs/filter_numeric_input.rb +++ /dev/null @@ -1,10 +0,0 @@ -module ActiveAdmin - module Inputs - class FilterNumericInput < ::Formtastic::Inputs::NumberInput - include FilterBase - include FilterBase::SearchMethodSelect - - filter :equals, :greater_than, :less_than - end - end -end diff --git a/lib/active_admin/inputs/filter_select_input.rb b/lib/active_admin/inputs/filter_select_input.rb deleted file mode 100644 index 85ddafcdeec..00000000000 --- a/lib/active_admin/inputs/filter_select_input.rb +++ /dev/null @@ -1,44 +0,0 @@ -module ActiveAdmin - module Inputs - class FilterSelectInput < ::Formtastic::Inputs::SelectInput - include FilterBase - - # When it's a HABTM or has_many association, Formtastic builds "object_ids". - # That doesn't fit our scenario, so we override it here. - def input_name - name = method.to_s - name.concat '_id' if reflection - name.concat multiple? ? '_in' : '_eq' - end - - # Include the "Any" option if it's a dropdown, but not if it's a multi-select. - def input_options - super.merge :include_blank => multiple? ? false : I18n.t('active_admin.any') - end - - # was "#{object_name}[#{association_primary_key}]" - def input_html_options_name - "#{object_name}[#{input_name}]" - end - - # Would normally return true for has_many and HABTM, which would subsequently - # cause the select field to be multi-select instead of a dropdown. - def multiple_by_association? - false - end - - # Provides an efficient default lookup query if the attribute is a DB column. - def collection - unless Rails::VERSION::MAJOR == 3 && Rails::VERSION::MINOR < 2 - return pluck_column if !options[:collection] && column_for(method) - end - super - end - - def pluck_column - @object.base.reorder("#{method} asc").uniq.pluck method - end - - end - end -end diff --git a/lib/active_admin/inputs/filter_string_input.rb b/lib/active_admin/inputs/filter_string_input.rb deleted file mode 100644 index cbc27a0f045..00000000000 --- a/lib/active_admin/inputs/filter_string_input.rb +++ /dev/null @@ -1,24 +0,0 @@ -module ActiveAdmin - module Inputs - class FilterStringInput < ::Formtastic::Inputs::StringInput - include FilterBase - include FilterBase::SearchMethodSelect - - filter :contains, :equals, :starts_with, :ends_with - - # If the filter method includes a search condition, build a normal string search field. - # Else, build a search field with a companion dropdown to choose a search condition from. - def to_html - if method.to_s =~ /_(#{filters.join('|')})\z/ - input_wrapping do - label_html << - builder.text_field(method, input_html_options) - end - else - super # SearchMethodSelect#to_html - end - end - - end - end -end diff --git a/lib/active_admin/inputs/filters/base.rb b/lib/active_admin/inputs/filters/base.rb new file mode 100644 index 00000000000..3eba474bcea --- /dev/null +++ b/lib/active_admin/inputs/filters/base.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true +module ActiveAdmin + module Inputs + module Filters + module Base + include ::Formtastic::Inputs::Base + include ::ActiveAdmin::Filters::FormtasticAddons + + extend ::ActiveSupport::Autoload + autoload :SearchMethodSelect + + def input_wrapping(&block) + template.content_tag :div, template.capture(&block), wrapper_html_options + end + + def required? + false + end + + # Can pass proc to filter label option + def label_from_options + res = super + res = res.call if res.is_a? Proc + res + end + + def wrapper_html_options + opts = super + (opts[:class] ||= "") << " filters-form-field" + opts + end + + # Override the standard finder to accept a proc + def collection_from_options + if options[:collection].is_a?(Proc) + template.instance_exec(&options[:collection]) + else + super + end + end + + end + end + end +end diff --git a/lib/active_admin/inputs/filters/base/search_method_select.rb b/lib/active_admin/inputs/filters/base/search_method_select.rb new file mode 100644 index 00000000000..06046e54c42 --- /dev/null +++ b/lib/active_admin/inputs/filters/base/search_method_select.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true +# This is a common set of Formtastic overrides needed to build a filter form +# that lets you select from a set of search methods for a given attribute. +# +# Your class must declare available filters for this module to work. +# Those filters must be recognizable by Ransack. For example: +# +# class NumericInput < ::Formtastic::Inputs::NumberInput +# include Base +# include Base::SearchMethodSelect +# +# filter :eq, :gt, :lt +# end +# +module ActiveAdmin + module Inputs + module Filters + module Base + module SearchMethodSelect + + def self.included(base) + base.extend ClassMethods + end + + module ClassMethods + attr_reader :filters + + def filter(*filters) + (@filters ||= []).push(*filters) + end + end + + def to_html + input_wrapping do + [ + label_html, # your label + '
', + select_html, # the dropdown that holds the available search methods + input_html, # your input field + '
' + ].join("\n").html_safe + end + end + + def input_html + builder.text_field current_filter, input_html_options + end + + def select_html + template.select_tag "", template.options_for_select(filter_options, current_filter), "data-search-methods": "" + end + + def filters + options[:filters] || self.class.filters + end + + def current_filter + @current_filter ||= begin + methods = filters.map { |f| "#{method}_#{f}" } + methods.detect { |m| @object.public_send m } || methods.first + end + end + + def filter_options + filters.collect do |filter| + [Ransack::Translate.predicate(filter).capitalize, "#{method}_#{filter}"] + end + end + + end + end + end + end +end diff --git a/lib/active_admin/inputs/filters/boolean_input.rb b/lib/active_admin/inputs/filters/boolean_input.rb new file mode 100644 index 00000000000..978addee236 --- /dev/null +++ b/lib/active_admin/inputs/filters/boolean_input.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true +module ActiveAdmin + module Inputs + module Filters + class BooleanInput < ::Formtastic::Inputs::SelectInput + include Base + + def input_name + return method if seems_searchable? + + "#{method}_eq" + end + + def input_html_options_name + "#{object_name}[#{input_name}]" # was "#{object_name}[#{association_primary_key}]" + end + + # Provide the AA translation to the blank input field. + def include_blank + I18n.t "active_admin.any" if super + end + end + end + end +end diff --git a/lib/active_admin/inputs/filters/check_boxes_input.rb b/lib/active_admin/inputs/filters/check_boxes_input.rb new file mode 100644 index 00000000000..79440992ed2 --- /dev/null +++ b/lib/active_admin/inputs/filters/check_boxes_input.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true +module ActiveAdmin + module Inputs + module Filters + class CheckBoxesInput < ::Formtastic::Inputs::CheckBoxesInput + include Base + + def input_name + "#{object_name}[#{searchable_method_name}_in][]" + end + + def checked?(value) + selected_values.include?(value.to_s) + end + + def selected_values + @selected_values ||= (@object.public_send(:"#{searchable_method_name}_in") || []).map(&:to_s) + end + + def searchable_method_name + if searchable_has_many_through? + "#{reflection.through_reflection.name}_#{reflection.foreign_key}" + else + association_primary_key || method + end + end + + # Don't wrap in UL tag + def choices_group_wrapping(&block) + template.capture(&block) + end + + # Don't wrap in LI tag + def choice_wrapping(html_options, &block) + template.capture(&block) + end + + # Don't render hidden fields + def hidden_field_for_all + "" + end + + # Don't render hidden fields + def hidden_fields? + false + end + end + end + end +end diff --git a/lib/active_admin/inputs/filters/date_range_input.rb b/lib/active_admin/inputs/filters/date_range_input.rb new file mode 100644 index 00000000000..edb33f215cf --- /dev/null +++ b/lib/active_admin/inputs/filters/date_range_input.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true +module ActiveAdmin + module Inputs + module Filters + class DateRangeInput < ::Formtastic::Inputs::StringInput + include Base + + def to_html + input_wrapping do + [ label_html, + '
', + builder.date_field(gt_input_name, input_html_options_for(gt_input_name, gt_input_placeholder)), + builder.date_field(lt_input_name, input_html_options_for(lt_input_name, lt_input_placeholder)), + '
' + ].join("\n").html_safe + end + end + + def gt_input_name + "#{method}_gteq" + end + alias :input_name :gt_input_name + + def lt_input_name + "#{method}_lteq" + end + + def input_html_options + { size: 12, + class: "datepicker", + maxlength: 10 }.merge(options[:input_html] || {}) + end + + def input_html_options_for(input_name, placeholder) + current_value = begin + #cast value to date object before rendering input + @object.public_send(input_name).to_s.to_date + rescue + nil + end + { placeholder: placeholder, + value: current_value ? current_value.strftime("%Y-%m-%d") : "" }.merge(input_html_options) + end + + def gt_input_placeholder + I18n.t("active_admin.filters.predicates.from") + end + + def lt_input_placeholder + I18n.t("active_admin.filters.predicates.to") + end + end + end + end +end diff --git a/lib/active_admin/inputs/filters/numeric_input.rb b/lib/active_admin/inputs/filters/numeric_input.rb new file mode 100644 index 00000000000..62838cab9e3 --- /dev/null +++ b/lib/active_admin/inputs/filters/numeric_input.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true +module ActiveAdmin + module Inputs + module Filters + class NumericInput < ::Formtastic::Inputs::NumberInput + include Base + include Base::SearchMethodSelect + + filter :eq, :gt, :lt + end + end + end +end diff --git a/lib/active_admin/inputs/filters/select_input.rb b/lib/active_admin/inputs/filters/select_input.rb new file mode 100644 index 00000000000..23c9a041022 --- /dev/null +++ b/lib/active_admin/inputs/filters/select_input.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true +module ActiveAdmin + module Inputs + module Filters + class SelectInput < ::Formtastic::Inputs::SelectInput + include Base + + def input_name + return method if seems_searchable? + + searchable_method_name + (multiple? ? "_in" : "_eq") + end + + def searchable_method_name + if searchable_has_many_through? + "#{reflection.through_reflection.name}_#{reflection.foreign_key}" + else + reflection_searchable? ? "#{method}_#{reflection.association_primary_key}" : method.to_s + end + end + + # Provide the AA translation to the blank input field. + def include_blank + I18n.t "active_admin.any" if super + end + + def input_html_options_name + "#{object_name}[#{input_name}]" # was "#{object_name}[#{association_primary_key}]" + end + + # Would normally return true for has_many and HABTM, which would subsequently + # cause the select field to be multi-select instead of a dropdown. + def multiple_by_association? + false + end + + # Provides an efficient default lookup query if the attribute is a DB column. + def collection + if !options[:collection] && column + pluck_column + else + super + end + rescue ActiveRecord::QueryCanceled => error + raise ActiveRecord::QueryCanceled.new "#{error.message.strip} while querying the values for the ActiveAdmin :#{method} filter" + end + + def pluck_column + klass.reorder("#{method} asc").distinct.pluck method + end + + def reflection_searchable? + reflection && !reflection.polymorphic? + end + + end + end + end +end diff --git a/lib/active_admin/inputs/filters/string_input.rb b/lib/active_admin/inputs/filters/string_input.rb new file mode 100644 index 00000000000..d1574a369dc --- /dev/null +++ b/lib/active_admin/inputs/filters/string_input.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true +module ActiveAdmin + module Inputs + module Filters + class StringInput < ::Formtastic::Inputs::StringInput + include Base + include Base::SearchMethodSelect + + filter :cont, :eq, :start, :end + + # If the filter method includes a search condition, build a normal string search field. + # Else, build a search field with a companion dropdown to choose a search condition from. + def to_html + if seems_searchable? + input_wrapping do + label_html << + builder.text_field(method, input_html_options) + end + else + super # SearchMethodSelect#to_html + end + end + + def filters + options[:filters] || resource_default_filters || namespace_default_filters || self.class.filters + end + + private + + def resource_default_filters + config = template.active_admin_config if template.respond_to?(:active_admin_config) + config.string_input_filters if config.respond_to?(:string_input_filters) + end + + def namespace_default_filters + template.respond_to?(:active_admin_config) && + template.active_admin_config&.namespace&.string_input_filters + end + + end + end + end +end diff --git a/lib/active_admin/inputs/filters/text_input.rb b/lib/active_admin/inputs/filters/text_input.rb new file mode 100644 index 00000000000..1b228f15604 --- /dev/null +++ b/lib/active_admin/inputs/filters/text_input.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true +module ActiveAdmin + module Inputs + module Filters + class TextInput < ::Formtastic::Inputs::TextInput + include Base + include Base::SearchMethodSelect + + def input_html_options + { + cols: builder.default_text_area_width, + rows: builder.default_text_area_height + }.merge(super) + end + + def to_html + input_wrapping do + label_html << + builder.text_area(method, input_html_options) + end + end + + end + end + end +end diff --git a/lib/active_admin/localizers.rb b/lib/active_admin/localizers.rb new file mode 100644 index 00000000000..a0a8f1f4b12 --- /dev/null +++ b/lib/active_admin/localizers.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +require_relative "localizers/resource_localizer" + +module ActiveAdmin + module Localizers + class << self + def resource(active_admin_config) + ResourceLocalizer.from_resource(active_admin_config) + end + end + end +end diff --git a/lib/active_admin/localizers/resource_localizer.rb b/lib/active_admin/localizers/resource_localizer.rb new file mode 100644 index 00000000000..2e65e3f8158 --- /dev/null +++ b/lib/active_admin/localizers/resource_localizer.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true +module ActiveAdmin + module Localizers + class ResourceLocalizer + class << self + def from_resource(resource_config) + new(resource_config.resource_name.i18n_key, resource_config.resource_label) + end + + def translate(key, options) + new(options.delete(:model_name), options.delete(:model)).translate(key, options) + end + alias_method :t, :translate + end + + def initialize(model_name, model = nil) + @model_name = model_name + @model = model || model_name.to_s.titleize + end + + def translate(key, options = {}) + scope = options.delete(:scope) + specific_key = array_to_key("resources", @model_name, scope, key) + defaults = [array_to_key(scope, key), key.to_s.titleize] + ::I18n.t specific_key, **options.reverse_merge(model: @model, default: defaults, scope: "active_admin") + end + alias_method :t, :translate + + protected + + def array_to_key(*arr) + arr.flatten.compact.join(".").to_sym + end + end + end +end diff --git a/lib/active_admin/menu.rb b/lib/active_admin/menu.rb index 7f1de9bfb67..9662ca88dd9 100644 --- a/lib/active_admin/menu.rb +++ b/lib/active_admin/menu.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin # Each Namespace builds up it's own menu as the global navigation @@ -27,6 +28,7 @@ def initialize def [](id) @children[normalize_id(id)] end + def []=(id, child) @children[normalize_id(id)] = child end @@ -46,11 +48,12 @@ def []=(id, child) # menu.add parent: 'Dashboard', label: 'My Child Dashboard' # def add(options) + options = options.dup # Make sure parameter is not modified item = if parent = options.delete(:parent) - (self[parent] || add(:label => parent)).add options - else - _add options.merge :parent => self - end + (self[parent] || add(label: parent)).add options + else + _add options.merge parent: self + end yield(item) if block_given? @@ -59,19 +62,19 @@ def add(options) # Whether any children match the given item. def include?(item) - @children.values.include? item + @children.value?(item) end # Used in the UI to visually distinguish which menu item is selected. - def current?(item) - self == item || include?(item) + def current?(item, children: true) + self == item || (children && include?(item)) end # Returns sorted array of menu items that should be displayed in this context. # Sorts by priority first, then alphabetically by label if needed. def items(context = nil) - @children.values.select{ |i| i.display?(context) }.sort do |a,b| - result = a.priority <=> b.priority + @children.values.select { |i| i.display?(context) }.sort do |a, b| + result = a.priority <=> b.priority result = a.label(context) <=> b.label(context) if result == 0 result end @@ -91,8 +94,10 @@ def _add(options) def normalize_id(id) case id - when String, Symbol - id.to_s.downcase.gsub ' ', '_' + when String, Symbol, ActiveModel::Name + id.to_s.downcase.tr " ", "_" + when ActiveAdmin::Resource::Name + id.param_key else raise TypeError, "#{id.class} isn't supported as a Menu ID" end diff --git a/lib/active_admin/menu_collection.rb b/lib/active_admin/menu_collection.rb index ca7ee359bd5..a6f604e672c 100644 --- a/lib/active_admin/menu_collection.rb +++ b/lib/active_admin/menu_collection.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin DEFAULT_MENU = :default @@ -5,9 +6,6 @@ module ActiveAdmin # A MenuCollection stores multiple menus for any given namespace. Namespaces delegate # the addition of menu items to this class. class MenuCollection - - NoMenuError = Class.new(KeyError) - def initialize @menus = {} @build_callbacks = [] @@ -27,14 +25,14 @@ def clear! end def exists?(menu_name) - @menus.keys.include? menu_name + @menus.key?(menu_name) end def fetch(menu_name) build_menus! @menus[menu_name] or - raise NoMenuError, "No menu by the name of #{menu_name.inspect} in availble menus: #{@menus.keys.join(", ")}" + raise NoMenuError, "No menu by the name of #{menu_name.inspect} in available menus: #{@menus.keys.join(", ")}" end # Add callbacks that will be run when the menu is going to be built. This @@ -89,8 +87,6 @@ def find_or_create(menu_name) @menus[menu_name] ||= ActiveAdmin::Menu.new end - - end end diff --git a/lib/active_admin/menu_item.rb b/lib/active_admin/menu_item.rb index bb67cc890d0..e9b3f707671 100644 --- a/lib/active_admin/menu_item.rb +++ b/lib/active_admin/menu_item.rb @@ -1,4 +1,5 @@ -require 'active_admin/view_helpers/method_or_proc_helper' +# frozen_string_literal: true +require_relative "view_helpers/method_or_proc_helper" module ActiveAdmin class MenuItem @@ -48,13 +49,13 @@ class MenuItem # def initialize(options = {}) super() # MenuNode - @label = options[:label] - @dirty_id = options[:id] || options[:label] - @url = options[:url] || '#' - @priority = options[:priority] || 10 - @html_options = options[:html_options] || {} - @should_display = options[:if] || proc{true} - @parent = options[:parent] + @label = options[:label] + @dirty_id = options[:id] || options[:label] + @url = options[:url] || "#" + @priority = options[:priority] || 10 + @html_options = options[:html_options] || {} + @should_display = options[:if] || proc { true } + @parent = options[:parent] yield(self) if block_given? # Builder style syntax end @@ -64,27 +65,21 @@ def id end def label(context = nil) - render_in_context context, @label + render_in_context(context, @label) end def url(context = nil) - render_in_context context, @url + render_in_context(context, @url) end # Don't display if the :if option passed says so # Don't display if the link isn't real, we have children, and none of the children are being displayed. def display?(context = nil) return false unless render_in_context(context, @should_display) - return false if !real_url?(context) && @children.any? && !items(context).any? + return false if !real_url?(context) && @children.any? && !items(context).any? true end - # Returns an array of the ancestory of this menu item. - # The first item is the immediate parent of the item. - def ancestors - parent ? [parent, parent.ancestors].flatten : [] - end - private # URL is not nil, empty, or '#' @@ -92,6 +87,5 @@ def real_url?(context = nil) url = url context url.present? && url != '#' end - end end diff --git a/lib/active_admin/namespace.rb b/lib/active_admin/namespace.rb index 6542782f0d2..7c28032123a 100644 --- a/lib/active_admin/namespace.rb +++ b/lib/active_admin/namespace.rb @@ -1,4 +1,5 @@ -require 'active_admin/resource_collection' +# frozen_string_literal: true +require_relative "resource_collection" module ActiveAdmin @@ -11,7 +12,7 @@ module ActiveAdmin # # For example: # - # ActiveAdmin.register Post, :namespace => :admin + # ActiveAdmin.register Post, namespace: :admin # # Will register the Post model into the "admin" namespace. This will namespace the # urls for the resource to "/admin/posts" and will set the controller to @@ -19,25 +20,47 @@ module ActiveAdmin # # You can also register to the "root" namespace, which is to say no namespace at all. # - # ActiveAdmin.register Post, :namespace => false + # ActiveAdmin.register Post, namespace: false # # This will register the resource to an instantiated namespace called :root. The # resource will be accessible from "/posts" and the controller will be PostsController. # class Namespace - RegisterEvent = 'active_admin.namespace.register'.freeze + class << self + def setting(name, default) + ActiveAdmin.deprecator.warn "This method does not do anything and will be removed." + end + end + + RegisterEvent = "active_admin.namespace.register".freeze - attr_reader :application, :resources, :name, :menus + attr_reader :application, :resources, :menus def initialize(application, name) @application = application - @name = name.to_s.underscore.to_sym + @name = name.to_s.underscore @resources = ResourceCollection.new register_module unless root? build_menu_collection end - # Register a resource into this namespace. The preffered method to access this is to + def name + @name.to_sym + end + + def settings + @settings ||= SettingsNode.build(application.namespace_settings) + end + + def respond_to_missing?(method, include_private = false) + settings.respond_to?(method) || super + end + + def method_missing(method, *args) + settings.respond_to?(method) ? settings.send(method, *args) : super + end + + # Register a resource into this namespace. The preferred method to access this is to # use the global registration ActiveAdmin.register which delegates to the proper # namespace instance. def register(resource_class, options = {}, &block) @@ -45,11 +68,11 @@ def register(resource_class, options = {}, &block) # Register the resource register_resource_controller(config) - parse_registration_block(config, &block) if block_given? + parse_registration_block(config, &block) if block reset_menu! # Dispatch a registration event - ActiveAdmin::Event.dispatch ActiveAdmin::Resource::RegisterEvent, config + ActiveSupport::Notifications.instrument ActiveAdmin::Resource::RegisterEvent, { active_admin_resource: config } # Return the config config @@ -60,7 +83,7 @@ def register_page(name, options = {}, &block) # Register the resource register_page_controller(config) - parse_page_registration_block(config, &block) if block_given? + parse_page_registration_block(config, &block) if block reset_menu! config @@ -78,8 +101,11 @@ def root? # Namespace.new(:root).module_name # => nil # def module_name - return nil if root? - @module_name ||= name.to_s.camelize + root? ? nil : @name.camelize + end + + def route_prefix + root? ? nil : @name end # Unload all the registered resources for this namespace @@ -93,12 +119,6 @@ def resource_for(klass) resources[klass] end - # Override from ActiveAdmin::Settings to inherit default attributes - # from the application - def read_default_setting(name) - application.send(name) - end - def fetch_menu(name) @menus.fetch(name) end @@ -110,65 +130,30 @@ def reset_menu! # Add a callback to be ran when we build the menu # # @param [Symbol] name The name of the menu. Default: :default - # @param [Proc] block The block to be ran when the menu is built + # @yield [ActiveAdmin::Menu] The block to be ran when the menu is built # - # @returns [void] - def build_menu(name = DEFAULT_MENU, &block) + # @return [void] + def build_menu(name = DEFAULT_MENU) @menus.before_build do |menus| menus.menu name do |menu| - block.call(menu) + yield menu end end end - # Add the default logout button to the menu, using the ActiveAdmin configuration settings - # - # @param [ActiveAdmin::MenuItem] menu The menu to add the logout link to - # @param [Fixnum] priority Override the default priority of 100 to position the logout button where you want - # @param [Hash] html_options An options hash to pass along to link_to - # - # @returns [void] - def add_logout_button_to_menu(menu, priority=100, html_options={}) - if logout_link_path - logout_method = logout_link_method || :get - menu.add :id => 'logout', - :priority => priority, - :label => proc{ I18n.t('active_admin.logout') }, - :html_options => html_options.reverse_merge(:method => logout_method), - :url => proc{ render_or_call_method_or_proc_on self, active_admin_namespace.logout_link_path }, - :if => proc{ current_active_admin_user? } - end - end - protected def build_menu_collection @menus = MenuCollection.new - @menus.on_build do |menus| - build_default_utility_nav - + @menus.on_build do resources.each do |resource| resource.add_to_menu(@menus) end end end - # Builds the default utility navigation in top right header with current user & logout button - def build_default_utility_nav - return if @menus.exists? :utility_navigation - @menus.menu :utility_navigation do |menu| - menu.add :label => proc{ display_name current_active_admin_user }, - :url => '#', - :id => 'current_user', - :if => proc{ current_active_admin_user? } - - add_logout_button_to_menu menu - end - end - - # Either returns an existing Resource instance or builds a new - # one for the resource and options + # Either returns an existing Resource instance or builds a new one. def find_or_build_resource(resource_class, options) resources.add Resource.new(self, resource_class, options) end @@ -177,6 +162,7 @@ def build_page(name, options) resources.add Page.new(self, name, options) end + # TODO: replace `eval` with `Class.new` def register_page_controller(config) eval "class ::#{config.controller_name} < ActiveAdmin::PageController; end" config.controller.active_admin_config = config @@ -184,10 +170,9 @@ def register_page_controller(config) def unload_resources! resources.each do |resource| - parent = (module_name || 'Object').constantize - const_name = resource.controller_name.split('::').last - # Remove the const if its been defined - parent.send(:remove_const, const_name) if parent.const_defined?(const_name) + parent = (module_name || "Object").constantize + name = resource.controller_name.split("::").last + parent.send(:remove_const, name) if parent.const_defined?(name, false) # Remove circular references resource.controller.active_admin_config = nil @@ -200,9 +185,12 @@ def unload_resources! # Creates a ruby module to namespace all the classes in if required def register_module - eval "module ::#{module_name}; end" + unless Object.const_defined? module_name + Object.const_set module_name, Module.new + end end + # TODO: replace `eval` with `Class.new` def register_resource_controller(config) eval "class ::#{config.controller_name} < ActiveAdmin::ResourceController; end" config.controller.active_admin_config = config @@ -217,5 +205,21 @@ def parse_page_registration_block(config, &block) PageDSL.new(config).run_registration_block(&block) end + class Store + include Enumerable + delegate :[], :[]=, :empty?, to: :@namespaces + + def initialize + @namespaces = {} + end + + def each(&block) + @namespaces.values.each(&block) + end + + def names + @namespaces.keys + end + end end end diff --git a/lib/active_admin/namespace_settings.rb b/lib/active_admin/namespace_settings.rb new file mode 100644 index 00000000000..0c817be8c0c --- /dev/null +++ b/lib/active_admin/namespace_settings.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true +require_relative "dynamic_settings_node" + +module ActiveAdmin + class NamespaceSettings < DynamicSettingsNode + # The default number of resources to display on index pages + register :default_per_page, 30 + + # The max number of resources to display on index pages and batch exports + register :max_per_page, 10_000 + + # The title which gets displayed in the main layout + register :site_title, "", :string_symbol_or_proc + + # The method to call in controllers to get the current user + register :current_user_method, false + + # The method to call in the controllers to ensure that there + # is a currently authenticated admin user + register :authentication_method, false + + # The path to log user's out with. If set to a symbol, we assume + # that it's a method to call which returns the path + register :logout_link_path, :destroy_admin_user_session_path + + # Whether the batch actions are enabled or not + register :batch_actions, false + + # Whether filters are enabled + register :filters, true + + # Default `filters` list for string filters in the namespace. + # When `nil`, the global `ActiveAdmin::Inputs::Filters::StringInput.filters` list is used. + register :string_input_filters, nil + + # The namespace root + register :root_to, "dashboard#index" + + # Options that are passed to root_to + register :root_to_options, {} + + # Options passed to the routes, i.e. { path: '/custom' } + register :route_options, {} + + # Display breadcrumbs + register :breadcrumb, true + + # Display create another checkbox on a new page + # @return [Boolean] (true) + register :create_another, false + + # Default CSV options + register :csv_options, { col_sep: ",", byte_order_mark: "\xEF\xBB\xBF" } + + # Default Download Links options + register :download_links, true + + # The authorization adapter to use + register :authorization_adapter, ActiveAdmin::AuthorizationAdapter + + # A proc to be used when a user is not authorized to view the current resource + register :on_unauthorized_access, :rescue_active_admin_access_denied + + # Whether to display 'Current Filters' on search screen + register :current_filters, true + + # class to handle ordering + register :order_clause, ActiveAdmin::OrderClause + + # default show_count for scopes + register :scopes_show_count, true + + # Request parameters that are permitted by default + register :permitted_params, [ + :utf8, :_method, :authenticity_token, :commit, :id + ] + + # Set flash message keys that shouldn't show in ActiveAdmin. + # By default, we remove the `timedout` key from Devise. + register :flash_keys_to_except, ["timedout"] + + # Include association filters by default + register :include_default_association_filters, true + + register :maximum_association_filter_arity, :unlimited + + register :filter_columns_for_large_association, [ + :display_name, + :full_name, + :name, + :username, + :login, + :title, + :email, + ] + register :filter_method_for_large_association, "_start" + end +end diff --git a/lib/active_admin/order_clause.rb b/lib/active_admin/order_clause.rb new file mode 100644 index 00000000000..75c803f4df4 --- /dev/null +++ b/lib/active_admin/order_clause.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true +module ActiveAdmin + class OrderClause + attr_reader :field, :order, :active_admin_config + + def initialize(active_admin_config, clause) + clause =~ /^([\w\.]+)(->'\w+')?_(desc|asc)$/ + @column = $1 + @op = $2 + @order = $3 + @active_admin_config = active_admin_config + @field = [@column, @op].compact.join + end + + def valid? + @field.present? && @order.present? + end + + def apply(chain) + chain.reorder(Arel.sql sql) + end + + def to_sql + [table_column, @op, " ", @order].compact.join + end + + def table + active_admin_config.resource_column_names.include?(@column) ? active_admin_config.resource_table_name : nil + end + + def table_column + if (@column.include?('.')) + @column + else + [table, active_admin_config.resource_quoted_column_name(@column)].compact.join(".") + end + end + + def sql + custom_sql || to_sql + end + + protected + + def custom_sql + if active_admin_config.ordering[@column].present? + active_admin_config.ordering[@column].call(self) + end + end + + end +end diff --git a/lib/active_admin/orm/active_record.rb b/lib/active_admin/orm/active_record.rb new file mode 100644 index 00000000000..7fa22c66cc7 --- /dev/null +++ b/lib/active_admin/orm/active_record.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +# ActiveRecord-specific plugins should be required here + +ActiveAdmin::DatabaseHitDuringLoad.database_error_classes << ActiveRecord::StatementInvalid + +require_relative "active_record/comments" diff --git a/lib/active_admin/orm/active_record/comments.rb b/lib/active_admin/orm/active_record/comments.rb new file mode 100644 index 00000000000..f2db90c8b92 --- /dev/null +++ b/lib/active_admin/orm/active_record/comments.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true +require_relative "comments/views" +require_relative "comments/namespace_helper" +require_relative "comments/resource_helper" + +# Add the comments configuration +ActiveAdmin::Application.inheritable_setting :comments, true +ActiveAdmin::Application.inheritable_setting :comments_registration_name, "Comment" +ActiveAdmin::Application.inheritable_setting :comments_order, "created_at ASC" +ActiveAdmin::Application.inheritable_setting :comments_menu, {} + +# Insert helper modules +ActiveAdmin::Namespace.send :include, ActiveAdmin::Comments::NamespaceHelper +ActiveAdmin::Resource.send :include, ActiveAdmin::Comments::ResourceHelper + +# Load the model as soon as it's referenced. By that point, Rails & Kaminari will be ready +ActiveAdmin.autoload :Comment, "active_admin/orm/active_record/comments/comment" + +# Hint i18n-tasks about model and attribute translations used by default install +# i18n-tasks-use t('activerecord.models.comment') +# i18n-tasks-use t('activerecord.models.active_admin/comment') +# i18n-tasks-use t('activerecord.attributes.active_admin/comment.author_type') +# i18n-tasks-use t('activerecord.attributes.active_admin/comment.body') +# i18n-tasks-use t('activerecord.attributes.active_admin/comment.created_at') +# i18n-tasks-use t('activerecord.attributes.active_admin/comment.namespace') +# i18n-tasks-use t('activerecord.attributes.active_admin/comment.resource_type') +# i18n-tasks-use t('activerecord.attributes.active_admin/comment.updated_at') +# i18n-tasks-use t('active_admin.scopes.all') + +# Walk through all the loaded namespaces after they're loaded +ActiveAdmin.after_load do |app| + app.namespaces.each do |namespace| + namespace.register ActiveAdmin::Comment, as: namespace.comments_registration_name do + actions :index, :show, :create, :destroy + + menu namespace.comments ? namespace.comments_menu : false + + config.comments = false # Don't allow comments on comments + config.batch_actions = false # The default destroy batch action isn't showing up anyway... + + scope :all, show_count: false + # Register a scope for every namespace that exists. + # The current namespace will be the default scope. + app.namespaces.map(&:name).each do |name| + scope name, default: namespace.name == name do |scope| + scope.where namespace: name.to_s + end + end + + # Store the author and namespace + before_save do |comment| + comment.namespace = active_admin_config.namespace.name + comment.author = current_active_admin_user + end + + controller do + # Prevent N+1 queries + def scoped_collection + super.includes(:author, :resource) + end + + # Redirect to the resource show page after comment creation + def create + create! do |success, failure| + success.html do + redirect_back fallback_location: active_admin_root + end + failure.html do + flash[:error] = I18n.t "active_admin.comments.errors.empty_text" + redirect_back fallback_location: active_admin_root + end + end + end + + def destroy + destroy! do |success, failure| + success.html do + if params[:return_to] == "back" + redirect_back fallback_location: active_admin_root + else + redirect_to action: :index + end + end + failure.html do + redirect_back fallback_location: active_admin_root + end + end + end + end + + permit_params :body, :namespace, :resource_id, :resource_type + + index do + column I18n.t("active_admin.comments.resource_type"), :resource_type + column I18n.t("active_admin.comments.resource"), :resource, class: "min-w-28" + column I18n.t("active_admin.comments.author_type"), :author_type + column I18n.t("active_admin.comments.author"), :author + column I18n.t("active_admin.comments.body"), :body, class: "min-w-52" do |comment| + truncate(comment.body, length: 50, separator: " ") + end + column I18n.t("active_admin.comments.created_at"), :created_at, class: "min-w-52" + actions + end + end + end +end diff --git a/lib/active_admin/orm/active_record/comments/comment.rb b/lib/active_admin/orm/active_record/comments/comment.rb new file mode 100644 index 00000000000..e1b2643eb2a --- /dev/null +++ b/lib/active_admin/orm/active_record/comments/comment.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true +module ActiveAdmin + class Comment < ActiveRecord::Base + + self.table_name = "#{table_name_prefix}active_admin_comments#{table_name_suffix}" + + belongs_to :resource, polymorphic: true, optional: true + belongs_to :author, polymorphic: true + + validates_presence_of :body, :namespace, :resource + + before_create :set_resource_type + + # @return [String] The name of the record to use for the polymorphic relationship + def self.resource_type(resource) + ResourceController::Decorators.undecorate(resource).class.base_class.name.to_s + end + + def self.find_for_resource_in_namespace(resource, namespace) + where( + resource_type: resource_type(resource), + resource_id: resource.id, + namespace: namespace.to_s + ).order(ActiveAdmin.application.namespaces[namespace.to_sym].comments_order) + end + + def set_resource_type + self.resource_type = self.class.resource_type(resource) + end + + def self.ransackable_attributes(auth_object = nil) + authorizable_ransackable_attributes + end + + def self.ransackable_associations(auth_object = nil) + authorizable_ransackable_associations + end + + end +end diff --git a/lib/active_admin/comments/namespace_helper.rb b/lib/active_admin/orm/active_record/comments/namespace_helper.rb similarity index 51% rename from lib/active_admin/comments/namespace_helper.rb rename to lib/active_admin/orm/active_record/comments/namespace_helper.rb index 0639ff80563..14157f49c80 100644 --- a/lib/active_admin/comments/namespace_helper.rb +++ b/lib/active_admin/orm/active_record/comments/namespace_helper.rb @@ -1,11 +1,12 @@ +# frozen_string_literal: true module ActiveAdmin module Comments module NamespaceHelper - # Returns true of the namespace allows comments + # Returns true if the namespace allows comments def comments? - allow_comments == true + comments == true end end diff --git a/lib/active_admin/comments/resource_helper.rb b/lib/active_admin/orm/active_record/comments/resource_helper.rb similarity index 67% rename from lib/active_admin/comments/resource_helper.rb rename to lib/active_admin/orm/active_record/comments/resource_helper.rb index 14922a9f22c..b7e6f746731 100644 --- a/lib/active_admin/comments/resource_helper.rb +++ b/lib/active_admin/orm/active_record/comments/resource_helper.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin module Comments @@ -9,7 +10,7 @@ module ResourceHelper end def comments? - namespace.comments? && comments != false + (namespace.comments? && comments != false) || comments == true end end diff --git a/lib/active_admin/orm/active_record/comments/views.rb b/lib/active_admin/orm/active_record/comments/views.rb new file mode 100644 index 00000000000..f844704792b --- /dev/null +++ b/lib/active_admin/orm/active_record/comments/views.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true +require_relative "../../../views" +require_relative "views/active_admin_comments" diff --git a/lib/active_admin/orm/active_record/comments/views/active_admin_comments.rb b/lib/active_admin/orm/active_record/comments/views/active_admin_comments.rb new file mode 100644 index 00000000000..fdaa962f55a --- /dev/null +++ b/lib/active_admin/orm/active_record/comments/views/active_admin_comments.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true +require_relative "../../../../views" + +module ActiveAdmin + module Comments + module Views + class Comments < Arbre::Element + builder_method :active_admin_comments_for + + def build(resource) + if authorized?(ActiveAdmin::Auth::READ, ActiveAdmin::Comment) + comments = active_admin_authorization.scope_collection(ActiveAdmin::Comment.find_for_resource_in_namespace(resource, active_admin_namespace.name).includes(:author).page(params[:page])) + render("active_admin/shared/resource_comments", resource: resource, comments: comments, comment_form_url: comment_form_url) + end + end + + protected + + def comment_form_url + parts = [] + parts << active_admin_namespace.name unless active_admin_namespace.root? + parts << active_admin_namespace.comments_registration_name.underscore.pluralize + parts << "path" + send parts.join "_" + end + end + end + end +end diff --git a/lib/active_admin/page.rb b/lib/active_admin/page.rb index 0333fbdb5c1..3912dc4e15a 100644 --- a/lib/active_admin/page.rb +++ b/lib/active_admin/page.rb @@ -1,7 +1,8 @@ +# frozen_string_literal: true module ActiveAdmin # Page is the primary data storage for page configuration in Active Admin # - # When you register a page (ActiveAdmin.page "Status") you are actually creating + # When you register a page (ActiveAdmin.register_page "Status") you are actually creating # a new Page instance within the given Namespace. # # The instance of the current page is available in PageController and views @@ -49,19 +50,23 @@ def resource_name end def underscored_resource_name - resource_name.parameterize.underscore + resource_name.to_s.parameterize.underscore end def camelized_resource_name underscored_resource_name.camelize end + def namespace_name + namespace.name.to_s + end + def default_menu_options - super.merge(:id => resource_name) + super.merge(id: resource_name) end def controller_name - [namespace.module_name, camelized_resource_name + "Controller"].compact.join('::') + [namespace.module_name, camelized_resource_name + "Controller"].compact.join("::") end # Override from `ActiveAdmin::Resource::Controllers` @@ -69,10 +74,6 @@ def route_uncountable? false end - def belongs_to? - false - end - def add_default_action_items end @@ -84,5 +85,28 @@ def clear_page_actions! @page_actions = [] end + def belongs_to(target, options = {}) + @belongs_to = Resource::BelongsTo.new(self, target, options) + self.navigation_menu_name = target unless @belongs_to.optional? + controller.send :belongs_to, target, options.dup + end + + def belongs_to_config + @belongs_to + end + + # Do we belong to another resource? + def belongs_to? + !!belongs_to_config + end + + def breadcrumb + instance_variable_defined?(:@breadcrumb) ? @breadcrumb : namespace.breadcrumb + end + + def order_clause + @order_clause || namespace.order_clause + end + end end diff --git a/lib/active_admin/page_dsl.rb b/lib/active_admin/page_dsl.rb index e1ab7f872b9..94607aa81b4 100644 --- a/lib/active_admin/page_dsl.rb +++ b/lib/active_admin/page_dsl.rb @@ -1,5 +1,6 @@ +# frozen_string_literal: true module ActiveAdmin - # This is the class where all the register_page blocks are instance eval'd + # This is the class where all the register_page blocks are evaluated. class PageDSL < DSL # Page content. @@ -21,8 +22,12 @@ def content(options = {}, &block) def page_action(name, options = {}, &block) config.page_actions << ControllerAction.new(name, options) controller do - define_method(name, &block || Proc.new{}) + define_method(name, &block || Proc.new {}) end end + + def belongs_to(target, options = {}) + config.belongs_to(target, options) + end end end diff --git a/lib/active_admin/page_presenter.rb b/lib/active_admin/page_presenter.rb index c1d61b4f7fe..f48441a55f3 100644 --- a/lib/active_admin/page_presenter.rb +++ b/lib/active_admin/page_presenter.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin # A simple object that gets used to present different aspects of views @@ -7,7 +8,7 @@ module ActiveAdmin # # Usage: # - # presenter = PagePresenter.new :as => :table do + # presenter = PagePresenter.new as: :table do # # some awesome stuff # end # @@ -18,10 +19,11 @@ class PagePresenter attr_reader :block, :options - delegate :has_key?, :to => :options + delegate :has_key?, :fetch, to: :options def initialize(options = {}, &block) - @options, @block = options, block + @options = options + @block = block end def [](key) diff --git a/lib/active_admin/pundit_adapter.rb b/lib/active_admin/pundit_adapter.rb new file mode 100644 index 00000000000..c81abe11ce4 --- /dev/null +++ b/lib/active_admin/pundit_adapter.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true +ActiveAdmin::Dependency.pundit! + +require "pundit" + +# Add a setting to the application to configure the pundit default policy +ActiveAdmin::Application.inheritable_setting :pundit_default_policy, nil +ActiveAdmin::Application.inheritable_setting :pundit_policy_namespace, nil + +module ActiveAdmin + + class PunditAdapter < AuthorizationAdapter + + def authorized?(action, subject = nil) + policy = retrieve_policy(subject) + action = format_action(action, subject) + + policy.respond_to?(action) && policy.public_send(action) + end + + def scope_collection(collection, action = Auth::READ) + # scoping is applicable only to read/index action + # which means there is no way how to scope other actions + Pundit.policy_scope!(user, namespace(collection)) + rescue Pundit::NotDefinedError => e + if default_policy_class&.const_defined?(:Scope) + default_policy_class::Scope.new(user, collection).resolve + else + raise e + end + end + + def retrieve_policy(subject) + target = policy_target(subject) + if (policy = policy(namespace(target)) || compat_policy(subject)) + policy + elsif default_policy_class + default_policy(subject) + else + raise Pundit::NotDefinedError, "unable to find a compatible policy for `#{target}`" + end + end + + def format_action(action, subject) + # https://github.com/varvet/pundit/blob/main/lib/generators/pundit/install/templates/application_policy.rb + case action + when Auth::READ then subject.is_a?(Class) ? :index? : :show? + when Auth::DESTROY then subject.is_a?(Class) ? :destroy_all? : :destroy? + else "#{action}?" + end + end + + private + + def policy_target(subject) + case subject + when nil then resource + when Class then subject.new + else subject + end + end + + # This method is needed to fallback to our previous policy searching logic. + # I.e.: when class name contains `default_policy_namespace` (eg: ShopAdmin) + # we should try to search it without namespace. This is because that's + # the only thing that worked in this case before we fixed our buggy namespace + # detection, so people are probably relying on it. + # This fallback might be removed in future versions of ActiveAdmin, so + # pundit_adapter search will work consistently with provided namespaces + def compat_policy(subject) + return unless default_policy_namespace + + target = policy_target(subject) + + return unless target.class.to_s.include?(default_policy_module) && + (policy = policy(target)) + + policy_name = policy.class.to_s + + ActiveAdmin.deprecator.warn "You have `pundit_policy_namespace` configured as `#{default_policy_namespace}`, " \ + "but ActiveAdmin was unable to find policy #{default_policy_module}::#{policy_name}. " \ + "#{policy_name} will be used instead. " \ + "This behavior will be removed in future versions of ActiveAdmin. " \ + "To fix this warning, move your #{policy_name} policy to the #{default_policy_module} namespace" + + policy + end + + def namespace(object) + if default_policy_namespace && !object.class.to_s.start_with?("#{default_policy_module}::") + [default_policy_namespace.to_sym, object] + else + object + end + end + + def default_policy_class + ActiveAdmin.application.pundit_default_policy&.constantize + end + + def default_policy(subject) + default_policy_class.new(user, subject) + end + + def default_policy_namespace + ActiveAdmin.application.pundit_policy_namespace + end + + def default_policy_module + default_policy_namespace.to_s.camelize + end + + def policy(target) + policies[target] ||= Pundit.policy(user, target) + end + + def policies + @policies ||= {} + end + + end + +end diff --git a/lib/active_admin/reloader.rb b/lib/active_admin/reloader.rb deleted file mode 100644 index 0830357a050..00000000000 --- a/lib/active_admin/reloader.rb +++ /dev/null @@ -1,130 +0,0 @@ -module ActiveAdmin - - module Reloader - - # Builds the proper Reloader implementation class given the - # current version of Rails. - # - # @param [Rails::Application] rails_app The current rails application - # @param [ActiveAdmin::Application] active_admin_app The current Active Admin app - # @param [String] rails_version The version of Rails we're using. - # - # @returns A concrete subclass of AbstractReloader - def self.build(rails_app, active_admin_app, rails_version) - reloader_class = rails_version.to_f >= 3.2 ? Rails32Reloader : RailsLessThan31Reloader - reloader_class.new(rails_app, active_admin_app, rails_version) - end - - class AbstractReloader - - attr_reader :active_admin_app, :rails_app, :rails_version - - def initialize(rails_app, active_admin_app, rails_version) - @rails_app = rails_app - @active_admin_app = active_admin_app - @rails_version = rails_version.to_s - end - - def attach! - raise "Please implement #{self.class}#attach!" - end - - def reload! - active_admin_app.unload! - rails_app.reload_routes! - end - - def major_rails_version - @rails_version[0..2] - end - - end - - # Reloads the application when using Rails 3.2 - # - # 3.2 introduced a to_prepare block that only gets called when - # files have actually changed. ActiveAdmin had built this functionality - # in to speed up applications. So in Rails >= 3.2, we can now piggy - # back off the existing reloader. This simplifies our duties... which is - # nice. - class Rails32Reloader < AbstractReloader - - # Attach to Rails and perform the reload on each request. - def attach! - active_admin_app.load_paths.each do |path| - rails_app.config.watchable_dirs[path] = [:rb] - end - - reloader = self - - ActionDispatch::Reloader.to_prepare do - reloader.reload! - end - end - - end - - # Deals with reloading Active Admin on each request in - # development and once in production in Rails < 3.2. - class RailsLessThan31Reloader < AbstractReloader - - class FileUpdateChecker < ::ActiveSupport::FileUpdateChecker - def paths - # hack to support both Rails 3.1 and 3.2 - @files || @paths - end - - # Over-ride the default #updated_at to support the deletion of files - def updated_at - paths.map { |path| File.mtime(path) rescue Time.now }.max - end - - def execute_if_updated - super - end - end - - attr_reader :file_update_checker - - def initialize(rails_app, active_admin_app, rails_version) - super - @file_update_checker = FileUpdateChecker.new(watched_paths) do - reload! - end - end - - def reload! - super - file_update_checker.paths.clear - watched_paths.each{|path| file_update_checker.paths << path } - end - - - # Attach to Rails and perform the reload on each request. - def attach! - # Bring the checker into local scope for the ruby block - checker = file_update_checker - - reloader_class.to_prepare do - checker.execute_if_updated - end - end - - def watched_paths - paths = active_admin_app.load_paths - active_admin_app.load_paths.each{|path| paths += Dir[File.join(path, "**", "*.rb")]} - paths - end - - def reloader_class - if major_rails_version == "3.1" - ActionDispatch::Reloader - else - ActionDispatch::Callbacks - end - end - - end - - end -end diff --git a/lib/active_admin/resource.rb b/lib/active_admin/resource.rb index 28ad5b22a39..96e9e31c075 100644 --- a/lib/active_admin/resource.rb +++ b/lib/active_admin/resource.rb @@ -1,14 +1,20 @@ -require 'active_admin/resource/action_items' -require 'active_admin/resource/controllers' -require 'active_admin/resource/menu' -require 'active_admin/resource/page_presenters' -require 'active_admin/resource/pagination' -require 'active_admin/resource/routes' -require 'active_admin/resource/naming' -require 'active_admin/resource/scopes' -require 'active_admin/resource/scope_to' -require 'active_admin/resource/sidebars' -require 'active_admin/resource/belongs_to' +# frozen_string_literal: true +require_relative "view_helpers/method_or_proc_helper" +require_relative "resource/action_items" +require_relative "resource/attributes" +require_relative "resource/controllers" +require_relative "resource/menu" +require_relative "resource/page_presenters" +require_relative "resource/pagination" +require_relative "resource/routes" +require_relative "resource/naming" +require_relative "resource/scopes" +require_relative "resource/includes" +require_relative "resource/scope_to" +require_relative "resource/sidebars" +require_relative "resource/belongs_to" +require_relative "resource/ordering" +require_relative "resource/model" module ActiveAdmin @@ -23,7 +29,7 @@ module ActiveAdmin class Resource # Event dispatched when a new resource is registered - RegisterEvent = 'active_admin.resource.register'.freeze + RegisterEvent = "active_admin.resource.register".freeze # The namespace this config belongs to attr_reader :namespace @@ -38,13 +44,22 @@ class Resource attr_reader :collection_actions # The default sort order to use in the controller - attr_accessor :sort_order + attr_writer :sort_order + def sort_order + @sort_order ||= (resource_class.respond_to?(:primary_key) ? resource_class.primary_key.to_s : "id") + "_desc" + end # Set the configuration for the CSV attr_writer :csv_builder # Set breadcrumb builder - attr_accessor :breadcrumb + attr_writer :breadcrumb + + #Set order clause + attr_writer :order_clause + # Display create another checkbox on a new page + # @return [Boolean] + attr_writer :create_another # Store a reference to the DSL so that we can dereference it during garbage collection. attr_accessor :dsl @@ -53,13 +68,19 @@ class Resource # nil to not decorate. attr_accessor :decorator_class_name + # Default `filters` list for string filters on this resource. Takes + # priority over the namespace-level `string_input_filters` setting when + # set; when nil, falls back to it, and then to the global default. + attr_accessor :string_input_filters + module Base def initialize(namespace, resource_class, options = {}) @namespace = namespace - @resource_class_name = "::#{resource_class.name}" - @options = default_options.merge(options) - @sort_order = @options[:sort_order] - @member_actions, @collection_actions = [], [] + @resource_class_name = resource_class.respond_to?(:name) ? "::#{resource_class.name}" : resource_class.to_s + @options = options + @sort_order = options[:sort_order] + @member_actions = [] + @collection_actions = [] end end @@ -74,19 +95,25 @@ def initialize(namespace, resource_class, options = {}) include PagePresenters include Pagination include Scopes + include Includes include ScopeTo include Sidebars - include Menu include Routes + include Ordering + include Attributes # The class this resource wraps. If you register the Post model, Resource#resource_class # will point to the Post class def resource_class - ActiveSupport::Dependencies.constantize(resource_class_name) + resource_class_name.constantize end def decorator_class - ActiveSupport::Dependencies.constantize(decorator_class_name) if decorator_class_name + decorator_class_name&.constantize + end + + def resource_name_extension + @resource_name_extension ||= define_resource_name_extension(self) end def resource_table_name @@ -98,7 +125,9 @@ def resource_column_names end def resource_quoted_column_name(column) - resource_class.connection.quote_column_name(column) + resource_class.with_connection do |connection| + connection.quote_column_name(column) + end end # Clears all the member actions this resource knows about @@ -117,7 +146,8 @@ def defined_actions def belongs_to(target, options = {}) @belongs_to = Resource::BelongsTo.new(self, target, options) - self.navigation_menu_name = target unless @belongs_to.optional? + self.menu_item_options = false if @belongs_to.required? + options[:class_name] ||= @belongs_to.resource.resource_class_name if @belongs_to.resource controller.send :belongs_to, target, options.dup end @@ -125,6 +155,12 @@ def belongs_to_config @belongs_to end + def belongs_to_param + if belongs_to? && belongs_to_config.required? + belongs_to_config.to_param + end + end + # Do we belong to another resource? def belongs_to? !!belongs_to_config @@ -135,24 +171,59 @@ def csv_builder @csv_builder || default_csv_builder end - # @deprecated - def resource - resource_class + def breadcrumb + instance_variable_defined?(:@breadcrumb) ? @breadcrumb : namespace.breadcrumb + end + + def order_clause + @order_clause || namespace.order_clause + end + + def create_another + instance_variable_defined?(:@create_another) ? @create_another : namespace.create_another + end + + def find_resource(id) + resource = resource_class.public_send(*method_for_find(id)) + (decorator_class && resource) ? decorator_class.new(resource) : resource + end + + def resource_columns + resource_attributes.values + end + + def resource_attributes + @resource_attributes ||= default_attributes + end + + def association_columns + @association_columns ||= resource_attributes.select { |key, value| key != value }.values + end + + def content_columns + @content_columns ||= resource_attributes.select { |key, value| key == value }.values end - ActiveAdmin::Deprecation.deprecate self, :resource, - "ActiveAdmin::Resource#resource is deprecated. Please use #resource_class instead." private - def default_options - { - :sort_order => (resource_class.respond_to?(:primary_key) ? resource_class.primary_key.to_s : 'id') + '_desc' - } + def method_for_find(id) + if finder = resources_configuration[:self][:finder] + [finder, id] + else + [:find_by, { resource_class.primary_key => id }] + end end def default_csv_builder - @default_csv_builder ||= CSVBuilder.default_for_resource(resource_class) + @default_csv_builder ||= CSVBuilder.default_for_resource(self) end + def define_resource_name_extension(resource) + Module.new do + define_method :model_name do + resource.resource_name + end + end + end end # class Resource end # module ActiveAdmin diff --git a/lib/active_admin/resource/action_items.rb b/lib/active_admin/resource/action_items.rb index 1e08a886ed6..0dc825392e0 100644 --- a/lib/active_admin/resource/action_items.rb +++ b/lib/active_admin/resource/action_items.rb @@ -1,12 +1,12 @@ -require 'active_admin/helpers/optional_display' +# frozen_string_literal: true +require_relative "../helpers/optional_display" module ActiveAdmin class Resource module ActionItems - # Add the default action items to a resource when it's - # initialized + # Adds the default action items to a resource when it's initialized def initialize(*args) super add_default_action_items @@ -19,13 +19,19 @@ def action_items # Add a new action item to a resource # + # @param [Symbol] name # @param [Hash] options valid keys include: # :only: A single or array of controller actions to display # this action item on. # :except: A single or array of controller actions not to # display this action item on. - def add_action_item(options = {}, &block) - self.action_items << ActiveAdmin::ActionItem.new(options, &block) + # :priority: A single integer value. To control the display order. Default is 10. + def add_action_item(name, options = {}, &block) + self.action_items << ActiveAdmin::ActionItem.new(name, options, &block) + end + + def remove_action_item(name) + self.action_items.delete_if { |item| item.name == name } end # Returns a set of action items to display for a specific controller action @@ -34,7 +40,7 @@ def add_action_item(options = {}, &block) # # @return [Array] Array of ActionItems for the controller actions def action_items_for(action, render_context = nil) - action_items.select{|item| item.display_on?(action, render_context) } + action_items.select { |item| item.display_on? action, render_context }.sort_by(&:priority) end # Clears all the existing action items for this resource @@ -51,27 +57,38 @@ def action_items? # Adds the default action items to each resource def add_default_action_items - # New Link on all actions except :new and :show - add_action_item :except => [:new, :show] do - if controller.action_methods.include?('new') && authorized?(ActiveAdmin::Auth::CREATE, active_admin_config.resource_class) - link_to(I18n.t('active_admin.new_model', :model => active_admin_config.resource_label), new_resource_path) - end + add_default_new_action_item + add_default_edit_action_item + add_default_destroy_action_item + end + + # Adds the default New link on index + def add_default_new_action_item + add_action_item :new, only: :index, if: -> { new_action_authorized?(active_admin_config.resource_class) } do + localizer = ActiveAdmin::Localizers.resource(active_admin_config) + link_to localizer.t(:new_model), new_resource_path, class: "action-item-button" end + end - # Edit link on show - add_action_item :only => :show do - if controller.action_methods.include?('edit') && authorized?(ActiveAdmin::Auth::UPDATE, resource) - link_to(I18n.t('active_admin.edit_model', :model => active_admin_config.resource_label), edit_resource_path(resource)) - end + # Adds the default Edit link on show + def add_default_edit_action_item + add_action_item :edit, only: :show, if: -> { edit_action_authorized?(resource) } do + localizer = ActiveAdmin::Localizers.resource(active_admin_config) + link_to localizer.t(:edit_model), edit_resource_path(resource), class: "action-item-button" end + end - # Destroy link on show - add_action_item :only => :show do - if controller.action_methods.include?("destroy") && authorized?(ActiveAdmin::Auth::DESTROY, resource) - link_to(I18n.t('active_admin.delete_model', :model => active_admin_config.resource_label), - resource_path(resource), - :method => :delete, :data => {:confirm => I18n.t('active_admin.delete_confirmation')}) - end + # Adds the default Destroy link on show + def add_default_destroy_action_item + add_action_item :destroy, only: :show, if: -> { destroy_action_authorized?(resource) } do + localizer = ActiveAdmin::Localizers.resource(active_admin_config) + link_to( + localizer.t(:delete_model), + resource_path(resource), + class: "action-item-button", + method: :delete, + data: { confirm: localizer.t(:delete_confirmation) } + ) end end @@ -82,12 +99,18 @@ def add_default_action_items class ActionItem include ActiveAdmin::OptionalDisplay - attr_accessor :block + attr_accessor :block, :name - def initialize(options = {}, &block) - @options, @block = options, block + def initialize(name, options = {}, &block) + @name = name + @options = options + @block = block normalize_display_options! end + + def priority + @options[:priority] || 10 + end end end diff --git a/lib/active_admin/resource/attributes.rb b/lib/active_admin/resource/attributes.rb new file mode 100644 index 00000000000..10986f28478 --- /dev/null +++ b/lib/active_admin/resource/attributes.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true +module ActiveAdmin + + class Resource + module Attributes + + def default_attributes + return {} unless resource_class.respond_to?(:columns) + + resource_class.columns.each_with_object({}) do |c, attrs| + unless reject_col?(c) + name = c.name.to_sym + attrs[name] = (method_for_column(name) || name) + end + end + end + + def method_for_column(c) + resource_class.respond_to?(:reflect_on_all_associations) && foreign_methods.has_key?(c) && foreign_methods[c].name.to_sym + end + + def foreign_methods + @foreign_methods ||= resource_class.reflect_on_all_associations. + select { |r| r.macro == :belongs_to }. + reject { |r| r.chain.length > 2 && !r.options[:polymorphic] }. + index_by { |r| r.foreign_key.to_sym } + end + + def reject_col?(c) + primary_col?(c) || sti_col?(c) || counter_cache_col?(c) || filtered_col?(c) + end + + def primary_col?(c) + c.name == resource_class.primary_key + end + + def sti_col?(c) + c.name == resource_class.inheritance_column + end + + def counter_cache_col?(c) + # This helper is called inside a loop. Let's memoize the result. + @counter_cache_columns ||= begin + resource_class.reflect_on_all_associations(:has_many) + .select(&:has_cached_counter?) + .map(&:counter_cache_column) + end + + @counter_cache_columns.include?(c.name) + end + + def filtered_col?(c) + ActiveAdmin.application.filter_attributes.include?(c.name.to_sym) + end + end + end +end diff --git a/lib/active_admin/resource/belongs_to.rb b/lib/active_admin/resource/belongs_to.rb index eb1cf49fa19..b65503f6ba9 100644 --- a/lib/active_admin/resource/belongs_to.rb +++ b/lib/active_admin/resource/belongs_to.rb @@ -1,23 +1,36 @@ -require 'active_admin/resource' +# frozen_string_literal: true module ActiveAdmin class Resource class BelongsTo - class TargetNotFound < StandardError; end + class TargetNotFound < StandardError + def initialize(key, namespace) + super "Could not find #{key} in #{namespace.name} " + + "with #{namespace.resources.map(&:resource_name)}" + end + end # The resource which initiated this relationship attr_reader :owner + # The name of the relation + attr_reader :target_name + def initialize(owner, target_name, options = {}) - @owner, @target_name, @options = owner, target_name, options + @owner = owner + @target_name = target_name + @options = options end # Returns the target resource class or raises an exception if it doesn't exist def target - key = @target_name.to_s.camelize - namespace.resources[key] or raise TargetNotFound, "Could not find #{key} in" + - " #{namespace.name} with #{namespace.resources.map(&:resource_name)}" + resource or raise TargetNotFound.new (@options[:class_name] || @target_name.to_s.camelize), namespace + end + + def resource + namespace.resources[@options[:class_name]] || + namespace.resources[@target_name.to_s.camelize] end def namespace @@ -31,6 +44,10 @@ def optional? def required? !optional? end + + def to_param + (@options[:param] || "#{@target_name}_id").to_sym + end end end end diff --git a/lib/active_admin/resource/controllers.rb b/lib/active_admin/resource/controllers.rb index da17260a4d5..c63ddd09115 100644 --- a/lib/active_admin/resource/controllers.rb +++ b/lib/active_admin/resource/controllers.rb @@ -1,11 +1,13 @@ +# frozen_string_literal: true module ActiveAdmin class Resource module Controllers + delegate :resources_configuration, to: :controller # Returns a properly formatted controller name for this # config within its namespace def controller_name - [namespace.module_name, resource_name.plural.camelize + "Controller"].compact.join('::') + [namespace.module_name, resource_name.plural.camelize + "Controller"].compact.join("::") end # Returns the controller for this config diff --git a/lib/active_admin/resource/includes.rb b/lib/active_admin/resource/includes.rb new file mode 100644 index 00000000000..0858b491b6c --- /dev/null +++ b/lib/active_admin/resource/includes.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true +module ActiveAdmin + class Resource + module Includes + + # Return an array of includes for this resource + def includes + @includes ||= [] + end + + end + end +end diff --git a/lib/active_admin/resource/menu.rb b/lib/active_admin/resource/menu.rb index 9bfa3a1f010..9970ff60328 100644 --- a/lib/active_admin/resource/menu.rb +++ b/lib/active_admin/resource/menu.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin class Resource @@ -7,11 +8,12 @@ module Menu # To disable this menu item, call `menu(false)` from the DSL def menu_item_options=(options) if options == false - @include_in_menu = false + @include_in_menu = false @menu_item_options = {} else + @include_in_menu = true @navigation_menu_name = options[:menu_name] - @menu_item_options = default_menu_options.merge options + @menu_item_options = default_menu_options.merge options end end @@ -24,19 +26,21 @@ def default_menu_options menu_resource_class = respond_to?(:resource_class) ? resource_class : self resource = self { - :id => resource_name.plural, - :label => proc{ resource.plural_resource_label }, - :url => proc{ resource.route_collection_path(params) }, - :if => proc{ authorized?(:read, menu_resource_class) } + id: resource_name.plural, + label: proc { resource.plural_resource_label }, + url: proc { resource.route_collection_path(params, url_options) }, + if: proc { authorized?(Auth::READ, menu_resource_class) } } end - attr_writer :navigation_menu_name + def navigation_menu_name=(menu_name) + self.menu_item_options = { menu_name: menu_name } + end def navigation_menu_name case @navigation_menu_name ||= DEFAULT_MENU when Proc - controller.instance_eval(&@navigation_menu_name).to_sym + controller.instance_exec(&@navigation_menu_name).to_sym else @navigation_menu_name end diff --git a/lib/active_admin/resource/model.rb b/lib/active_admin/resource/model.rb new file mode 100644 index 00000000000..aeb894c3cf5 --- /dev/null +++ b/lib/active_admin/resource/model.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +module ActiveAdmin + class Model + def initialize(resource, record) + @record = record + + if resource + @record.extend(resource.resource_name_extension) + end + end + + def to_model + @record + end + end +end diff --git a/lib/active_admin/resource/naming.rb b/lib/active_admin/resource/naming.rb index 92047cf7338..5c0c8813350 100644 --- a/lib/active_admin/resource/naming.rb +++ b/lib/active_admin/resource/naming.rb @@ -1,77 +1,61 @@ +# frozen_string_literal: true module ActiveAdmin class Resource module Naming - - # Returns a name used to uniquely identify this resource - # this should be an instance of ActiveAdmin:Resource::Name, which responds to - # #singular, #plural, #route_key, #human etc. def resource_name - custom_name = @options[:as] && @options[:as].gsub(/\s/,'') - @resource_name ||= if custom_name || !resource_class.respond_to?(:model_name) - Resource::Name.new(resource_class, custom_name) + @resource_name ||= begin + as = @options[:as].gsub(/\s/, "") if @options[:as] + + if as || !resource_class.respond_to?(:model_name) + Name.new resource_class, as else - Resource::Name.new(resource_class) + Name.new resource_class end + end end # Returns the name to call this resource such as "Bank Account" def resource_label - resource_name.translate :count => 1, :default => resource_name.gsub('::', ' ').titleize + resource_name.translate count: 1, + default: resource_name.to_s.gsub("::", " ").titleize end # Returns the plural version of this resource such as "Bank Accounts" def plural_resource_label(options = {}) - resource_name.translate ({:count => 1.1, :default => resource_label.pluralize.titleize}).merge(options) + defaults = { count: 2.1, default: resource_label.pluralize.titleize } + resource_name.translate defaults.merge options + end + + # Forms use the model's original `param_key`, so we can't use our + # custom `resource_name` when the model's been renamed in ActiveAdmin. + def param_key + if resource_class.respond_to? :model_name + resource_class.model_name.param_key + else + resource_name.param_key + end end end - # A subclass of ActiveModel::Name which supports the different APIs presented - # in Rails < 3.1 and > 3.1. class Name < ActiveModel::Name + delegate :hash, to: :to_str def initialize(klass, name = nil) - if ActiveModel::Name.instance_method(:initialize).arity == 1 - super(proxy_for_initializer(klass, name)) - else - super(klass, nil, name) - end + super(klass, nil, name) end def translate(options = {}) - I18n.t i18n_key, {:scope => [:activerecord, :models]}.merge(options) - end - - def proxy_for_initializer(klass, name) - return klass unless name - return StringClassProxy.new(klass, name) if klass - - StringProxy.new(name) + I18n.t i18n_key, **{ scope: [:activerecord, :models] }.merge(options) end def route_key plural end - class StringProxy - def initialize(name) - @name = name - end - - def name - @name - end - end - - class StringClassProxy < StringProxy - delegate :lookup_ancestors, :i18n_scope, :to => :"@klass" - - def initialize(klass, name) - @klass = klass || name - super(name) - end + def eql?(other) + to_str.eql?(other.to_str) end - end end diff --git a/lib/active_admin/resource/ordering.rb b/lib/active_admin/resource/ordering.rb new file mode 100644 index 00000000000..5dd7383571f --- /dev/null +++ b/lib/active_admin/resource/ordering.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +module ActiveAdmin + class Resource + module Ordering + + def ordering + @ordering ||= {}.with_indifferent_access + end + + end + end +end diff --git a/lib/active_admin/resource/page_presenters.rb b/lib/active_admin/resource/page_presenters.rb index 18ed9a3ed37..1ea736504d8 100644 --- a/lib/active_admin/resource/page_presenters.rb +++ b/lib/active_admin/resource/page_presenters.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin class Resource module PagePresenters @@ -32,8 +33,8 @@ def set_page_presenter(action, page_presenter) # # @param [Symbol, String] action The action to get the config for # @param [String] type The string specified in the presenters index_name method - # @returns [PagePresenter, nil] - def get_page_presenter(action, type=nil) + # @return [PagePresenter, nil] + def get_page_presenter(action, type = nil) if action.to_s == "index" && type && page_presenters[:index].kind_of?(Hash) page_presenters[:index][type.to_sym] @@ -50,7 +51,7 @@ def get_page_presenter(action, type=nil) # Stores a config for all index actions supplied # # @param [Symbol] index_as The index type to store in the configuration - # @param [PagePresenter] page_presenter The intance of PagePresenter to store + # @param [PagePresenter] page_presenter The instance of PagePresenter to store def set_index_presenter(index_as, page_presenter) page_presenters[:index] ||= {} @@ -63,7 +64,7 @@ def set_index_presenter(index_as, page_presenter) page_presenters[:index][index_as] = page_presenter end - # Returns the actual class for renderering the main content on the index + # Returns the actual class for rendering the main content on the index # page. To set this, use the :as option in the page_presenter block. # # @param [Symbol, Class] symbol_or_class The component symbol or class diff --git a/lib/active_admin/resource/pagination.rb b/lib/active_admin/resource/pagination.rb index 7033627ccec..f619fcc0888 100644 --- a/lib/active_admin/resource/pagination.rb +++ b/lib/active_admin/resource/pagination.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin class Resource @@ -6,6 +7,9 @@ module Pagination # The default number of records to display per page attr_accessor :per_page + # The default number of records to display per page + attr_accessor :max_per_page + # Enable / disable pagination (defaults to true) attr_accessor :paginate @@ -13,6 +17,7 @@ def initialize(*args) super @paginate = true @per_page = namespace.default_per_page + @max_per_page = namespace.max_per_page end end end diff --git a/lib/active_admin/resource/routes.rb b/lib/active_admin/resource/routes.rb index c36229f4c22..7caea753a64 100644 --- a/lib/active_admin/resource/routes.rb +++ b/lib/active_admin/resource/routes.rb @@ -1,76 +1,115 @@ +# frozen_string_literal: true module ActiveAdmin class Resource module Routes - # @params params [Hash] of params: {study_id: 3} + # @param params [Hash] of params: { study_id: 3 } # @return [String] the path to this resource collection page # @example "/admin/posts" - def route_collection_path(params = {}) - RouteBuilder.new(self).collection_path(params) + def route_collection_path(params = {}, additional_params = {}) + route_builder.collection_path(params, additional_params) end - + + def route_batch_action_path(params = {}, additional_params = {}) + route_builder.batch_action_path(params, additional_params) + end + # @param resource [ActiveRecord::Base] the instance we want the path of # @return [String] the path to this resource collection page # @example "/admin/posts/1" - def route_instance_path(resource) - RouteBuilder.new(self).instance_path(resource) + def route_instance_path(resource, additional_params = {}) + route_builder.instance_path(resource, additional_params) + end + + def route_edit_instance_path(resource, additional_params = {}) + route_builder.member_action_path(:edit, resource, additional_params) + end + + def route_member_action_path(action, resource, additional_params = {}) + route_builder.member_action_path(action, resource, additional_params) end # Returns the routes prefix for this config def route_prefix - namespace.module_name.try(:underscore) + namespace.route_prefix + end + + def route_builder + @route_builder ||= RouteBuilder.new(self) end def route_uncountable? - config = controller.resources_configuration[:self] + config = resources_configuration[:self] config[:route_collection_name] == config[:route_instance_name] end - private - class RouteBuilder def initialize(resource) @resource = resource end - def collection_path(params) + def collection_path(params, additional_params = {}) + route_name = route_name( + resource.resources_configuration[:self][:route_collection_name], + suffix: (resource.route_uncountable? ? "index_path" : "path") + ) + + routes.public_send route_name, *route_collection_params(params), additional_params + end + + def batch_action_path(params, additional_params = {}) route_name = route_name( - resource.controller.resources_configuration[:self][:route_collection_name], - (resource.route_uncountable? ? 'index_path' : 'path') + resource.resources_configuration[:self][:route_collection_name], + action: :batch_action, + suffix: (resource.route_uncountable? ? "index_path" : "path") ) - routes.send(route_name, *route_collection_params(params)) + query = params.slice(:q, :scope) + query = query.permit!.to_h + routes.public_send route_name, *route_collection_params(params), additional_params.merge(query) end # @return [String] the path to this resource collection page # @param instance [ActiveRecord::Base] the instance we want the path of # @example "/admin/posts/1" - def instance_path(instance) - route_name = route_name(resource.controller.resources_configuration[:self][:route_instance_name]) + def instance_path(instance, additional_params = {}) + route_name = route_name(resource.resources_configuration[:self][:route_instance_name]) + + routes.public_send route_name, *route_instance_params(instance), additional_params + end + + # @return [String] the path to the member action of this resource + # @param action [Symbol] + # @param instance [ActiveRecord::Base] the instance we want the path of + # @example "/admin/posts/1/edit" + def member_action_path(action, instance, additional_params = {}) + path = resource.resources_configuration[:self][:route_instance_name] + route_name = route_name(path, action: action) - routes.send(route_name, *route_instance_params(instance)) + routes.public_send route_name, *route_instance_params(instance), additional_params end private attr_reader :resource - def route_name(resource_path_name, suffix = 'path') + def route_name(resource_path_name, options = {}) + suffix = options[:suffix] || "path" route = [] - route << resource.route_prefix # "admin" + route << options[:action] # "batch_action", "edit" or "new" + route << resource.route_prefix # "admin" route << belongs_to_name if nested? # "category" - route << resource_path_name # "posts" or "post" - route << suffix # "path" or "index path" + route << resource_path_name # "posts" or "post" + route << suffix # "path" or "index path" - route.compact.join('_').to_sym # :admin_category_posts_path + route.compact.join("_").to_sym # :admin_category_posts_path end - # @return params to pass to instance path def route_instance_params(instance) if nested? - [instance.send(belongs_to_name).to_param, instance.to_param] + [instance.public_send(belongs_to_target_name).to_param, instance.to_param] else instance.to_param end @@ -83,15 +122,23 @@ def route_collection_params(params) end def nested? - resource.belongs_to? && resource.belongs_to_config.required? + resource.belongs_to? && belongs_to_config.required? + end + + def belongs_to_target_name + belongs_to_config.target_name end def belongs_to_name - resource.belongs_to_config.target.resource_name.singular if nested? + belongs_to_config.target.resource_name.singular + end + + def belongs_to_config + resource.belongs_to_config end def routes - Rails.application.routes.url_helpers + Helpers::Routes end end end diff --git a/lib/active_admin/resource/scope_to.rb b/lib/active_admin/resource/scope_to.rb index 04998243c75..e1325f8d0a2 100644 --- a/lib/active_admin/resource/scope_to.rb +++ b/lib/active_admin/resource/scope_to.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin class Resource module ScopeTo @@ -20,7 +21,7 @@ module ScopeTo # method to call as the association. If its different, you can # pass in the association_method as an option. # - # scope_to :current_user, :association_method => :blog_posts + # scope_to :current_user, association_method: :blog_posts # # will result in the following # @@ -28,20 +29,20 @@ module ScopeTo # # To conditionally use this scope, you can use conditional procs # - # scope_to :current_user, :if => proc{ admin_user_signed_in? } + # scope_to :current_user, if: proc{ admin_user_signed_in? } # # or # - # scope_to :current_user, :unless => proc{ current_user.admin? } + # scope_to :current_user, unless: proc{ current_user.admin? } # def scope_to(*args, &block) options = args.extract_options! method = args.first - scope_to_config[:method] = block || method - scope_to_config[:association_method] = options[:association_method] - scope_to_config[:if] = options[:if] - scope_to_config[:unless] = options[:unless] + scope_to_config[:method] = block || method + scope_to_config[:association_method] = options[:association_method] + scope_to_config[:if] = options[:if] + scope_to_config[:unless] = options[:unless] end @@ -55,10 +56,10 @@ def scope_to_method def scope_to_config @scope_to_config ||= { - :method => nil, - :association_method => nil, - :if => nil, - :unless => nil + method: nil, + association_method: nil, + if: nil, + unless: nil } end diff --git a/lib/active_admin/resource/scopes.rb b/lib/active_admin/resource/scopes.rb index 5c834f64b3d..3ff15f90137 100644 --- a/lib/active_admin/resource/scopes.rb +++ b/lib/active_admin/resource/scopes.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin class Resource module Scopes @@ -10,7 +11,7 @@ def scopes # Returns a scope for this object by its identifier def get_scope_by_id(id) id = id.to_s - scopes.find{|s| s.id == id } + scopes.find { |s| s.id == id } end def default_scope(context = nil) @@ -27,14 +28,16 @@ def default_scope(context = nil) # If you want to internationalize the scope name, you can add # to your i18n files a key like "active_admin.scopes.scope_method". def scope(*args, &block) - options = args.extract_options! + default_options = { show_count: namespace.scopes_show_count } + options = default_options.merge(args.extract_options!) title = args[0] rescue nil method = args[1] rescue nil + options[:localizer] ||= ActiveAdmin::Localizers.resource(self) scope = ActiveAdmin::Scope.new(title, method, options, &block) # Finds and replaces a scope by the same name if it already exists - existing_scope_index = scopes.index{|existing_scope| existing_scope.id == scope.id } + existing_scope_index = scopes.index { |existing_scope| existing_scope.id == scope.id } if existing_scope_index scopes.delete_at(existing_scope_index) scopes.insert(existing_scope_index, scope) diff --git a/lib/active_admin/resource/sidebars.rb b/lib/active_admin/resource/sidebars.rb index 4a73357ad67..7d73d44456b 100644 --- a/lib/active_admin/resource/sidebars.rb +++ b/lib/active_admin/resource/sidebars.rb @@ -1,4 +1,5 @@ -require 'active_admin/helpers/optional_display' +# frozen_string_literal: true +require_relative "../helpers/optional_display" module ActiveAdmin @@ -14,7 +15,8 @@ def clear_sidebar_sections! end def sidebar_sections_for(action, render_context = nil) - sidebar_sections.select{|section| section.display_on?(action, render_context) } + sidebar_sections.select { |section| section.display_on?(action, render_context) } + .sort_by(&:priority) end def sidebar_sections? diff --git a/lib/active_admin/resource_collection.rb b/lib/active_admin/resource_collection.rb index 1eb37d8993d..2b7dc334a30 100644 --- a/lib/active_admin/resource_collection.rb +++ b/lib/active_admin/resource_collection.rb @@ -1,41 +1,72 @@ +# frozen_string_literal: true module ActiveAdmin # This is a container for resources, which acts much like a Hash. # It's assumed that an added resource responds to `resource_name`. class ResourceCollection include Enumerable extend Forwardable - def_delegators :@resources, :empty?, :has_key?, :keys, :values, :[]= + def_delegators :@collection, :empty?, :has_key?, :keys, :values, :size def initialize - @resources = {} + @collection = {} end def add(resource) - self[resource.resource_name] ||= resource + if match = @collection[resource.resource_name] + raise_if_mismatched! match, resource + match + else + @collection[resource.resource_name] = resource + end end # Changes `each` to pass in the value, instead of both the key and value. def each(&block) - values.each &block + values.each(&block) end - # Finds a resource based on the resource name, the resource class, or the base class. - def [](klass) - if match = @resources[klass] - match - elsif match = real_resources.detect{ |r| r.resource_class.to_s == klass.to_s } - match - elsif klass.respond_to? :base_class - real_resources.detect{ |r| r.resource_class.to_s == klass.base_class.to_s } - end + def [](obj) + @collection[obj] || find_resource(obj) end private - # REFACTOR: ResourceCollection currently stores Resource and Page objects. That doesn't - # make sense, because by definition a ResourceCollection is a collection of resources. - def real_resources - select{ |r| r.respond_to? :resource_class } + # Finds a resource based on the resource name, resource class, or base class. + def find_resource(obj) + resources.detect do |r| + r.resource_name.to_s == obj.to_s + end || resources.detect do |r| + r.resource_class.to_s == obj.to_s + end || + if obj.respond_to? :base_class + resources.detect { |r| r.resource_class.to_s == obj.base_class.to_s } + end + end + + def resources + select { |r| r.class <= Resource } # can otherwise be a Page + end + + def raise_if_mismatched!(existing, given) + if existing.class != given.class + raise IncorrectClass.new existing, given + elsif given.class <= Resource && existing.resource_class != given.resource_class + raise ConfigMismatch.new existing, given + end + end + + class IncorrectClass < StandardError + def initialize(existing, given) + super "You're trying to register #{given.resource_name} which is a #{given.class}, " + + "but #{existing.resource_name}, a #{existing.class} has already claimed that name." + end + end + + class ConfigMismatch < StandardError + def initialize(existing, given) + super "You're trying to register #{given.resource_class} as #{given.resource_name}, " + + "but the existing #{existing.class} config was built for #{existing.resource_class}!" + end end end diff --git a/lib/active_admin/resource_controller.rb b/lib/active_admin/resource_controller.rb deleted file mode 100644 index 6950ae55be1..00000000000 --- a/lib/active_admin/resource_controller.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'inherited_resources' -require 'active_admin/resource_controller/actions' -require 'active_admin/resource_controller/action_builder' -require 'active_admin/resource_controller/data_access' -require 'active_admin/resource_controller/decorators' -require 'active_admin/resource_controller/scoping' -require 'active_admin/resource_controller/sidebars' -require 'active_admin/resource_controller/resource_class_methods' - -module ActiveAdmin - # All Resources Controller inherits from this controller. - # It implements actions and helpers for resources. - class ResourceController < BaseController - layout :determine_active_admin_layout - - respond_to :html, :xml, :json - respond_to :csv, :only => :index - - include Actions - include ActionBuilder - include Decorators - include DataAccess - include Scoping - include Sidebars - extend ResourceClassMethods - - def self.active_admin_config=(config) - if @active_admin_config = config - defaults :resource_class => config.resource_class, - :route_prefix => config.route_prefix, - :instance_name => config.resource_name.singular - end - end - - # Inherited Resources uses the `self.inherited(base)` hook to add - # in `self.resource_class`. To override it, we need to install - # our resource_class method each time we're inherited from. - def self.inherited(base) - super(base) - base.override_resource_class_methods! - end - - private - - # Returns the renderer class to use for the given action. - def renderer_for(action) - active_admin_namespace.view_factory["#{action}_page"] - end - helper_method :renderer_for - - end -end diff --git a/lib/active_admin/resource_controller/actions.rb b/lib/active_admin/resource_controller/actions.rb deleted file mode 100644 index e0e3f434153..00000000000 --- a/lib/active_admin/resource_controller/actions.rb +++ /dev/null @@ -1,81 +0,0 @@ -module ActiveAdmin - class ResourceController < BaseController - - # Override the InheritedResources actions to use the - # Active Admin templates. - # - # We ensure that the functionality provided by Inherited - # Resources is still available within any ResourceController - - def index(options={}, &block) - super(options) do |format| - block.call(format) if block - format.html { render active_admin_template('index') } - format.csv do - headers['Content-Type'] = 'text/csv; charset=utf-8' - headers['Content-Disposition'] = %{attachment; filename="#{csv_filename}"} - render active_admin_template('index') - end - end - end - alias :index! :index - - def show(options={}, &block) - super do |format| - block.call(format) if block - format.html { render active_admin_template('show') } - end - end - alias :show! :show - - def new(options={}, &block) - super do |format| - block.call(format) if block - format.html { render active_admin_template('new') } - end - end - alias :new! :new - - def edit(options={}, &block) - super do |format| - block.call(format) if block - format.html { render active_admin_template('edit') } - end - end - alias :edit! :edit - - def create(options={}, &block) - super(options) do |success, failure| - block.call(success, failure) if block - failure.html { render active_admin_template('new') } - end - end - alias :create! :create - - def update(options={}, &block) - super do |success, failure| - block.call(success, failure) if block - failure.html { render active_admin_template('edit') } - end - end - alias :update! :update - - - # Make aliases protected - protected :index!, :show!, :new!, :create!, :edit!, :update! - - protected - - # Returns the full location to the Active Admin template path - def active_admin_template(template) - "active_admin/resource/#{template}" - end - - # Returns a filename for the csv file using the collection_name - # and current date such as 'my-articles-2011-06-24.csv'. - def csv_filename - "#{resource_collection_name.to_s.gsub('_', '-')}-#{Time.now.strftime("%Y-%m-%d")}.csv" - end - - end -end diff --git a/lib/active_admin/resource_controller/data_access.rb b/lib/active_admin/resource_controller/data_access.rb deleted file mode 100644 index bad78f3f0a5..00000000000 --- a/lib/active_admin/resource_controller/data_access.rb +++ /dev/null @@ -1,301 +0,0 @@ -module ActiveAdmin - class ResourceController < BaseController - - # This module overrides most of the data access methods in Inherited - # Resources to provide Active Admin with it's data. - # - # The module also deals with authorization and resource callbacks. - # - module DataAccess - extend ActiveSupport::Concern - - include ActiveAdmin::Callbacks - include ActiveAdmin::ScopeChain - - included do - define_active_admin_callbacks :build, :create, :update, :save, :destroy - end - - protected - - # Retrieve, memoize and authorize the current collection from the db. This - # method delegates the finding of the collection to #find_collection. - # - # Once #collection has been called, the collection is available using - # either the @collection instance variable or an instance variable named - # after the resource that the collection is for. eg: Post => @post. - # - # @returns [ActiveRecord::Relation] The collection for the index - def collection - _collection = get_collection_ivar - - return _collection if _collection - - _collection = find_collection - authorize! ActiveAdmin::Authorization::READ, active_admin_config.resource_class - - set_collection_ivar _collection - end - - - # Does the actual work of retrieving the current collection from the db. - # This is a great method to override if you would like to perform - # some additional db # work before your controller returns and - # authorizes the collection. - # - # @returns [ActiveRecord::Relation] The collectin for the index - def find_collection - collection = scoped_collection - - collection = apply_authorization_scope(collection) - collection = apply_sorting(collection) - collection = apply_filtering(collection) - collection = apply_scoping(collection) - collection = apply_pagination(collection) - collection = apply_decorator(collection) - - collection - end - - - # Override this method in your controllers to modify the start point - # of our searches and index. - # - # This method should return an ActiveRecord::Relation object so that - # the searching and filtering can be applied on top - # - # Note, unless you are doing something special, you should use the - # scope_to method from the Scoping module instead of overriding this - # method. - def scoped_collection - end_of_association_chain - end - - # Retrieve, memoize and authorize a resource based on params[:id]. The - # actual work of finding the resource is done in #find_resource. - # - # This method is used on all the member actions: - # - # * show - # * edit - # * update - # * destroy - # - # @returns [ActiveRecord::Base] An active record object - def resource - _resource = get_resource_ivar - - return _resource if _resource - - _resource = find_resource - authorize_resource! _resource - - if decorator? - _resource = decorator_class.new(_resource) - end - - set_resource_ivar(_resource) - end - - def decorator? - !!active_admin_config.decorator_class - end - - def decorator_class - active_admin_config.decorator_class - end - - - # Does the actual work of finding a resource in the database. This - # method uses the finder method as defined in InheritedResources. - # - # @returns [ActiveRecord::Base] An active record object. - def find_resource - scoped_collection.send(method_for_find, params[:id]) - end - - - # Builds, memoize and authorize a new instance of the resource. The - # actual work of building the new instance is delegated to the - # #build_new_resource method. - # - # This method is used to instantiate and authorize new resources in the - # new and create controller actions. - # - # @returns [ActiveRecord::Base] An un-saved active record base object - def build_resource - return resource if resource = get_resource_ivar - - resource = build_new_resource - - run_build_callbacks resource - authorize_resource! resource - - set_resource_ivar(resource) - end - - # Builds a new resource. This method uses the method_for_build provided - # by Inherited Resources. - # - # @returns [ActiveRecord::Base] An un-saved active record base object - def build_new_resource - scoped_collection.send(method_for_build, *resource_params) - end - - # Calls all the appropriate callbacks and then creates the new resource. - # - # @param [ActiveRecord::Base] object The new resource to create - # - # @returns [void] - def create_resource(object) - run_create_callbacks object do - save_resource(object) - end - end - - # Calls all the appropriate callbacks and then saves the new resource. - # - # @param [ActiveRecord::Base] object The new resource to save - # - # @returns [void] - def save_resource(object) - run_save_callbacks object do - object.save - end - end - - # Update an object with the given attributes. Also calls the appropriate - # callbacks for update action. - # - # @param [ActiveRecord::Base] object The instance to update - # - # @param [Array] attributes An array with the attributes in the first position - # and the Active Record "role" in the second. The role - # may be set to nil. - # - # @returns [void] - def update_resource(object, attributes) - if object.respond_to?(:assign_attributes) - object.assign_attributes(*attributes) - else - object.attributes = attributes[0] - end - - run_update_callbacks object do - save_resource(object) - end - end - - # Destroys an object from the database and calls appropriate callbacks. - # - # @returns [void] - def destroy_resource(object) - run_destroy_callbacks object do - object.destroy - end - end - - - # - # Collection Helper Methods - # - - - # Gives the authorization library a change to pre-scope the collection. - # - # In the case of the CanCan adapter, it calls `#accessible_by` on - # the collection. - # - # @param [ActiveRecord::Relation] collection The collection to scope - # - # @retruns [ActiveRecord::Relation] a scoped collection of query - def apply_authorization_scope(collection) - action_name = action_to_permission(params[:action]) - active_admin_authorization.scope_collection(collection, action_name) - end - - - def apply_sorting(chain) - params[:order] ||= active_admin_config.sort_order - if params[:order] && params[:order] =~ /^([\w\_\.]+)_(desc|asc)$/ - column = $1 - order = $2 - table = active_admin_config.resource_column_names.include?(column) ? active_admin_config.resource_table_name : nil - table_column = (column =~ /\./) ? column : - [table, active_admin_config.resource_quoted_column_name(column)].compact.join(".") - - chain.reorder("#{table_column} #{order}") - else - chain # just return the chain - end - end - - def apply_filtering(chain) - @search = chain.metasearch(clean_search_params(params[:q])) - @search.relation - end - - def clean_search_params(search_params) - return {} unless search_params.is_a?(Hash) - search_params = search_params.dup - search_params.delete_if do |key, value| - value == "" - end - search_params - end - - def apply_scoping(chain) - @collection_before_scope = chain - - if current_scope - scope_chain(current_scope, chain) - else - chain - end - end - - def collection_before_scope - @collection_before_scope - end - - def current_scope - @current_scope ||= if params[:scope] - active_admin_config.get_scope_by_id(params[:scope]) if params[:scope] - else - active_admin_config.default_scope(self) - end - end - - def apply_pagination(chain) - page_method_name = Kaminari.config.page_method_name - page = params[Kaminari.config.param_name] - - chain.send(page_method_name, page).per(per_page) - end - - def per_page - return max_csv_records if request.format == 'text/csv' - return max_per_page if active_admin_config.paginate == false - - @per_page || active_admin_config.per_page - end - - def max_csv_records - 10_000 - end - - def max_per_page - 10_000 - end - - def apply_decorator(chain) - if decorator? - decorator_class.decorate_collection(chain) - else - chain - end - end - - end - end -end diff --git a/lib/active_admin/resource_controller/decorators.rb b/lib/active_admin/resource_controller/decorators.rb deleted file mode 100644 index cbac583d198..00000000000 --- a/lib/active_admin/resource_controller/decorators.rb +++ /dev/null @@ -1,20 +0,0 @@ -module ActiveAdmin - class ResourceController < BaseController - module Decorators - protected - - def resource - decorator = active_admin_config.decorator_class - resource = super - decorator ? decorator.new(resource) : resource - end - - def active_admin_collection - decorator = active_admin_config.decorator_class - collection = super - decorator ? decorator.decorate_collection(collection) : collection - end - - end - end -end diff --git a/lib/active_admin/resource_controller/sidebars.rb b/lib/active_admin/resource_controller/sidebars.rb deleted file mode 100644 index dfbaa0e6389..00000000000 --- a/lib/active_admin/resource_controller/sidebars.rb +++ /dev/null @@ -1,18 +0,0 @@ -module ActiveAdmin - class ResourceController < BaseController - - module Sidebars - - protected - - def skip_sidebar! - @skip_sidebar = true - end - - def skip_sidebar? - @skip_sidebar == true - end - end - - end -end diff --git a/lib/active_admin/resource_dsl.rb b/lib/active_admin/resource_dsl.rb index 9672bc03b83..5526cddd129 100644 --- a/lib/active_admin/resource_dsl.rb +++ b/lib/active_admin/resource_dsl.rb @@ -1,8 +1,29 @@ +# frozen_string_literal: true module ActiveAdmin - # This is the class where all the register blocks are instance eval'd + # This is the class where all the register blocks are evaluated. class ResourceDSL < DSL + private + # Redefine sort behaviour for column + # + # For example: + # + # # nulls last + # order_by(:age) do |order_clause| + # [order_clause.to_sql, 'NULLS LAST'].join(' ') if order_clause.order == 'desc' + # end + # + # # by last_name but in the case that there is no last name, by first_name. + # order_by(:full_name) do |order_clause| + # ['COALESCE(NULLIF(last_name, ''), first_name), first_name', order_clause.order].join(' ') + # end + # + # + def order_by(column, &block) + config.ordering[column] = block + end + def belongs_to(target, options = {}) config.belongs_to(target, options) end @@ -17,6 +38,49 @@ def scope(*args, &block) config.scope(*args, &block) end + # Store relations that should be included + def includes(*args) + config.includes.push(*args) + end + + # + # Keys included in the `permitted_params` setting are automatically whitelisted. + # + # Either + # + # permit_params :title, :author, :body, tags: [] + # + # Or + # + # permit_params do + # defaults = [:title, :body] + # if current_user.admin? + # defaults + [:author] + # else + # defaults + # end + # end + # + def permit_params(*args, &block) + param_key = config.param_key.to_sym + + controller do + define_method :permitted_params do + belongs_to_param = active_admin_config.belongs_to_param + create_another_param = :create_another if active_admin_config.create_another + + permitted_params = + active_admin_namespace.permitted_params + + Array.wrap(belongs_to_param) + + Array.wrap(create_another_param) + + params.permit(*permitted_params, param_key => block ? instance_exec(&block) : args) + end + + private :permitted_params + end + end + # Configure the index page for the resource def index(options = {}, &block) options[:as] ||= :table @@ -41,11 +105,13 @@ def form(options = {}, &block) # column("Author") { |post| post.author.full_name } # end # - # csv :col_sep => ";", :force_quotes => true do + # csv col_sep: ";", force_quotes: true do # column :name # end # - def csv(options={}, &block) + def csv(options = {}, &block) + options[:resource] = config + config.csv_builder = CSVBuilder.new(options, &block) end @@ -57,7 +123,7 @@ def csv(options={}, &block) # # ActiveAdmin.register Post do # member_action :comments do - # @post = Post.find(params[:id] + # @post = Post.find(params[:id]) # @comments = @post.comments # end # end @@ -69,12 +135,14 @@ def csv(options={}, &block) # action. # def action(set, name, options = {}, &block) + warn "Warning: method `#{name}` already defined in #{controller.name}" if controller.method_defined?(name) + set << ControllerAction.new(name, options) title = options.delete(:title) controller do - before_filter(:only => [name]) { @page_title = title } if title - define_method(name, &block || Proc.new{}) + before_action(only: [name]) { @page_title = title } if title + define_method(name, &block || Proc.new {}) end end @@ -86,6 +154,26 @@ def collection_action(name, options = {}, &block) action config.collection_actions, name, options, &block end + # Set the default `filters` list for this resource's string filters. + # Takes priority over the namespace-level `string_input_filters` setting. + # + # For example: + # + # ActiveAdmin.register Post do + # string_input_filters [:eq, :cont] + # end + # + def string_input_filters(value) + config.string_input_filters = value + end + + def decorate_with(decorator_class) + # Force storage as a string. This will help us with reloading issues. + # Assuming decorator_class.to_s will return the name of the class allows + # us to handle a string or a class. + config.decorator_class_name = "::#{ decorator_class }" + end + # Defined Callbacks # # == After Build @@ -110,14 +198,15 @@ def collection_action(name, options = {}, &block) # == Before / After Destroy # Called before and after the object is destroyed from the database. # - delegate :before_build, :after_build, :to => :controller - delegate :before_create, :after_create, :to => :controller - delegate :before_update, :after_update, :to => :controller - delegate :before_save, :after_save, :to => :controller - delegate :before_destroy, :after_destroy, :to => :controller + delegate :before_build, :after_build, to: :controller + delegate :before_create, :after_create, to: :controller + delegate :before_update, :after_update, to: :controller + delegate :before_save, :after_save, to: :controller + delegate :before_destroy, :after_destroy, to: :controller - # Standard rails filters - delegate :before_filter, :skip_before_filter, :after_filter, :around_filter, :to => :controller + standard_rails_filters = + AbstractController::Callbacks::ClassMethods.public_instance_methods.select { |m| m.end_with?('_action') } + delegate(*standard_rails_filters, to: :controller) # Specify which actions to create in the controller # @@ -128,7 +217,7 @@ def collection_action(name, options = {}, &block) # end # # Will only create the index and show actions (no create, update or delete) - delegate :actions, :to => :controller + delegate :actions, to: :controller end end diff --git a/lib/active_admin/router.rb b/lib/active_admin/router.rb index 3e53ebf3b0a..38b8f92b101 100644 --- a/lib/active_admin/router.rb +++ b/lib/active_admin/router.rb @@ -1,106 +1,115 @@ +# frozen_string_literal: true module ActiveAdmin + # @private class Router - def initialize(application) - @application = application + attr_reader :namespaces, :router + + def initialize(router:, namespaces:) + @router = router + @namespaces = namespaces end - # Creates all the necessary routes for the ActiveAdmin configurations - # - # Use this within the routes.rb file: - # - # Application.routes.draw do |map| - # ActiveAdmin.routes(self) - # end - # - def apply(router) - define_root_routes router - define_resource_routes router + def apply + define_root_routes + define_resources_routes end - def define_root_routes(router) - router.instance_exec @application.namespaces.values do |namespaces| - namespaces.each do |namespace| - if namespace.root? - root :to => namespace.root_to - else - namespace namespace.name do - root :to => namespace.root_to - end + private + + def define_root_routes + namespaces.each do |namespace| + if namespace.root? + router.root namespace.root_to_options.merge(to: namespace.root_to) + else + router.namespace namespace.name, **namespace.route_options.dup do + router.root namespace.root_to_options.merge(to: namespace.root_to, as: :root) end end end end # Defines the routes for each resource - def define_resource_routes(router) - router.instance_exec @application.namespaces, self do |namespaces, aa_router| - resources = namespaces.values.map{ |n| n.resources.values }.flatten - resources.each do |config| - routes = aa_router.resource_routes(config) - - # Add in the parent if it exists - if config.belongs_to? - belongs_to = routes - routes = Proc.new do - # If it's optional, make the normal resource routes - instance_exec &belongs_to if config.belongs_to_config.optional? - - # Make the nested belongs_to routes - # :only is set to nothing so that we don't clobber any existing routes on the resource - resources config.belongs_to_config.target.resource_name.plural, :only => [] do - instance_exec &belongs_to - end - end - end + def define_resources_routes + resources = namespaces.flat_map { |n| n.resources.values } + resources.each do |config| + define_resource_routes(config) + end + end - # Add on the namespace if required - unless config.namespace.root? - nested = routes - routes = Proc.new do - namespace config.namespace.name do - instance_exec &nested - end - end - end + def define_resource_routes(config) + if config.namespace.root? + define_routes(config) + else + # Add on the namespace if required + define_namespace(config) + end + end - instance_exec &routes - end + def define_routes(config) + if config.belongs_to? + define_belongs_to_routes(config) + else + page_or_resource_routes(config) end end + def page_or_resource_routes(config) + config.is_a?(Page) ? page_routes(config) : resource_routes(config) + end + def resource_routes(config) - Proc.new do - # Builds one route for each HTTP verb passed in - build_route = proc{ |verbs, *args| - [*verbs].each{ |verb| send verb, *args } - } - # Deals with +ControllerAction+ instances - build_action = proc{ |action| - build_route.call(action.http_verb, action.name) - } - case config - when ::ActiveAdmin::Resource - resources config.resource_name.route_key, :only => config.defined_actions do - member do - config.member_actions.each &build_action - end - - collection do - config.collection_actions.each &build_action - post :batch_action - end - end - when ::ActiveAdmin::Page - page = config.underscored_resource_name - get "/#{page}" => "#{page}#index" - config.page_actions.each do |action| - build_route.call action.http_verb, "/#{page}/#{action.name}" => "#{page}##{action.name}" - end - else - raise "Unsupported config class: #{config.class}" + router.resources config.resource_name.route_key, only: config.defined_actions do + define_actions(config) + end + end + + def page_routes(config) + page = config.underscored_resource_name + router.get "/#{page}", to: "#{page}#index" + config.page_actions.each do |action| + Array.wrap(action.http_verb).each do |verb| + build_route(verb, "/#{page}/#{action.name}", to: "#{page}##{action.name}") end end + end + + # Defines member and collection actions + def define_actions(config) + router.member do + config.member_actions.each { |action| build_action(action) } + end + + router.collection do + config.collection_actions.each { |action| build_action(action) } + router.post :batch_action if config.batch_actions_enabled? + end + end + + # Deals with +ControllerAction+ instances + # Builds one route for each HTTP verb passed in + def build_action(action) + build_route(action.http_verb, action.name) + end + + def build_route(verbs, ...) + Array.wrap(verbs).each { |verb| router.send(verb, ...) } + end + + def define_belongs_to_routes(config) + # If it's optional, make the normal resource routes + page_or_resource_routes(config) if config.belongs_to_config.optional? + + # Make the nested belongs_to routes + # :only is set to nothing so that we don't clobber any existing routes on the resource + router.resources config.belongs_to_config.target.resource_name.plural, only: [] do + page_or_resource_routes(config) + end + end + def define_namespace(config) + router.namespace config.namespace.name, **config.namespace.route_options.dup do + define_routes(config) + end end end end diff --git a/lib/active_admin/sass/active_admin.scss b/lib/active_admin/sass/active_admin.scss deleted file mode 100644 index 72958351118..00000000000 --- a/lib/active_admin/sass/active_admin.scss +++ /dev/null @@ -1,3 +0,0 @@ -// This file is included when using Rails 3.0 -@import "active_admin/mixins"; -@import "active_admin/base"; diff --git a/lib/active_admin/sass/css_loader.rb b/lib/active_admin/sass/css_loader.rb deleted file mode 100644 index ac78be3d9cc..00000000000 --- a/lib/active_admin/sass/css_loader.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'sass/importers' - -# This monkey patches the SASS filesystem importer to work with files -# that are named *.css.scss. This allows us to be compatible with both -# Rails 3.0.* and Rails 3.1 -# -# This should only be loaded in Rails 3.0 apps. -class Sass::Importers::Filesystem - - # We want to ensure that all *.css.scss files are loaded as scss files - def extensions_with_css - extensions_without_css.merge('{css.,}scss' => :scss) - end - alias_method_chain :extensions, :css - -end diff --git a/lib/active_admin/sass/helpers.rb b/lib/active_admin/sass/helpers.rb deleted file mode 100644 index edceec770d9..00000000000 --- a/lib/active_admin/sass/helpers.rb +++ /dev/null @@ -1,45 +0,0 @@ -require 'sass' -require 'sass/script/functions' -require 'sass-rails' if ActiveAdmin::DependencyChecker.rails_3_1? - -module ActiveAdmin - module Sass - module Helpers - - if ActiveAdmin::DependencyChecker.rails_3_1? - include ::Sass::Rails::Helpers - end - - # Provides a helper in SASS to ensure that the paths to image - # assets are always correct across Rails versions. - # - # Example: - # - # background: url(active_admin_image_path('some_image.png')) 0 0 repeat-x; - # - # Will result in: - # - # background: url("/images/active_admin/some_image.png") 0 0 repeat-x; - # - # Or in Rails 3.1 with asset pipeline enebaled: - # - # background: url("/assets/active_admin/some_image.png") 0 0 repeat-x; - # - # @param [Sass::Script::String] asset the path to the image after */active_admin/ - # - # @return [Sass::Script::String] path to the image - # - def active_admin_image_path(asset) - if ActiveAdmin.use_asset_pipeline? - asset_path(::Sass::Script::String.new("active_admin/#{asset.value}"), ::Sass::Script::String.new('image')) - else - ::Sass::Script::String.new("/images/active_admin/#{asset.value}", true) - end - end - - end - end -end - -# Install for use in Sass -Sass::Script::Functions.send :include, ActiveAdmin::Sass::Helpers diff --git a/lib/active_admin/scope.rb b/lib/active_admin/scope.rb index 3e0090757e1..6eece2052ea 100644 --- a/lib/active_admin/scope.rb +++ b/lib/active_admin/scope.rb @@ -1,7 +1,8 @@ +# frozen_string_literal: true module ActiveAdmin class Scope - attr_reader :scope_method, :id, :scope_block, :display_if_block, :show_count, :default_block + attr_reader :scope_method, :id, :scope_block, :display_if_block, :show_count, :default_block, :group # Create a Scope # @@ -13,45 +14,62 @@ class Scope # Scope.new('Published', :public) # # => Scope with name 'Published' and scope method :public # - # Scope.new 'Published', :public, :if => proc { current_admin_user.can? :manage, resource_class } do |articles| - # articles.where :published => true + # Scope.new(:published, show_count: :async) + # # => Scope with name 'Published' that queries its count asynchronously + # + # Scope.new(:published, show_count: false) + # # => Scope with name 'Published' that does not display a count + # + # Scope.new 'Published', :public, if: proc { current_admin_user.can? :manage, resource_class } do |articles| + # articles.where published: true # end # # => Scope with name 'Published' and scope method :public, optionally displaying the scope per the :if block # - # Scope.new('Published') { |articles| articles.where(:published => true) } + # Scope.new('Published') { |articles| articles.where(published: true) } # # => Scope with name 'Published' using a block to scope # # Scope.new ->{Date.today.strftime '%A'}, :published_today # # => Scope with dynamic title using the :published_today scope method # + # Scope.new :published, nil, group: :status + # # => Scope with the group :status + # def initialize(name, method = nil, options = {}, &block) - @name, @scope_method = name, method.try(:to_sym) + @name = name + @scope_method = method.try(:to_sym) if name.is_a? Proc raise "A string/symbol is required as the second argument if your label is a proc." unless method - @id = method.to_s.gsub(' ', '').underscore + @id = method.to_s.parameterize(separator: "_") else @scope_method ||= name.to_sym - @id = name.to_s.gsub(' ', '').underscore + @id = name.to_s.parameterize(separator: "_") end - @scope_method = nil if @scope_method == :all - @scope_method, @scope_block = nil, block if block_given? - - @show_count = options[:show_count].nil? ? true : options[:show_count] - @display_if_block = options[:if] || proc{ true } - @default_block = options[:default] || proc{ false } + @scope_method = nil if @scope_method == :all + if block + @scope_method = nil + @scope_block = block + end + @localizer = options[:localizer] + @show_count = options.fetch(:show_count, true) + @display_if_block = options[:if] || proc { true } + @default_block = options[:default] || proc { false } + @group = options[:group].try(:to_sym) end def name case @name - when Proc then @name.call.to_s - when String then @name - when Symbol then @name.to_s.titleize - else @name.to_s + when String then @name + when Symbol then @localizer ? @localizer.t(@name, scope: "scopes") : @name.to_s.titleize + else @name end end + def async_count? + @show_count == :async + end + end end diff --git a/lib/active_admin/settings_node.rb b/lib/active_admin/settings_node.rb new file mode 100644 index 00000000000..aae29e8953f --- /dev/null +++ b/lib/active_admin/settings_node.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true +module ActiveAdmin + + class SettingsNode + class << self + # Never instantiated. Variables are stored in the singleton_class. + private_class_method :new + + # @return anonymous class with same accessors as the superclass. + def build(superclass = self) + Class.new(superclass) + end + + def register(name, value) + class_attribute name + send :"#{name}=", value + end + end + end +end diff --git a/lib/active_admin/sidebar_section.rb b/lib/active_admin/sidebar_section.rb index 49397e4a5c9..a920f5a432f 100644 --- a/lib/active_admin/sidebar_section.rb +++ b/lib/active_admin/sidebar_section.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin class SidebarSection @@ -6,31 +7,28 @@ class SidebarSection attr_accessor :name, :options, :block def initialize(name, options = {}, &block) - @name, @options, @block = name, options, block + @name = name.to_s + @options = options + @block = block normalize_display_options! end # The id gets used for the div in the view def id - "#{name.to_s.downcase.underscore}_sidebar_section".parameterize + "#{name.downcase.underscore}_sidebar_section".parameterize end - def icon? - !!options[:icon] - end - - def icon - options[:icon] if icon? + # If a block is not passed in, the name of the partial to render + def partial_name + options[:partial] || "#{name.downcase.tr(' ', '_')}_sidebar" end - # The title gets displayed within the section in the view - def title - I18n.t("active_admin.sidebars.#{name.to_s}", :default => name.to_s.titlecase) + def custom_class + options[:class] end - # If a block is not passed in, the name of the partial to render - def partial_name - options[:partial] || "#{name.to_s.downcase.gsub(' ', '_')}_sidebar" + def priority + options[:priority] || 10 end end diff --git a/lib/active_admin/version.rb b/lib/active_admin/version.rb index 605ec4b7d95..be8415a6c12 100644 --- a/lib/active_admin/version.rb +++ b/lib/active_admin/version.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin - VERSION = '0.6.0' + VERSION = "4.0.0.beta23" end diff --git a/lib/active_admin/view_factory.rb b/lib/active_admin/view_factory.rb deleted file mode 100644 index b0689721f84..00000000000 --- a/lib/active_admin/view_factory.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'active_admin/abstract_view_factory' - -module ActiveAdmin - class ViewFactory < AbstractViewFactory - - # Register Helper Renderers - register :global_navigation => ActiveAdmin::Views::TabbedNavigation, - :utility_navigation => ActiveAdmin::Views::TabbedNavigation, - :site_title => ActiveAdmin::Views::SiteTitle, - :action_items => ActiveAdmin::Views::ActionItems, - :title_bar => ActiveAdmin::Views::TitleBar, - :header => ActiveAdmin::Views::Header, - :footer => ActiveAdmin::Views::Footer, - :index_scopes => ActiveAdmin::Views::Scopes, - :blank_slate => ActiveAdmin::Views::BlankSlate, - :action_list_popover => ActiveAdmin::Views::ActionListPopover - - # Register All The Pages - register :dashboard_page => ActiveAdmin::Views::Pages::Dashboard, - :index_page => ActiveAdmin::Views::Pages::Index, - :show_page => ActiveAdmin::Views::Pages::Show, - :new_page => ActiveAdmin::Views::Pages::Form, - :edit_page => ActiveAdmin::Views::Pages::Form, - :layout => ActiveAdmin::Views::Pages::Layout, - :page => ActiveAdmin::Views::Pages::Page - - end -end diff --git a/lib/active_admin/view_helpers.rb b/lib/active_admin/view_helpers.rb index d5dfdbff9c0..d4c84d9ba50 100644 --- a/lib/active_admin/view_helpers.rb +++ b/lib/active_admin/view_helpers.rb @@ -1,21 +1,9 @@ +# frozen_string_literal: true module ActiveAdmin module ViewHelpers - # Require all ruby files in the view helpers dir - Dir[File.expand_path('../view_helpers', __FILE__) + "/*.rb"].each{|f| require f } + Dir[File.expand_path("view_helpers", __dir__) + "/*.rb"].each { |f| require f } - include AssignsWithIndifferentAccessHelper - include ActiveAdminApplicationHelper - include AutoLinkHelper - include BreadcrumbHelper - include DisplayHelper - include IconHelper include MethodOrProcHelper - include SidebarHelper - include FormHelper - include TitleHelper - include ViewFactoryHelper - include FlashHelper - end end diff --git a/lib/active_admin/view_helpers/active_admin_application_helper.rb b/lib/active_admin/view_helpers/active_admin_application_helper.rb deleted file mode 100644 index d475c919c56..00000000000 --- a/lib/active_admin/view_helpers/active_admin_application_helper.rb +++ /dev/null @@ -1,12 +0,0 @@ -module ActiveAdmin - module ViewHelpers - module ActiveAdminApplicationHelper - - # Returns the current Active Admin application instance - def active_admin_application - ActiveAdmin.application - end - - end - end -end diff --git a/lib/active_admin/view_helpers/assigns_with_indifferent_access_helper.rb b/lib/active_admin/view_helpers/assigns_with_indifferent_access_helper.rb deleted file mode 100644 index c866e235391..00000000000 --- a/lib/active_admin/view_helpers/assigns_with_indifferent_access_helper.rb +++ /dev/null @@ -1,7 +0,0 @@ -module AssignsWithIndifferentAccessHelper - - def assigns - @assigns_with_indifferent_access_helper ||= HashWithIndifferentAccess.new(super) - end - -end diff --git a/lib/active_admin/view_helpers/auto_link_helper.rb b/lib/active_admin/view_helpers/auto_link_helper.rb deleted file mode 100644 index 4b92f24b97d..00000000000 --- a/lib/active_admin/view_helpers/auto_link_helper.rb +++ /dev/null @@ -1,34 +0,0 @@ -module ActiveAdmin - module ViewHelpers - module AutoLinkHelper - - # Automatically links objects to their resource controllers. If - # the resource has not been registered, a string representation of - # the object is returned. - # - # The default content in the link is returned from ActiveAdmin::ViewHelpers::DisplayHelper#display_name - # - # You can pass in the content to display - # eg: auto_link(@post, "My Link Content") - # - def auto_link(resource, link_content = nil) - content = link_content || display_name(resource) - if (registration = active_admin_resource_for(resource.class)) - begin - content = link_to(content, registration.route_instance_path(resource)) - rescue - # ignored - end - end - content - end - - # Returns the ActiveAdmin::Resource instance for a class - def active_admin_resource_for(klass) - return nil unless respond_to?(:active_admin_namespace) - active_admin_namespace.resource_for(klass) - end - - end - end -end diff --git a/lib/active_admin/view_helpers/breadcrumb_helper.rb b/lib/active_admin/view_helpers/breadcrumb_helper.rb deleted file mode 100644 index bf3f1c91759..00000000000 --- a/lib/active_admin/view_helpers/breadcrumb_helper.rb +++ /dev/null @@ -1,27 +0,0 @@ -module ActiveAdmin - module ViewHelpers - module BreadcrumbHelper - - # Returns an array of links to use in a breadcrumb - def breadcrumb_links(path = request.path) - parts = path[1..-1].split('/') # remove leading "/" and split up the URL - parts.pop # remove last since it's used as the page title - - parts.each_with_index.map do |part, index| - # 1. try using `display_name` if we can locate a DB object - # 2. try using the model name translation - # 3. default to calling `titlecase` on the URL fragment - if part =~ /\A(\d+|[a-f0-9]{24})\z/ && parts[index-1] - config = active_admin_config.belongs_to_config.try(:target) || active_admin_config - obj = config.resource_class.where( config.resource_class.primary_key => part ).first - name = display_name obj - end - name ||= I18n.t "activerecord.models.#{part.singularize}", :count => 1.1, :default => part.titlecase - - link_to name, '/' + parts[0..index].join('/') - end - end - - end - end -end diff --git a/lib/active_admin/view_helpers/display_helper.rb b/lib/active_admin/view_helpers/display_helper.rb deleted file mode 100644 index 4390ffac2f0..00000000000 --- a/lib/active_admin/view_helpers/display_helper.rb +++ /dev/null @@ -1,47 +0,0 @@ -module ActiveAdmin - module ViewHelpers - module DisplayHelper - - # Attempts to call any known display name methods on the resource. - # See the setting in `application.rb` for the list of methods and their priority. - def display_name(resource) - resource.send display_name_method_for resource if resource - end - - # Looks up and caches the first available display name method. - def display_name_method_for(resource) - @@display_name_methods_cache ||= {} - @@display_name_methods_cache[resource.class] ||= begin - methods = active_admin_application.display_name_methods - association_methods_for(resource) - methods.detect{ |method| resource.respond_to? method } - end - end - - # To prevent conflicts, we exclude any methods that happen to be associations. - def association_methods_for(resource) - return [] unless resource.class.respond_to? :reflect_on_all_associations - resource.class.reflect_on_all_associations.map(&:name) - end - - # Return a pretty string for any object - # Date Time are formatted via #localize with :format => :long - # ActiveRecord objects are formatted via #auto_link - # We attempt to #display_name of any other objects - def pretty_format(object) - case object - when String - object - when Arbre::Element - object - when Date, Time - localize(object, :format => :long) - when ActiveRecord::Base - auto_link(object) - else - display_name(object) - end - end - - end - end -end diff --git a/lib/active_admin/view_helpers/download_format_links_helper.rb b/lib/active_admin/view_helpers/download_format_links_helper.rb deleted file mode 100644 index 17785f54e47..00000000000 --- a/lib/active_admin/view_helpers/download_format_links_helper.rb +++ /dev/null @@ -1,50 +0,0 @@ -module ActiveAdmin - module ViewHelpers - module DownloadFormatLinksHelper - - module ClassMethods - - # A ready only of formats to make available in index/paginated - # collection view. - # @return [Array] - # @see add_format for information on adding custom download link - # formats - def formats - @formats ||= [:csv, :xml, :json] - @formats.clone - end - - # Adds a mime type extension to the list of available formats. - # You must register the extension prior to adding it to the list - # of avilable formats. This should be used by plugins that want - # to add additional formats to the download format links. - # @param [Symbol] extension the mime extension to add - # @return [Array] A copy of the updated formats array. - def add_format extension - unless formats.include?(extension) - if Mime::Type.lookup_by_extension(extension).nil? - raise ArgumentError, "The mime extension you defined: #{extension} is not registered. Please register it via Mime::Type.register before adding it to the available formats." - end - @formats << extension - end - formats - end - end - - # TODO: Refactor to new HTML DSL - def build_download_format_links(formats = self.class.formats) - links = formats.collect do |format| - link_to format.to_s.upcase, { :format => format}.merge(request.query_parameters.except(:commit, :format)) - end - div :class => "download_links" do - text_node [I18n.t('active_admin.download'), links].flatten.join(" ").html_safe - end - end - - def self.included base - base.extend ClassMethods - end - end - end -end - diff --git a/lib/active_admin/view_helpers/fields_for.rb b/lib/active_admin/view_helpers/fields_for.rb deleted file mode 100644 index 09ea6f3da9c..00000000000 --- a/lib/active_admin/view_helpers/fields_for.rb +++ /dev/null @@ -1,50 +0,0 @@ -module ActiveAdmin - module ViewHelpers - module FormHelper - - # Flatten a params Hash to an array of fields. - # - # @param params [Hash] - # @param options [Hash] :namespace and :except - # - # @return [Array] of [Hash] with one element. - # - # @example - # fields_for_params(scope: "all", users: ["greg"]) - # => [ {"scope" => "all"} , {"users[]" => "greg"} ] - # - def fields_for_params(params, options = {}) - namespace = options[:namespace] - except = options[:except].is_a?(Array) ? options[:except] : [options[:except]] - - params.map do |k, v| - next if namespace.nil? && %w(controller action commit utf8).include?(k.to_s) - next if except.map(&:to_s).include?(k.to_s) - - if namespace - k = "#{namespace}[#{k}]" - end - - case v - when String - { k => v } - when Symbol - { k => v.to_s } - when Hash - fields_for_params(v, :namespace => k) - when Array - v.map do |v| - { "#{k}[]" => v } - end - when nil - { k => '' } - when TrueClass,FalseClass - { k => v } - else - raise "I don't know what to do with #{v.class} params: #{v.inspect}" - end - end.flatten.compact - end - end - end -end diff --git a/lib/active_admin/view_helpers/flash_helper.rb b/lib/active_admin/view_helpers/flash_helper.rb deleted file mode 100644 index 868888e9af3..00000000000 --- a/lib/active_admin/view_helpers/flash_helper.rb +++ /dev/null @@ -1,13 +0,0 @@ -module ActiveAdmin - module ViewHelpers - module FlashHelper - - # Returns all the flash keys to display in any Active Admin view. - # This method removes the :timedout key that Devise uses by default - def active_admin_flash_messages - @active_admin_flash_messages ||= flash.to_hash.except(:timedout) - end - - end - end -end diff --git a/lib/active_admin/view_helpers/form_helper.rb b/lib/active_admin/view_helpers/form_helper.rb deleted file mode 100644 index 38c6f65f033..00000000000 --- a/lib/active_admin/view_helpers/form_helper.rb +++ /dev/null @@ -1,24 +0,0 @@ -module ActiveAdmin - module ViewHelpers - module FormHelper - - def active_admin_form_for(resource, options = {}, &block) - options = Marshal.load( Marshal.dump(options) ) - options[:builder] ||= ActiveAdmin::FormBuilder - - if ! options.fetch(:decorate, true) - resource = resource.model - end - - semantic_form_for resource, options, &block - end - - def hidden_field_tags_for(params, options={}) - fields_for_params(params, options).map do |kv| - k, v = kv.first - hidden_field_tag k, v, :id => sanitize_to_id("hidden_active_admin_#{k}") - end.join("\n").html_safe - end - end - end -end diff --git a/lib/active_admin/view_helpers/icon_helper.rb b/lib/active_admin/view_helpers/icon_helper.rb deleted file mode 100644 index da73f140f0d..00000000000 --- a/lib/active_admin/view_helpers/icon_helper.rb +++ /dev/null @@ -1,12 +0,0 @@ -module ActiveAdmin - module ViewHelpers - module IconHelper - - # Render an icon from the Iconic icon set - def icon(*args) - ActiveAdmin::Iconic.icon(*args) - end - - end - end -end diff --git a/lib/active_admin/view_helpers/method_or_proc_helper.rb b/lib/active_admin/view_helpers/method_or_proc_helper.rb index 75c47ce7634..6dc9a0156bd 100644 --- a/lib/active_admin/view_helpers/method_or_proc_helper.rb +++ b/lib/active_admin/view_helpers/method_or_proc_helper.rb @@ -1,4 +1,8 @@ +# frozen_string_literal: true +# Utility methods for internal use. +# @private module MethodOrProcHelper + extend self # This method will either call the symbol on self or instance_exec the Proc # within self. Any args will be passed along to the method dispatch. @@ -18,6 +22,8 @@ def call_method_or_exec_proc(symbol_or_proc, *args) send(symbol_or_proc, *args) when Proc instance_exec(*args, &symbol_or_proc) + else + symbol_or_proc end end @@ -25,7 +31,7 @@ def call_method_or_exec_proc(symbol_or_proc, *args) # or instance_exec a proc passing in the object as the first parameter. This # method wraps that pattern. # - # Calling with a Symbol: + # Calling with a String or Symbol: # # call_method_or_proc_on(@my_obj, :size) same as @my_obj.size # @@ -35,11 +41,11 @@ def call_method_or_exec_proc(symbol_or_proc, *args) # call_method_or_proc_on(@my_obj, proc) # # By default, the Proc will be instance_exec'd within self. If you would rather - # not instance exec, but just call the Proc, then pass along `:exec => false` in + # not instance exec, but just call the Proc, then pass along `exec: false` in # the options hash. # # proc = Proc.new{|s| s.size } - # call_method_or_proc_on(@my_obj, proc, :exec => false) + # call_method_or_proc_on(@my_obj, proc, exec: false) # # You can pass along any necessary arguments to the method / Proc as arguments. For # example: @@ -47,19 +53,21 @@ def call_method_or_exec_proc(symbol_or_proc, *args) # call_method_or_proc_on(@my_obj, :find, 1) #=> @my_obj.find(1) # def call_method_or_proc_on(receiver, *args) - options = { :exec => true }.merge(args.extract_options!) + options = { exec: true }.merge(args.extract_options!) symbol_or_proc = args.shift case symbol_or_proc when Symbol, String - receiver.send(symbol_or_proc.to_sym, *args) + receiver.public_send symbol_or_proc.to_sym, *args when Proc if options[:exec] instance_exec(receiver, *args, &symbol_or_proc) else symbol_or_proc.call(receiver, *args) end + else + symbol_or_proc end end @@ -71,21 +79,21 @@ def render_or_call_method_or_proc_on(obj, string_symbol_or_proc, options = {}) case string_symbol_or_proc when Symbol, Proc call_method_or_proc_on(obj, string_symbol_or_proc, options) - when String + else string_symbol_or_proc end end - # This method is different from the others in that it calls `instance_exec` on the reciever, - # passing it the proc. This evaluates the proc in the context of the reciever, thus changing + # This method is different from the others in that it calls `instance_exec` on the receiver, + # passing it the proc. This evaluates the proc in the context of the receiver, thus changing # what `self` means inside the proc. def render_in_context(context, obj, *args) - context ||= self # default to `self` + context = self if context.nil? # default to `self` only when nil case obj when Proc - context.instance_exec *args, &obj + context.instance_exec(*args, &obj) when Symbol - context.send obj, *args + context.public_send obj, *args else obj end diff --git a/lib/active_admin/view_helpers/sidebar_helper.rb b/lib/active_admin/view_helpers/sidebar_helper.rb deleted file mode 100644 index 866a3a6a4fb..00000000000 --- a/lib/active_admin/view_helpers/sidebar_helper.rb +++ /dev/null @@ -1,15 +0,0 @@ -module ActiveAdmin - module ViewHelpers - module SidebarHelper - - def skip_sidebar! - @skip_sidebar = true - end - - def skip_sidebar? - @skip_sidebar == true - end - - end - end -end diff --git a/lib/active_admin/view_helpers/title_helper.rb b/lib/active_admin/view_helpers/title_helper.rb deleted file mode 100644 index 1cc737d9845..00000000000 --- a/lib/active_admin/view_helpers/title_helper.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ActiveAdmin - module ViewHelpers - module TitleHelper - - def title(_title) - @page_title = _title - end - - end - end -end diff --git a/lib/active_admin/view_helpers/view_factory_helper.rb b/lib/active_admin/view_helpers/view_factory_helper.rb deleted file mode 100644 index 06b85ed9b88..00000000000 --- a/lib/active_admin/view_helpers/view_factory_helper.rb +++ /dev/null @@ -1,11 +0,0 @@ -module ActiveAdmin - module ViewHelpers - module ViewFactoryHelper - - def view_factory - active_admin_namespace.view_factory - end - - end - end -end diff --git a/lib/active_admin/views.rb b/lib/active_admin/views.rb index daae1a56611..aa3851a8831 100644 --- a/lib/active_admin/views.rb +++ b/lib/active_admin/views.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true module ActiveAdmin module Views # Loads all the classes in views/*.rb - Dir[File.expand_path('../views', __FILE__) + "/**/*.rb"].sort.each{ |f| require f } + Dir[File.expand_path("views", __dir__) + "/**/*.rb"].sort.each { |f| require f } end end diff --git a/lib/active_admin/views/action_items.rb b/lib/active_admin/views/action_items.rb deleted file mode 100644 index 2f87a25b011..00000000000 --- a/lib/active_admin/views/action_items.rb +++ /dev/null @@ -1,17 +0,0 @@ -module ActiveAdmin - module Views - - class ActionItems < ActiveAdmin::Component - - def build(action_items) - action_items.each do |action_item| - span :class => "action_item" do - instance_eval(&action_item.block) - end - end - end - - end - - end -end diff --git a/lib/active_admin/views/components/action_list_popover.rb b/lib/active_admin/views/components/action_list_popover.rb deleted file mode 100644 index 449c5e472fb..00000000000 --- a/lib/active_admin/views/components/action_list_popover.rb +++ /dev/null @@ -1,29 +0,0 @@ -require 'active_admin/views/components/popover' - -module ActiveAdmin - module Views - # Build an ActionListPopover - class ActionListPopover < ActiveAdmin::Views::Popover - builder_method :action_list_popover - - - def build(*args, &block) - @contents = ul :class => "popover_contents" - - options = args.extract_options! - - super(options) - end - - def action(title, url, *args) - options = args.extract_options! - within @contents do - li do - text_node link_to( title, url, options ) - end - end - end - - end - end -end diff --git a/lib/active_admin/views/components/active_admin_form.rb b/lib/active_admin/views/components/active_admin_form.rb new file mode 100644 index 00000000000..27c91e951ff --- /dev/null +++ b/lib/active_admin/views/components/active_admin_form.rb @@ -0,0 +1,156 @@ +# frozen_string_literal: true +module ActiveAdmin + module Views + class FormtasticProxy < ::Arbre::Rails::Forms::FormBuilderProxy + def split_string_on(string, match) + return "" unless string && match + part_1 = string.split(Regexp.new("#{match}\\z")).first + [part_1, match] + end + + def opening_tag + @opening_tag || "" + end + + def closing_tag + @closing_tag || "" + end + + def to_s + opening_tag + children.to_s + closing_tag + end + end + + class ActiveAdminForm < FormtasticProxy + builder_method :active_admin_form_for + + def build(resource, options = {}, &block) + @resource = resource + options = options.deep_dup + options[:builder] ||= ActiveAdmin::FormBuilder + form_string = helpers.semantic_form_for(resource, options) do |f| + @form_builder = f + end + + @opening_tag, @closing_tag = split_string_on(form_string, "") + instance_eval(&block) if block + + # Rails sets multipart automatically if a file field is present, + # but the form tag has already been rendered before the block eval. + if multipart? && !@opening_tag.include?('multipart') + @opening_tag.sub!(/
    " + @closing_tag = "
" + super(*args, &block) + end + end + + class HasManyProxy < FormtasticProxy + def build(form_builder, *args, &block) + text_node form_builder.has_many(*args, &block) + end + end + end +end diff --git a/lib/active_admin/views/components/attributes_table.rb b/lib/active_admin/views/components/attributes_table.rb index b43cb9e9b4b..78af5704ff1 100644 --- a/lib/active_admin/views/components/attributes_table.rb +++ b/lib/active_admin/views/components/attributes_table.rb @@ -1,32 +1,40 @@ +# frozen_string_literal: true module ActiveAdmin module Views class AttributesTable < ActiveAdmin::Component builder_method :attributes_table_for - attr_reader :resource - - def build(record, *attrs) - @record = record - super(:for => @record) + def build(obj, *attrs) + @collection = Array.wrap(obj) + @resource_class = @collection.first.class + options = {} + options[:for] = @collection.first if single_record? + super(options) + add_class "attributes-table" @table = table + build_colgroups rows(*attrs) end def rows(*attrs) - attrs.each {|attr| row(attr) } + attrs.each { |attr| row(attr) } end def row(*args, &block) - title = args[0] + title = args[0] + data = args[1] || args[0] options = args.extract_options! - options[:class] ||= :row + options["data-row"] = title.to_s.parameterize(separator: "_") if title.present? + @table << tr(options) do th do header_content_for(title) end - td do - content_for(block || title) + @collection.each do |record| + td do + content_for(record, block || data) + end end end end @@ -34,35 +42,50 @@ def row(*args, &block) protected def default_id_for_prefix - 'attributes_table' + "attributes_table" + end + + # Build Colgroups + # + # Colgroups are only necessary for a collection of records; not + # a single record. + def build_colgroups + return if single_record? + reset_cycle(self.class.to_s) + within @table do + col # column for row headers + @collection.each do |record| + col(id: dom_id_for(record)) + end + end end def header_content_for(attr) - if @record.class.respond_to?(:human_attribute_name) - @record.class.human_attribute_name(attr, :default => attr.to_s.titleize) + if attr.is_a?(Symbol) + default = attr.to_s.titleize + if @resource_class.respond_to?(:human_attribute_name) + @resource_class.human_attribute_name(attr, default: default) + else + default + end else - attr.to_s.titleize + attr.to_s end end def empty_value - span I18n.t('active_admin.empty'), :class => "empty" + span I18n.t("active_admin.empty"), class: "attributes-table-empty-value" end - def content_for(attr) - previous = current_arbre_element.to_s - value = pretty_format find_attr_value attr - value.blank? && previous == current_arbre_element.to_s ? empty_value : value + def content_for(record, attr) + value = helpers.format_attribute record, attr + value.blank? && current_arbre_element.children.to_s.empty? ? empty_value : value + # Don't add the same Arbre twice, while still allowing format_attribute to call status_tag + current_arbre_element << value unless current_arbre_element.children.include? value end - def find_attr_value(attr) - if attr.is_a?(Proc) - attr.call(@record) - elsif attr.to_s[/\A(.+)_id\z/] && @record.respond_to?($1.to_sym) - @record.send($1.to_sym) - else - @record.send(attr.to_sym) - end + def single_record? + @single_record ||= @collection.size == 1 end end diff --git a/lib/active_admin/views/components/blank_slate.rb b/lib/active_admin/views/components/blank_slate.rb deleted file mode 100644 index 12ebe1adcb7..00000000000 --- a/lib/active_admin/views/components/blank_slate.rb +++ /dev/null @@ -1,17 +0,0 @@ -module ActiveAdmin - module Views - # Build a Blank Slate - class BlankSlate < ActiveAdmin::Component - builder_method :blank_slate - - def default_class_name - 'blank_slate_container' - end - - def build(content) - super(span(content.html_safe, :class => "blank_slate")) - end - - end - end -end diff --git a/lib/active_admin/views/components/columns.rb b/lib/active_admin/views/components/columns.rb deleted file mode 100644 index 13eb1049b98..00000000000 --- a/lib/active_admin/views/components/columns.rb +++ /dev/null @@ -1,150 +0,0 @@ -module ActiveAdmin - module Views - - # = Columns Component - # - # The Columns component allows you draw content into scalable columns. All - # you need to do is define the number of columns and the component will - # take care of the rest. - # - # == Simple Columns - # - # To display columns, use the #columns method. Within the block, call the - # #column method to create a new column. - # - # To createa a two column layout: - # - # colums do - # column do - # span "Column # 1 - # end - # column do - # span "Column # 2 - # end - # end - # - # - # == Multiple Span Columns - # - # To make a column span multiple, pass the :span option to the column method: - # - # colums do - # column :span => 2 do - # span "Column # 1 - # end - # column do - # span "Column # 2 - # end - # end - # - # By default, each column spans 1 column. So the above layout would have 2 columns, - # the first being 2 time bigger than the second. - # - # - # == Max and Mix Column Sizes - # - # Active Admin is a fluid width layout, which means that columns are all defined - # using percentages. Sometimes this can cause issues if you don't want a column - # to shrink or expand past a certain point. - # - # To overcome this, columns include a :max_width and :min_width option. - # - # colums do - # column :max_width => "200px", :min_width => "100px" do - # span "Column # 1 - # end - # column do - # span "Column # 2 - # end - # end - # - # Now the first column will not grow bigger than 200px and will not shrink smaller - # than 100px. - class Columns < ActiveAdmin::Component - builder_method :columns - - - # For documentation, please take a look at Column#build - def column(*args, &block) - insert_tag Column, *args, &block - end - - # Override add child to set widths - def add_child(*) - super - calculate_columns! - end - - protected - - # Override the closing tag to include a clear - def closing_tag - "
" + super - end - - def margin_size - 2 - end - - # Calculate our columns sizes and margins - def calculate_columns! - span_count = columns_span_count - columns_count = children.size - - all_margins_width = margin_size * (span_count - 1) - column_width = (100.00 - all_margins_width) / span_count - - children.each_with_index do |col, i| - is_last_column = i == (columns_count - 1) - col.set_column_styles(column_width, margin_size, is_last_column) - end - end - - def columns_span_count - count = 0 - children.each {|column| count += column.span_size } - - count - end - - end - - class Column < ActiveAdmin::Component - - attr_accessor :span_size, :max_width, :min_width - - # @param [Hash] options An options hash for the column - # - # @options options [Integer] :span The columns this column should span - def build(options = {}) - options = options.dup - @span_size = options.delete(:span) || 1 - @max_width = options.delete(:max_width) - @min_width = options.delete(:min_width) - - super(options) - end - - def set_column_styles(column_width, margin_width, is_last_column = false) - column_with_span_width = (span_size * column_width) + ((span_size - 1) * margin_width) - - styles = [] - - styles << "width: #{column_with_span_width}%;" - - if max_width - styles << "max-width: #{max_width};" - end - - if min_width - styles << "min-width: #{min_width};" - end - - styles << "margin-right: #{margin_width}%;" unless is_last_column - - set_attribute :style, styles.join(" ") - end - - end - end -end diff --git a/lib/active_admin/views/components/dropdown_menu.rb b/lib/active_admin/views/components/dropdown_menu.rb deleted file mode 100644 index 00491e96104..00000000000 --- a/lib/active_admin/views/components/dropdown_menu.rb +++ /dev/null @@ -1,73 +0,0 @@ -require 'active_admin/views/components/popover' - -module ActiveAdmin - module Views - - # Action List - A button with a drop down menu of links - # - # Creating a new action list: - # - # dropdown_menu "Administration" do - # item "Edit Details", edit_details_path - # item "Edit My Account", edit_my_acccount_path - # end - # - # This will create a button with the label "Administration" and - # a drop down once clicked with 2 options. - # - class DropdownMenu < ActiveAdmin::Component - builder_method :dropdown_menu - - # Build a new action list - # - # @param [String] name The name to display in the button - # - # @param [Hash] options A set of options that get passed along to - # to the parent dom element. - def build(name, options = {}) - options = options.dup - - # Easily set options for the button or menu - button_options = options.delete(:button) || {} - menu_options = options.delete(:menu) || {} - - @button = build_button(name, button_options) - @menu = build_menu(menu_options) - - super(options) - end - - def item(*args) - within @menu do - li link_to(*args) - end - end - - private - - def build_button(name, button_options) - button_options[:class] ||= "" - button_options[:class] << " dropdown_menu_button" - - button_options[:href] = "#" - - a name, button_options - end - - def build_menu(options) - options[:class] ||= "" - options[:class] << " dropdown_menu_list" - - menu_list = nil - - div :class => "dropdown_menu_list_wrapper", :style => "display:none;" do - menu_list = ul(options) - end - - menu_list - end - - end - - end -end diff --git a/lib/active_admin/views/components/index_list.rb b/lib/active_admin/views/components/index_list.rb index bf6d724c6d3..972ba5e84c5 100644 --- a/lib/active_admin/views/components/index_list.rb +++ b/lib/active_admin/views/components/index_list.rb @@ -1,27 +1,18 @@ -require 'active_admin/helpers/collection' - +# frozen_string_literal: true module ActiveAdmin - module Views - + module Views # Renders a collection of index views available to the resource # as a list with a separator - class IndexList < ActiveAdmin::Component - builder_method :index_list_renderer - - include ::ActiveAdmin::Helpers::Collection - - def default_class_name - "indexes table_tools_segmented_control" - end - + class IndexList < ActiveAdmin::Component def tag_name - 'ul' + "div" end # Builds the links for presenting different index views to the user # # @param [Array] index_classes The class constants that represent index page presenters def build(index_classes) + add_class "index-button-group index-list" unless current_filter_search_empty? index_classes.each do |index_class| build_index_list(index_class) @@ -35,17 +26,18 @@ def build(index_classes) # # @param [Class] index_class The class on which to build the link and html classes def build_index_list(index_class) - li :class => classes_for_index(index_class) do - a :href => url_for(params.merge(:as => index_class.index_name.to_sym)), :class => "table_tools_button" do - name = index_class.index_name - I18n.t("active_admin.index_list.#{name}", :default => name.to_s.titleize) - end + params = request.query_parameters.except :page, :commit, :format + url_with_params = url_for(**params.merge(as: index_class.index_name.to_sym).symbolize_keys) + + a href: url_with_params, class: classes_for_index(index_class) do + name = index_class.index_name + I18n.t("active_admin.index_list.#{name}", default: name.to_s.titleize) end end def classes_for_index(index_class) - classes = ["index"] - classes << "selected" if current_index?(index_class) + classes = ["index-button"] + classes << "index-button-selected" if current_index?(index_class) classes.join(" ") end @@ -58,11 +50,8 @@ def current_index?(index_class) end def current_filter_search_empty? - params.include?(:q) && collection_is_empty? + params.include?(:q) && collection_empty? end - - end - end + end + end end - - diff --git a/lib/active_admin/views/components/paginated_collection.rb b/lib/active_admin/views/components/paginated_collection.rb index de6cd1d8d54..3169d537628 100644 --- a/lib/active_admin/views/components/paginated_collection.rb +++ b/lib/active_admin/views/components/paginated_collection.rb @@ -1,13 +1,11 @@ -require 'active_admin/helpers/collection' - +# frozen_string_literal: true module ActiveAdmin module Views - # Wraps the content with pagination and available formats. # # *Example:* # - # paginated_collection collection, :entry_name => "Post" do + # paginated_collection collection, entry_name: "Post" do # div do # h2 "Inside the # end @@ -17,8 +15,8 @@ module Views # posts in one of the following formats: # # * "No Posts found" - # * "Displaying all 10 Posts" - # * "Displaying Posts 1 - 30 of 31 in total" + # * "Showing all 10 Posts" + # * "Showing Posts 1 - 30 of 31 in total" # # It will also generate pagination links. # @@ -32,19 +30,23 @@ class PaginatedCollection < ActiveAdmin::Component # collection => A paginated collection from kaminari # options => These options will be passed to `page_entries_info` # entry_name => The name to display for this resource collection + # params => Extra parameters for pagination (e.g. { anchor: 'details' }) # param_name => Parameter name for page number in the links (:page by default) # download_links => Download links override (false or [:csv, :pdf]) # def build(collection, options = {}) - @collection = collection - @param_name = options.delete(:param_name) + @collection = collection + @params = options.delete(:params) + @param_name = options.delete(:param_name) @download_links = options.delete(:download_links) + @display_total = options.delete(:pagination_total) { true } + @per_page = options.delete(:per_page) - unless collection.respond_to?(:num_pages) + unless @collection.respond_to?(:total_pages) raise(StandardError, "Collection is not a paginated scope. Set collection.page(params[:page]).per(10) before calling :paginated_collection.") end - - @contents = div(:class => "paginated_collection_contents") + add_class "paginated-collection" + @contents = div(class: "paginated-collection-contents") build_pagination_with_formats(options) @built = true end @@ -61,56 +63,97 @@ def add_child(*args, &block) protected def build_pagination_with_formats(options) - div :id => "index_footer" do + div class: "paginated-collection-pagination" do + div page_entries_info(options).html_safe, class: "pagination-information" build_pagination - div(page_entries_info(options).html_safe, :class => "pagination_information") - - if @download_links.is_a?(Array) && !@download_links.empty? - build_download_format_links @download_links - else - build_download_format_links unless @download_links == false + end + formats = build_download_formats @download_links + if @per_page.is_a?(Array) || formats.any? + div class: "paginated-collection-footer" do + build_per_page_select if @per_page.is_a?(Array) + render("active_admin/shared/download_format_links", formats: formats) if formats.any? end + end + end + def build_per_page_select + div do + text_node I18n.t("active_admin.pagination.per_page") + select class: "pagination-per-page" do + @per_page.each do |per_page| + option( + per_page, + value: per_page, + selected: @collection.limit_value == per_page ? "selected" : nil + ) + end + end end end def build_pagination - options = request.query_parameters.except(:commit, :format) + options = { views_prefix: :active_admin, outer_window: 1, window: 2 } + options[:params] = @params if @params options[:param_name] = @param_name if @param_name - text_node paginate(collection, options.symbolize_keys) - end + if !@display_total + # The #paginate method in kaminari will query the resource with a + # count(*) to determine how many pages there should be unless + # you pass in the :total_pages option. We issue a query to determine + # if there is another page or not, but the limit/offset make this + # query fast. + offset_scope = @collection.offset(@collection.current_page * @collection.limit_value) + # Support array collections. Kaminari::PaginatableArray does not respond to except + offset_scope = offset_scope.except(:select, :order) if offset_scope.respond_to?(:except) + offset = offset_scope.limit(1).count + options[:total_pages] = @collection.current_page + offset + options[:right] = 0 + end - include ::ActiveAdmin::Helpers::Collection - include ::ActiveAdmin::ViewHelpers::DownloadFormatLinksHelper + text_node paginate @collection, **options + end # modified from will_paginate def page_entries_info(options = {}) if options[:entry_name] - entry_name = options[:entry_name] + entry_name = options[:entry_name] entries_name = options[:entries_name] || entry_name.pluralize - elsif collection_is_empty? - entry_name = I18n.t "active_admin.pagination.entry", :count => 1, :default => 'entry' - entries_name = I18n.t "active_admin.pagination.entry", :count => 2, :default => 'entries' + elsif collection_empty?(@collection) + entry_name = I18n.t "active_admin.pagination.entry", count: 1, default: "entry" + entries_name = I18n.t "active_admin.pagination.entry", count: 2, default: "entries" else - key = "activerecord.models." + collection.first.class.model_name.i18n_key.to_s - entry_name = I18n.translate key, :count => 1, :default => collection.first.class.name.underscore.sub('_', ' ') - entries_name = I18n.translate key, :count => collection.size, :default => entry_name.pluralize + key = "activerecord.models." + @collection.first.class.model_name.i18n_key.to_s + + entry_name = I18n.translate key, count: 1, default: @collection.first.class.name.underscore.sub("_", " ") + entries_name = I18n.translate key, count: @collection.size, default: entry_name.pluralize end - if collection.num_pages < 2 - case collection_size - when 0; I18n.t('active_admin.pagination.empty', :model => entries_name) - when 1; I18n.t('active_admin.pagination.one', :model => entry_name) - else; I18n.t('active_admin.pagination.one_page', :model => entries_name, :n => collection.total_count) + if @display_total + if @collection.total_pages < 2 + case collection_size(@collection) + when 0; I18n.t("active_admin.pagination.empty", model: entries_name) + when 1; I18n.t("active_admin.pagination.one", model: entry_name) + else; I18n.t("active_admin.pagination.one_page", model: entries_name, n: @collection.total_count) + end + else + offset = (@collection.current_page - 1) * @collection.limit_value + total = @collection.total_count + I18n.t "active_admin.pagination.multiple", + model: entries_name, + total: total, + from: offset + 1, + to: offset + collection_size(@collection) end else - offset = (collection.current_page - 1) * collection.limit_value - total = collection.total_count - I18n.t('active_admin.pagination.multiple', :model => entries_name, :from => offset + 1, :to => offset + collection_size, :total => total) + # Do not display total count, in order to prevent a `SELECT count(*)`. + # To do so we must not call `@collection.total_pages` + offset = (@collection.current_page - 1) * @collection.limit_value + I18n.t "active_admin.pagination.multiple_without_total", + model: entries_name, + from: offset + 1, + to: offset + collection_size(@collection) end end - end end end diff --git a/lib/active_admin/views/components/panel.rb b/lib/active_admin/views/components/panel.rb index 9da02aded9d..81d8ab8e6fe 100644 --- a/lib/active_admin/views/components/panel.rb +++ b/lib/active_admin/views/components/panel.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin module Views @@ -5,12 +6,10 @@ class Panel < ActiveAdmin::Component builder_method :panel def build(title, attributes = {}) - icon_name = attributes.delete(:icon) - icn = icon_name ? icon(icon_name) : "" super(attributes) add_class "panel" - @title = h3(icn + title.to_s) - @contents = div(:class => "panel_contents") + @title = h3(title.to_s, class: "panel-title") + @contents = div(class: "panel-body") end def add_child(child) @@ -23,12 +22,10 @@ def add_child(child) # Override children? to only report children when the panel's # contents have been added to. This ensures that the panel - # correcly appends string values, etc. + # correctly appends string values, etc. def children? @contents.children? end - end - end end diff --git a/lib/active_admin/views/components/popover.rb b/lib/active_admin/views/components/popover.rb deleted file mode 100644 index b0bb147cbac..00000000000 --- a/lib/active_admin/views/components/popover.rb +++ /dev/null @@ -1,27 +0,0 @@ -module ActiveAdmin - module Views - - class Popover < ActiveAdmin::Component - builder_method :popover - - def build(options = {}, &block) - options = options.dup - contents_root_tag = options.delete(:contents_root_tag) || :div - options[:style] = "display: none" - - super(options) - - @contents_root = send(contents_root_tag, :class => "popover_contents") - end - - def add_child(child) - if @contents_root - @contents_root << child - else - super - end - end - - end - end -end diff --git a/lib/active_admin/views/components/scopes.rb b/lib/active_admin/views/components/scopes.rb index 21f6e6d5346..c7f89ced7b5 100644 --- a/lib/active_admin/views/components/scopes.rb +++ b/lib/active_admin/views/components/scopes.rb @@ -1,50 +1,50 @@ -require 'active_admin/helpers/collection' -require 'active_admin/view_helpers/method_or_proc_helper' +# frozen_string_literal: true +require_relative "../../async_count" +require_relative "../../view_helpers/method_or_proc_helper" module ActiveAdmin module Views - # Renders a collection of ActiveAdmin::Scope objects as a - # simple list with a seperator + # simple list with a separator class Scopes < ActiveAdmin::Component - builder_method :scopes_renderer - include ActiveAdmin::ScopeChain - include ::ActiveAdmin::Helpers::Collection - - - def default_class_name - "scopes table_tools_segmented_control" - end def tag_name - 'ul' + "div" end def build(scopes, options = {}) - scopes.each do |scope| - build_scope(scope, options) if call_method_or_proc_on(self, scope.display_if_block) + super({ role: "toolbar" }) + add_class "scopes" + prepare_async_counts(scopes, options) + + scopes.group_by(&:group).each do |group, group_scopes| + div class: "index-button-group", role: "group", data: { "group": group_name(group) } do + group_scopes.each do |scope| + build_scope(scope, options) if display_scope?(scope) + end + + nil + end end end protected def build_scope(scope, options) - li :class => classes_for_scope(scope) do - scope_name = I18n.t("active_admin.scopes.#{scope.id}", :default => scope.name) - - a :href => url_for(params.merge(:scope => scope.id, :page => 1)), :class => "table_tools_button" do - text_node scope_name - span :class => 'count' do - "(#{get_scope_count(scope)})" - end if options[:scope_count] && scope.show_count + params = request.query_parameters.except :page, :scope, :commit, :format + + a href: url_for(scope: scope.id, params: params), class: classes_for_scope(scope) do + text_node scope_name(scope) + if options[:scope_count] && scope.show_count + span get_scope_count(scope), class: "scopes-count" end end end def classes_for_scope(scope) - classes = ["scope", scope.id] - classes << "selected" if current_scope?(scope) + classes = ["index-button"] + classes << "index-button-selected" if current_scope?(scope) classes.join(" ") end @@ -58,7 +58,30 @@ def current_scope?(scope) # Return the count for the scope passed in. def get_scope_count(scope) - collection_size(scope_chain(scope, collection_before_scope)) + chained = @async_counts[scope] || scope_chain(scope, collection_before_scope) + + collection_size(chained) + end + + def group_name(group) + group.present? ? group : "default" + end + + private + + def display_scope?(scope) + call_method_or_exec_proc(scope.display_if_block) + end + + def prepare_async_counts(scopes, options) + @async_counts = if options[:scope_count] + scopes + .select(&:async_count?) + .select { |scope| display_scope?(scope) } + .index_with { |scope| AsyncCount.new(scope_chain(scope, collection_before_scope)) } + else + {} + end end end end diff --git a/lib/active_admin/views/components/sidebar_section.rb b/lib/active_admin/views/components/sidebar_section.rb deleted file mode 100644 index 706fe892a01..00000000000 --- a/lib/active_admin/views/components/sidebar_section.rb +++ /dev/null @@ -1,28 +0,0 @@ -module ActiveAdmin - module Views - - class SidebarSection < Panel - builder_method :sidebar_section - - # Takes a ActiveAdmin::SidebarSection instance - def build(section) - @section = section - super(@section.title, :icon => @section.icon) - self.id = @section.id - build_sidebar_content - end - - protected - - def build_sidebar_content - if @section.block - rvalue = instance_eval(&@section.block) - self << rvalue if rvalue.is_a?(String) - else - render(@section.partial_name) - end - end - end - - end -end diff --git a/lib/active_admin/views/components/site_title.rb b/lib/active_admin/views/components/site_title.rb deleted file mode 100644 index b9c63b4a9fa..00000000000 --- a/lib/active_admin/views/components/site_title.rb +++ /dev/null @@ -1,55 +0,0 @@ -module ActiveAdmin - module Views - - class SiteTitle < Component - - def tag_name - 'h1' - end - - def build(namespace) - super(:id => "site_title") - @namespace = namespace - - if site_title_link? - text_node site_title_with_link - else - text_node site_title_content - end - end - - def site_title_link? - @namespace.site_title_link.present? - end - - def site_title_image? - @namespace.site_title_image.present? - end - - private - - def site_title_with_link - helpers.link_to(site_title_content, @namespace.site_title_link) - end - - def site_title_content - if site_title_image? - title_image - else - title_text - end - end - - def title_text - helpers.render_or_call_method_or_proc_on(self, @namespace.site_title) - end - - def title_image - path = helpers.render_or_call_method_or_proc_on(self, @namespace.site_title_image) - helpers.image_tag(path, :id => "site_title_image", :alt => @namespace.site_title) - end - - end - - end -end diff --git a/lib/active_admin/views/components/status_tag.rb b/lib/active_admin/views/components/status_tag.rb index c42e9123dbf..a16d6bb5093 100644 --- a/lib/active_admin/views/components/status_tag.rb +++ b/lib/active_admin/views/components/status_tag.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveAdmin module Views # Build a StatusTag @@ -5,51 +6,73 @@ class StatusTag < ActiveAdmin::Component builder_method :status_tag def tag_name - 'span' + "span" end - def default_class_name - 'status_tag' - end - - # @method status_tag(status, type = nil, options = {}) + # @overload status_tag(status, options = {}) + # @param [String] status the status to display. + # @param [Hash] options + # @option options [String] :class to override the default class + # @option options [String] :id to override the default id + # @option options [String] :label to override the default label + # @return [ActiveAdmin::Views::StatusTag] # - # @param [String] status the status to display. One of the span classes will be an underscored version of the status. - # @param [Symbol] type type of status. Will become a class of the span. ActiveAdmin provide style for :ok, :warning and :error. - # @param [Hash] options such as :class, :id and :label to override the default label + # @example + # status_tag(true) + # # => Yes # - # @return [ActiveAdmin::Views::StatusTag] + # @example + # status_tag(false) + # # => No # - # Examples: - # status_tag('In Progress') - # # => In Progress + # @example + # status_tag(nil) + # # => Unknown # - # status_tag('active', :ok) - # # => Active + # @example + # status_tag('In Progress') + # # => In Progress # - # status_tag('active', :ok, :class => 'important', :id => 'status_123', :label => 'on') - # # => on + # @example + # status_tag('Active', class: 'important', id: 'status_123', label: 'on') + # # => on # - def build(*args) - options = args.extract_options! - status = args[0] - type = args[1] + def build(status, options = {}) label = options.delete(:label) classes = options.delete(:class) + boolean_status = convert_to_boolean_status(status) + status = boolean_status || status - content = label || status.titleize if status + if status + content = label || if s = status.to_s and s.present? + I18n.t "active_admin.status_tag.#{s.downcase}", default: s.titleize + end + end super(content, options) - - add_class(status_to_class(status)) if status - add_class(type.to_s) if type + add_class "status-tag" + set_attribute("data-status", convert_status(status)) if status add_class(classes) if classes end protected - def status_to_class(status) - status.titleize.gsub(/\s/, '').underscore + def convert_to_boolean_status(status) + case status + when true, "true" + "Yes" + when false, "false" + "No" + when nil + "Unset" + end + end + + def convert_status(status) + case status + when String, Symbol + status.to_s.titleize.delete(" ").underscore + end end end end diff --git a/lib/active_admin/views/components/table_for.rb b/lib/active_admin/views/components/table_for.rb index a19b4302046..cfcddcedd0a 100644 --- a/lib/active_admin/views/components/table_for.rb +++ b/lib/active_admin/views/components/table_for.rb @@ -1,25 +1,40 @@ +# frozen_string_literal: true module ActiveAdmin module Views class TableFor < Arbre::HTML::Table builder_method :table_for def tag_name - 'table' + "table" end - def build(collection, options = {}) + def build(obj, *attrs) + options = attrs.extract_options! @sortable = options.delete(:sortable) + @collection = obj.respond_to?(:each) && !obj.is_a?(Hash) ? obj : [obj] @resource_class = options.delete(:i18n) - @collection = collection + @resource_class ||= @collection.klass if @collection.respond_to? :klass + @columns = [] + @tbody_html = options.delete(:tbody_html) + @row_html = options.delete(:row_html) + # To be deprecated, please use row_html instead. + @row_class = options.delete(:row_class) + build_table super(options) + add_class "data-table" + columns(*attrs) + end + + def columns(*attrs) + attrs.each { |attr| column(attr) } end def column(*args, &block) options = default_options.merge(args.extract_options!) title = args[0] - data = args[1] || args[0] + data = args[1] || args[0] col = Column.new(title, data, @resource_class, options, &block) @columns << col @@ -30,20 +45,15 @@ def column(*args, &block) end # Add a table cell for each item - @collection.each_with_index do |item, i| - within @tbody.children[i] do - build_table_cell(col, item) + @collection.each_with_index do |resource, index| + within @tbody.children[index] do + build_table_cell col, resource end end end def sortable? - @sortable - end - - # Returns the columns to display based on the conditional block - def visible_columns - @visible_columns ||= @columns.select{|col| col.display_column? } + !!@sortable end protected @@ -60,36 +70,45 @@ def build_table_head end def build_table_header(col) - classes = Arbre::HTML::ClassList.new sort_key = sortable? && col.sortable? && col.sort_key + params = request.query_parameters.except :page, :order, :commit, :format - classes << 'sortable' if sort_key - classes << "sorted-#{current_sort[1]}" if sort_key && current_sort[0] == sort_key - classes << col.html_class + attributes = { + class: col.html_class, + "data-column": col.title_id.presence, + "data-sortable": (sort_key.present?) ? "" : nil, + "data-sort-direction": (sort_key && current_sort[0] == sort_key) ? current_sort[1] : nil + } if sort_key - th :class => classes do - link_to(col.pretty_title, params.merge(:order => "#{sort_key}_#{order_for_sort_key(sort_key)}").except(:page)) + th(attributes) do + link_to params: params, order: "#{sort_key}_#{order_for_sort_key(sort_key)}" do + svg = ''.html_safe + + safe_join([col.pretty_title, svg]) + end end else - th(col.pretty_title, :class => classes) + th col.pretty_title, attributes end end def build_table_body - @tbody = tbody do + @tbody = tbody(**(@tbody_html || {})) do # Build enough rows for our collection - @collection.each{|elem| tr(:class => cycle('odd', 'even'), :id => dom_id(elem)) } + @collection.each do |elem| + html_options = @row_html&.call(elem) || {} + html_options.reverse_merge!(class: @row_class&.call(elem)) + tr(id: dom_id_for(elem), **html_options) + end end end - def build_table_cell(col, item) - td(:class => col.html_class) do - rvalue = call_method_or_proc_on(item, col.data, :exec => false) - if col.data.is_a?(Symbol) - rvalue = pretty_format(rvalue) - end - rvalue + def build_table_cell(col, resource) + td class: col.html_class, "data-column": col.title_id.presence do + html = helpers.format_attribute(resource, col.data) + # Don't add the same Arbre twice, while still allowing format_attribute to call status_tag + current_arbre_element << html unless current_arbre_element.children.include? html end end @@ -97,10 +116,14 @@ def build_table_cell(col, item) # current_sort[0] #=> sort_key # current_sort[1] #=> asc | desc def current_sort - @current_sort ||= if params[:order] && params[:order] =~ /^([\w\_\.]+)_(desc|asc)$/ - [$1,$2] - else - [] + @current_sort ||= begin + order_clause = active_admin_config.order_clause.new(active_admin_config, params[:order]) + + if order_clause.valid? + [order_clause.field, order_clause.order] + else + [] + end end end @@ -110,39 +133,37 @@ def current_sort # 'desc' it will return 'asc' def order_for_sort_key(sort_key) current_key, current_order = current_sort - return 'desc' unless current_key == sort_key - current_order == 'desc' ? 'asc' : 'desc' + return "desc" unless current_key == sort_key + current_order == "desc" ? "asc" : "desc" end def default_options { - :i18n => @resource_class + i18n: @resource_class } end class Column - attr_accessor :title, :data , :html_class + attr_accessor :title, :title_id, :data, :html_class def initialize(*args, &block) @options = args.extract_options! - @title = args[0] - @html_class = @options.delete(:class) || @title.to_s.downcase.underscore.gsub(/ +/,'_') - @data = args[1] || args[0] + @title_id = @title.to_s.parameterize(separator: "_") if @title.present? && !title.is_a?(Arbre::Element) + @html_class = @options.delete(:class) + @data = args[1] || args[0] @data = block if block @resource_class = args[2] end def sortable? - if @data.is_a?(Proc) - [String, Symbol].include?(@options[:sortable].class) - elsif @options.has_key?(:sortable) - @options[:sortable] - elsif @data.respond_to?(:to_sym) && @resource_class - !@resource_class.reflect_on_association(@data.to_sym) + if @options.has_key?(:sortable) + !!@options[:sortable] + elsif @resource_class.respond_to?(:column_names) + @resource_class.column_names.include?(sort_column_name) else - true + @title.present? end end @@ -155,36 +176,35 @@ def sortable? # # You can set the sort key by passing a string or symbol # to the sortable option: - # column :username, :sortable => 'other_column_to_sort_on' - # - # If you pass a block to be rendered for this column, the column - # will not be sortable unless you pass a string to sortable to - # sort the column on: - # - # column('Username', :sortable => 'login'){ @user.pretty_name } - # # => Sort key will be 'login' + # column :username, sortable: 'other_column_to_sort_on' # def sort_key # If boolean or nil, use the default sort key. - if @options[:sortable] == true || @options[:sortable] == false || @options[:sortable].nil? - @data.to_s + if @options[:sortable].nil? || @options[:sortable] == true || @options[:sortable] == false + sort_column_name else @options[:sortable].to_s end end def pretty_title - if @title.is_a?(Symbol) - default_title = @title.to_s.titleize - if @options[:i18n] && @options[:i18n].respond_to?(:human_attribute_name) - @title = @options[:i18n].human_attribute_name(@title, :default => default_title) + if @title.is_a? Symbol + default = @title.to_s.titleize + if @options[:i18n].respond_to? :human_attribute_name + @title = @options[:i18n].human_attribute_name @title, default: default else - default_title + default end else @title end end + + private + + def sort_column_name + @data.is_a?(Symbol) ? @data.to_s : @title.to_s + end end end end diff --git a/lib/active_admin/views/dashboard_section_renderer.rb b/lib/active_admin/views/dashboard_section_renderer.rb deleted file mode 100644 index 672f8f25d12..00000000000 --- a/lib/active_admin/views/dashboard_section_renderer.rb +++ /dev/null @@ -1,19 +0,0 @@ -module ActiveAdmin - module Views - class DashboardSection < ActiveAdmin::Views::Panel - - def build(section) - @section = section - super(title, :icon => @section.icon) - instance_eval &@section.block - end - - protected - - def title - I18n.t("active_admin.sections.#{@section.name.to_s}", :default => @section.name.to_s.titleize) - end - - end - end -end diff --git a/lib/active_admin/views/footer.rb b/lib/active_admin/views/footer.rb deleted file mode 100644 index 45a2f07af4e..00000000000 --- a/lib/active_admin/views/footer.rb +++ /dev/null @@ -1,20 +0,0 @@ -module ActiveAdmin - module Views - class Footer < Component - - def build - super :id => "footer" - powered_by_message - end - - private - - def powered_by_message - para I18n.t('active_admin.powered_by', - :active_admin => link_to("Active Admin", "http://www.activeadmin.info"), - :version => ActiveAdmin::VERSION).html_safe - end - - end - end -end diff --git a/lib/active_admin/views/header.rb b/lib/active_admin/views/header.rb deleted file mode 100644 index 8b336278966..00000000000 --- a/lib/active_admin/views/header.rb +++ /dev/null @@ -1,32 +0,0 @@ -module ActiveAdmin - module Views - class Header < Component - - def build(namespace, menu) - super(:id => "header") - - @namespace = namespace - @menu = menu - @utility_menu = @namespace.fetch_menu(:utility_navigation) - - build_site_title - build_global_navigation - build_utility_navigation - end - - - def build_site_title - insert_tag view_factory.site_title, @namespace - end - - def build_global_navigation - insert_tag view_factory.global_navigation, @menu, :class => 'header-item tabs' - end - - def build_utility_navigation - insert_tag view_factory.utility_navigation, @utility_menu, :id => "utility_nav", :class => 'header-item tabs' - end - - end - end -end diff --git a/lib/active_admin/views/index_as_block.rb b/lib/active_admin/views/index_as_block.rb deleted file mode 100644 index 8eab3291d66..00000000000 --- a/lib/active_admin/views/index_as_block.rb +++ /dev/null @@ -1,36 +0,0 @@ -module ActiveAdmin - module Views - - # = Index as a Block - # - # If you want to fully customize the display of your resources on the index - # screen, Index as a Block allows you to render a block of content for each - # resource. - # - # index :as => :block do |product| - # div :for => product do - # resource_selection_cell product - # h2 auto_link(product.title) - # div do - # simple_format product.description - # end - # end - # end - # - class IndexAsBlock < ActiveAdmin::Component - - def build(page_presenter, collection) - add_class "index" - resource_selection_toggle_panel if active_admin_config.batch_actions.any? - collection.each do |obj| - instance_exec(obj, &page_presenter.block) - end - end - - def self.index_name - "block" - end - - end - end -end diff --git a/lib/active_admin/views/index_as_blog.rb b/lib/active_admin/views/index_as_blog.rb deleted file mode 100644 index 2e50323d9a6..00000000000 --- a/lib/active_admin/views/index_as_blog.rb +++ /dev/null @@ -1,150 +0,0 @@ -module ActiveAdmin - module Views - - # = Index as Blog - # - # Render your index page as a set of posts. The post has two main options: - # title and body. - # - # index :as => :blog do - # title :my_title # Calls #my_title on each resource - # body :my_body # Calls #my_body on each resource - # end - # - # == Post Title - # - # The title is the content that will be rendered within a link to the - # resource. There are two main ways to set the content for the title - # - # First, you can pass in a method to be called on your - # resource. For example: - # - # index :as => :blog do - # title :a_method_to_call - # end - # - # This will result in the title of the post being the return value of - # Resource#a_method_to_call - # - # Second, you can pass a block to the tile option which will then be - # used as the contents fo the title. The resource being rendered - # is passed in to the block. For Example: - # - # index :as => :blog do - # title do |post| - # span post.title, :class => 'title' - # span post.created_at, :class => 'created_at' - # end - # end - # - # == Post Body - # - # The body is rendered underneath the title of each post. The same two - # style of options work as the Post Title above. - # - # Call a method on the resource as the body: - # - # index :as => :blog do - # title :my_title - # body :my_body # Return value of #my_body will be the body - # end - # - # Or, render a block as the body: - # - # index :as => :blog do - # title :my_title - # body do |post| - # div truncate(post.title) - # div :class => 'meta' do - # span "Post in #{post.categories.join(', ')}" - # end - # end - # end - # - class IndexAsBlog < ActiveAdmin::Component - - def build(page_presenter, collection) - @page_presenter = page_presenter - @collection = collection - - # Call the block passed in. This will set the - # title and body methods - instance_eval &page_presenter.block if page_presenter.block - - add_class "index" - build_posts - end - - # Setter method for the configuration of the title - def title(method = nil, &block) - if block_given? || method - @title = block_given? ? block : method - end - @title - end - - - # Setter method for the configuration of the body - # - def body(method = nil, &block) - if block_given? || method - @body = block_given? ? block : method - end - @body - end - - def self.index_name - "blog" - end - - private - - def build_posts - resource_selection_toggle_panel if active_admin_config.batch_actions.any? - @collection.each do |post| - build_post(post) - end - end - - def build_post(post) - div :for => post do - resource_selection_cell(post) if active_admin_config.batch_actions.any? - build_title(post) - build_body(post) - end - end - - def build_title(post) - if @title - h3 do - a(:href => resource_path(post)) do - render_method_on_post_or_call_proc post, @title - end - end - else - h3 do - auto_link(post) - end - end - end - - def build_body(post) - if @body - div :class => 'content' do - render_method_on_post_or_call_proc post, @body - end - end - end - - def render_method_on_post_or_call_proc(post, proc) - case proc - when String,Symbol - post.send proc - else - instance_exec post, &proc - end - end - - end # Posts - end -end diff --git a/lib/active_admin/views/index_as_grid.rb b/lib/active_admin/views/index_as_grid.rb deleted file mode 100644 index 37c7b888220..00000000000 --- a/lib/active_admin/views/index_as_grid.rb +++ /dev/null @@ -1,76 +0,0 @@ -module ActiveAdmin - module Views - - # = Index as a Grid - # - # Sometimes you want to display the index screen for a set of resources as a grid - # (possibly a grid of thumbnail images). To do so, use the :grid option for the - # index block. - # - # index :as => :grid do |product| - # link_to(image_tag(product.image_path), admin_products_path(product)) - # end - # - # The block is rendered within a cell in the grid once for each resource in the - # collection. The resource is passed into the block for you to use in the view. - # - # You can customize the number of colums that are rendered using the columns - # option: - # - # index :as => :grid, :columns => 5 do |product| - # link_to(image_tag(product.image_path), admin_products_path(product)) - # end - # - class IndexAsGrid < ActiveAdmin::Component - - def build(page_presenter, collection) - @page_presenter = page_presenter - @collection = collection - add_class "index" - build_table - end - - def number_of_columns - @page_presenter[:columns] || default_number_of_columns - end - - def self.index_name - "grid" - end - - protected - - def build_table - resource_selection_toggle_panel if active_admin_config.batch_actions.any? - table :class => "index_grid" do - collection.in_groups_of(number_of_columns).each do |group| - build_row(group) - end - end - end - - def build_row(group) - tr do - group.each do |item| - item ? build_item(item) : build_empty_cell - end - end - end - - def build_item(item) - td :for => item do - instance_exec(item, &@page_presenter.block) - end - end - - def build_empty_cell - td ' '.html_safe - end - - def default_number_of_columns - 3 - end - - end - end -end diff --git a/lib/active_admin/views/index_as_table.rb b/lib/active_admin/views/index_as_table.rb index 4e7ab394c7a..3b6a7ec1760 100644 --- a/lib/active_admin/views/index_as_table.rb +++ b/lib/active_admin/views/index_as_table.rb @@ -1,138 +1,240 @@ +# frozen_string_literal: true module ActiveAdmin module Views - # = Index as a Table + # # Index as a Table # # By default, the index page is a table with each of the models content columns and links to # show, edit and delete the object. There are many ways to customize what gets # displayed. # - # == Defining Columns + # ## Defining Columns # # To display an attribute or a method on a resource, simply pass a symbol into the # column method: # - # index do - # selectable_column - # column :title - # end + # ```ruby + # index do + # selectable_column + # column :title + # end + # ``` + # + # For association columns we make an educated guess on what to display by + # calling the following methods in the following order: + # + # ```ruby + # :display_name, :full_name, :name, :username, :login, :title, :email, :to_s + # ``` + # + # This can be customized in `config/initializers/active_admin.rb`. # # If the default title does not work for you, pass it as the first argument: # - # index do - # selectable_column - # column "My Custom Title", :title - # end + # ```ruby + # index do + # selectable_column + # column "My Custom Title", :title + # end + # ``` + # + # Sometimes that just isn't enough and you need to write some view-specific code. + # For example, say we wanted a "Title" column that links to the posts admin screen. # - # Sometimes calling methods just isn't enough and you need to write some view - # specific code. For example, say we wanted a colum called Title which holds a - # link to the posts admin screen. + # `column` accepts a block that will be rendered for each of the objects in the collection. + # The block is called once for each resource, which is passed as an argument to the block. # - # The column method accepts a block as an argument which will then be rendered - # within the context of the view for each of the objects in the collection. + # ```ruby + # index do + # selectable_column + # column "Title" do |post| + # link_to post.title, admin_post_path(post) + # end + # end + # ``` # - # index do - # selectable_column - # column "Title" do |post| - # link_to post.title, admin_post_path(post) - # end - # end + # ## Defining Actions # - # The block gets called once for each resource in the collection. The resource gets passed into - # the block as an argument. + # To setup links to View, Edit and Delete a resource, use the `actions` method: # - # To setup links to View, Edit and Delete a resource, use the default_actions method: + # ```ruby + # index do + # selectable_column + # column :title + # actions + # end + # ``` # - # index do - # selectable_column - # column :title - # default_actions - # end + # You can also append custom links to the default links: # - # Alternatively, you can create a column with custom links: + # ```ruby + # index do + # selectable_column + # column :title + # actions do |post| + # item "Preview", admin_preview_post_path(post), class: "preview-link" + # end + # end + # ``` # - # index do - # selectable_column - # column :title - # column "Actions" do |post| - # link_to "View", admin_post_path(post) - # end - # end + # Or forego the default links entirely: # + # ```ruby + # index do + # column :title + # actions defaults: false do |post| + # item "View", admin_post_path(post) + # end + # end + # ``` # - # == Sorting + # Or append custom action with custom html via arbre: + # + # ```ruby + # index do + # column :title + # actions do |post| + # a "View", href: admin_post_path(post) + # end + # end + # ``` + # + # ## Sorting # # When a column is generated from an Active Record attribute, the table is # sortable by default. If you are creating a custom column, you may need to give # Active Admin a hint for how to sort the table. # - # If a column is defined using a block, you must pass the key to turn on sorting. The key - # is the attribute which gets used to sort objects using Active Record. - # + # You can pass the key specifying the attribute which gets used to sort objects using Active Record. # By default, this is the column on the resource's table that the attribute corresponds to. # Otherwise, any attribute that the resource collection responds to can be used. # - # index do - # column "Title", :sortable => :title do |post| - # link_to post.title, admin_post_path(post) - # end - # end + # ```ruby + # index do + # column :title, sortable: :title do |post| + # link_to post.title, admin_post_path(post) + # end + # end + # ``` # - # You can also sort using an attribute on another table by passing the table name - # and the attribute separated by a dot: + # You can turn off sorting on any column by passing false: # - # index do - # column :title, :sortable => 'categories.name' - # end + # ```ruby + # index do + # column :title, sortable: false + # end + # ``` # - # You can turn off sorting on any column by passing false: + # It's also possible to sort by PostgreSQL's hstore column key. You should set `sortable` + # option to a `column->'key'` value: + # + # ```ruby + # index do + # column :keywords, sortable: "meta->'keywords'" + # end + # ``` + # + # ## Custom sorting + # + # It is also possible to use database specific expressions and options for sorting by column + # + # ```ruby + # order_by(:title) do |order_clause| + # if order_clause.order == 'desc' + # [order_clause.to_sql, 'NULLS LAST'].join(' ') + # else + # [order_clause.to_sql, 'NULLS FIRST'].join(' ') + # end + # end + # + # index do + # column :title + # end + # ``` # - # index do - # column :title, :sortable => false - # end + # ## Associated Sorting # - # == Showing and Hiding Columns + # You're normally able to sort columns alphabetically, but by default you + # can't sort by associated objects. Though with a few simple changes, you can. + # + # Assuming you're on the Books index page, and Book has_one Publisher: + # + # ```ruby + # controller do + # def scoped_collection + # super.includes :publisher # prevents N+1 queries to your database + # end + # end + # ``` + # + # You can also define associated objects to include outside of the + # `scoped_collection` method: + # + # ```ruby + # includes :publisher + # ``` + # + # Then it's simple to sort by any Publisher attribute from within the index table: + # + # ```ruby + # index do + # column :publisher, sortable: 'publishers.name' + # end + # ``` + # + # ## Showing and Hiding Columns # # The entire index block is rendered within the context of the view, so you can # easily do things that show or hide columns based on the current context. # # For example, if you were using CanCan: # - # index do - # column :title, :sortable => false - # if can? :manage, Post - # column :some_secret_data - # end - # end + # ```ruby + # index do + # column :title, sortable: false + # column :secret_data if can? :manage, Post + # end + # ``` # + # ## Custom tbody HTML attributes + # + # In order to add HTML attributes to the tbody use the `:tbody_html` option. + # + # ```ruby + # index tbody_html: { class: "my-class", data: { controller: 'stimulus-controller' } } do + # # columns + # end + # ``` + # + # ## Custom row HTML attributes + # + # In order to add HTML attributes to table rows, use a proc object in the `:row_html` option. + # + # ```ruby + # index row_html: ->elem { { class: ('active' if elem.active?), data: { 'element-id' => elem.id } } } do + # # columns + # end + # ``` class IndexAsTable < ActiveAdmin::Component - def build(page_presenter, collection) + add_class "index-as-table" table_options = { - :id => "index_table_#{active_admin_config.resource_name.plural}", - :sortable => true, - :class => "index_table index", - :i18n => active_admin_config.resource_class, - :paginator => page_presenter[:paginator] != false + id: "index_table_#{active_admin_config.resource_name.plural}", + sortable: true, + i18n: active_admin_config.resource_class, + paginator: page_presenter[:paginator] != false, + tbody_html: page_presenter[:tbody_html], + row_html: page_presenter[:row_html], + # To be deprecated, please use row_html instead. + row_class: page_presenter[:row_class] } - table_for collection, table_options do |t| - table_config_block = page_presenter.block || default_table - instance_exec(t, &table_config_block) - end - end - - def table_for(*args, &block) - insert_tag IndexTableFor, *args, &block - end - - def default_table - proc do - id_column - resource_class.content_columns.each do |col| - column col.name.to_sym + if page_presenter.block + insert_tag(IndexTableFor, collection, table_options) do |t| + instance_exec(t, &page_presenter.block) end - default_actions + else + render "index_as_table_default", table_options: table_options end end @@ -145,94 +247,93 @@ def self.index_name # methods for quickly displaying items on the index page # class IndexTableFor < ::ActiveAdmin::Views::TableFor - # Display a column for checkbox - def selectable_column + def selectable_column(**options) return unless active_admin_config.batch_actions.any? - column( resource_selection_toggle_cell, { :class => "selectable" } ) { |resource| resource_selection_cell( resource ) } + column resource_selection_toggle_cell, class: options[:class], sortable: false do |resource| + resource_selection_cell resource + end end # Display a column for the id - def id_column - column(resource_class.human_attribute_name(resource_class.primary_key), :sortable => resource_class.primary_key) do |resource| - link_to resource.id, resource_path(resource), :class => "resource_id_link" + def id_column(*args) + raise "#{resource_class.name} has no primary_key!" unless resource_class.primary_key + + options = args.extract_options! + title = args[0].presence || resource_class.human_attribute_name(resource_class.primary_key) + sortable = options.fetch(:sortable, resource_class.primary_key) + + column(title, sortable: sortable) do |resource| + if controller.action_methods.include?("show") + link_to resource.id, resource_path(resource) + elsif controller.action_methods.include?("edit") + link_to resource.id, edit_resource_path(resource) + else + resource.id + end end end # Add links to perform actions. # - # # Add default links. - # actions + # ```ruby + # # Add default links. + # actions + # + # # Add default links with a custom column title (empty by default). + # actions name: 'A title!' + # + # # Append some actions onto the end of the default actions. + # actions do |admin_user| + # item 'Grant Admin', grant_admin_admin_user_path(admin_user) + # item 'Grant User', grant_user_admin_user_path(admin_user) + # end + # + # # Append some actions onto the end of the default actions using arbre dsl. + # actions do |admin_user| + # a 'Grant Admin', href: grant_admin_admin_user_path(admin_user) + # end # - # # Append some actions onto the end of the default actions. - # actions do |admin_user| - # link_to 'Grant Admin', grant_admin_admin_user_path(admin_user) - # end + # # Custom actions without the defaults. + # actions defaults: false do |admin_user| + # item 'Grant Admin', grant_admin_admin_user_path(admin_user) + # end # - # # Custom actions without the defaults. - # actions :defaults => false do |admin_user| - # link_to 'Grant Admin', grant_admin_admin_user_path(admin_user) - # end + # ``` def actions(options = {}, &block) - options = { - :name => "", - :defaults => true - }.merge(options) - column options[:name] do |resource| - text_node default_actions(resource) if options[:defaults] - text_node instance_exec(resource, &block) if block_given? - end - end + name = options.delete(:name) { "" } + defaults = options.delete(:defaults) { true } - def default_actions(*args) - links = proc do |resource| - links = ''.html_safe - if controller.action_methods.include?('show') && authorized?(ActiveAdmin::Auth::READ, resource) - links << link_to(I18n.t('active_admin.view'), resource_path(resource), :class => "member_link view_link") - end - if controller.action_methods.include?('edit') && authorized?(ActiveAdmin::Auth::UPDATE, resource) - links << link_to(I18n.t('active_admin.edit'), edit_resource_path(resource), :class => "member_link edit_link") - end - if controller.action_methods.include?('destroy') && authorized?(ActiveAdmin::Auth::DESTROY, resource) - links << link_to(I18n.t('active_admin.delete'), resource_path(resource), :method => :delete, :data => {:confirm => I18n.t('active_admin.delete_confirmation')}, :class => "member_link delete_link") + column name, options do |resource| + insert_tag(TableActions, class: "data-table-resource-actions") do + render "index_table_actions_default", defaults_data(resource) if defaults + if block + block_result = instance_exec(resource, &block) + text_node block_result unless block_result.is_a? Arbre::Element + end end - links end + end - options = args.extract_options! - if options.present? || args.empty? - actions options - else - links.call(args.first) - end + private + + def defaults_data(resource) + localizer = ActiveAdmin::Localizers.resource(active_admin_config) + { + resource: resource, + view_label: localizer.t(:view), + edit_label: localizer.t(:edit), + delete_label: localizer.t(:delete), + delete_confirmation_text: localizer.t(:delete_confirmation) + } end - # Display A Status Tag Column - # - # index do |i| - # i.status_tag :state - # end - # - # index do |i| - # i.status_tag "State", :status_name - # end - # - # index do |i| - # i.status_tag do |post| - # post.published? ? 'published' : 'draft' - # end - # end - # - def status_tag(*args, &block) - col = Column.new(*args, &block) - data = col.data - col.data = proc do |resource| - status_tag call_method_or_proc_on(resource, data) + class TableActions < ActiveAdmin::Component + def item *args, **kwargs + text_node link_to(*args, **kwargs) end - add_column col end - end # TableBuilder - - end # Table + end # IndexTableFor + end end end diff --git a/lib/active_admin/views/pages/base.rb b/lib/active_admin/views/pages/base.rb deleted file mode 100644 index 26b33930bd3..00000000000 --- a/lib/active_admin/views/pages/base.rb +++ /dev/null @@ -1,134 +0,0 @@ -module ActiveAdmin - module Views - module Pages - class Base < Arbre::HTML::Document - - def build(*args) - super - add_classes_to_body - build_active_admin_head - build_page - end - - private - - def add_classes_to_body - @body.add_class(params[:action]) - @body.add_class(params[:controller].gsub('/', '_')) - @body.add_class("active_admin") - @body.add_class("logged_in") - @body.add_class(active_admin_namespace.name.to_s + "_namespace") - end - - def build_active_admin_head - within @head do - insert_tag Arbre::HTML::Title, [title, render_or_call_method_or_proc_on(self, active_admin_application.site_title)].join(" | ") - active_admin_application.stylesheets.each do |style, options| - text_node stylesheet_link_tag(style, options).html_safe - end - - active_admin_application.javascripts.each do |path| - text_node(javascript_include_tag(path)) - end - text_node csrf_meta_tag - end - end - - def build_page - within @body do - div :id => "wrapper" do - build_header - build_title_bar - build_page_content - build_footer - end - end - end - - def build_header - insert_tag view_factory.header, active_admin_namespace, current_menu - end - - def build_title_bar - insert_tag view_factory.title_bar, title, action_items_for_action - end - - - def build_page_content - build_flash_messages - div :id => "active_admin_content", :class => (skip_sidebar? ? "without_sidebar" : "with_sidebar") do - build_main_content_wrapper - build_sidebar unless skip_sidebar? - end - end - - def build_flash_messages - if active_admin_flash_messages.any? - div :class => 'flashes' do - active_admin_flash_messages.each do |type, message| - div message, :class => "flash flash_#{type}" - end - end - end - end - - def build_main_content_wrapper - div :id => "main_content_wrapper" do - div :id => "main_content" do - main_content - end - end - end - - def main_content - I18n.t('active_admin.main_content', :model => self.class.name).html_safe - end - - def title - self.class.name - end - - # Set's the page title for the layout to render - def set_page_title - set_ivar_on_view "@page_title", title - end - - # Returns the sidebar sections to render for the current action - def sidebar_sections_for_action - if active_admin_config && active_admin_config.sidebar_sections? - active_admin_config.sidebar_sections_for(params[:action], self) - else - [] - end - end - - def action_items_for_action - if active_admin_config && active_admin_config.action_items? - active_admin_config.action_items_for(params[:action], self) - else - [] - end - end - - # Renders the sidebar - def build_sidebar - div :id => "sidebar" do - sidebar_sections_for_action.collect do |section| - sidebar_section(section) - end - end - end - - def skip_sidebar? - sidebar_sections_for_action.empty? || assigns[:skip_sidebar] == true - end - - # Renders the content for the footer - def build_footer - insert_tag view_factory.footer - end - - end - end - end -end diff --git a/lib/active_admin/views/pages/dashboard.rb b/lib/active_admin/views/pages/dashboard.rb deleted file mode 100644 index 3a8e45688a3..00000000000 --- a/lib/active_admin/views/pages/dashboard.rb +++ /dev/null @@ -1,62 +0,0 @@ -module ActiveAdmin - module Views - module Pages - class Dashboard < Base - - def main_content - if assigns[:dashboard_sections] && assigns[:dashboard_sections].any? - render_sections(assigns[:dashboard_sections]) - else - default_welcome_section - end - end - - protected - - # Dashboards don't have a sidebar - def build_sidebar; end - - def title - I18n.t("active_admin.dashboard") - end - - def render_sections(sections) - table :class => "dashboard" do - sections.in_groups_of(3, false).each do |row| - tr do - row.each do |section| - td do - render_section(section) - end - end - end - end - end - end - - # Renders each section using their renderer - def render_section(section) - insert_tag section_renderer(section), section - end - - def section_renderer(section) - if section.options[:as] - view_factory["dashboard_section_as_#{section.options[:as]}"] - else - view_factory.dashboard_section - end - end - - def default_welcome_section - div :class => "blank_slate_container", :id => "dashboard_default_message" do - span :class => "blank_slate" do - span I18n.t('active_admin.dashboard_welcome.welcome') - small I18n.t('active_admin.dashboard_welcome.call_to_action') - end - end - end - - end - end - end -end diff --git a/lib/active_admin/views/pages/form.rb b/lib/active_admin/views/pages/form.rb deleted file mode 100644 index 05c303fc9ec..00000000000 --- a/lib/active_admin/views/pages/form.rb +++ /dev/null @@ -1,51 +0,0 @@ -module ActiveAdmin - module Views - module Pages - - class Form < Base - - def title - I18n.t("active_admin.#{params[:action]}_model", - :model => active_admin_config.resource_label) - end - - def form_presenter - active_admin_config.get_page_presenter(:form) || default_form_config - end - - def main_content - form_options = default_form_options.merge(form_presenter.options) - - if form_options[:partial] - render(form_options[:partial]) - else - active_admin_form_for(resource, form_options) do |f| - instance_exec f, &form_presenter.block - end - end - end - - private - - def default_form_options - { - :url => default_form_path, - :as => active_admin_config.resource_name.singular - } - end - - def default_form_path - resource.persisted? ? resource_path(resource) : collection_path - end - - def default_form_config - ActiveAdmin::PagePresenter.new do |f| - f.inputs - f.actions - end - end - end - - end - end -end diff --git a/lib/active_admin/views/pages/index.rb b/lib/active_admin/views/pages/index.rb deleted file mode 100644 index f5f725e55dc..00000000000 --- a/lib/active_admin/views/pages/index.rb +++ /dev/null @@ -1,156 +0,0 @@ -require 'active_admin/helpers/collection' - -module ActiveAdmin - module Views - module Pages - - class Index < Base - - def title - case config[:title] - when Proc then controller.instance_eval(&config[:title]) - when String then config[:title] - else - active_admin_config.plural_resource_label - end - end - - def config - active_admin_config.get_page_presenter(:index, params[:as]) || default_index_config - end - - # Render's the index configuration that was set in the - # controller. Defaults to rendering the ActiveAdmin::Pages::Index::Table - def main_content - wrap_with_batch_action_form do - build_table_tools - build_collection - end - end - - protected - - def wrap_with_batch_action_form(&block) - if active_admin_config.batch_actions.any? - batch_action_form(&block) - else - block.call - end - end - - include ::ActiveAdmin::Helpers::Collection - - def items_in_collection? - !collection_is_empty? - end - - def build_collection - if items_in_collection? - render_index - else - if params[:q] || params[:scope] - render_empty_results - else - render_blank_slate - end - end - end - - include ::ActiveAdmin::ViewHelpers::DownloadFormatLinksHelper - - def build_table_tools - div :class => "table_tools" do - build_batch_actions_selector - build_scopes - build_index_list - end if any_table_tools? - end - - def any_table_tools? - active_admin_config.batch_actions.any? || - active_admin_config.scopes.any? || - active_admin_config.page_presenters[:index].try(:size).try(:>, 1) - end - - def build_batch_actions_selector - if active_admin_config.batch_actions.any? - insert_tag view_factory.batch_action_selector, active_admin_config.batch_actions - end - end - - def build_scopes - if active_admin_config.scopes.any? - scope_options = { - :scope_count => config[:scope_count].nil? ? true : config[:scope_count] - } - - scopes_renderer active_admin_config.scopes, scope_options - end - end - - def build_index_list - indexes = active_admin_config.page_presenters[:index] - - if indexes.kind_of?(Hash) && indexes.length > 1 - index_classes = [] - active_admin_config.page_presenters[:index].each do |type, page_presenter| - index_classes << find_index_renderer_class(page_presenter[:as]) - end - - index_list_renderer index_classes - end - end - - # Creates a default configuration for the resource class. This is a table - # with each column displayed as well as all the default actions - def default_index_config - @default_index_config ||= ::ActiveAdmin::PagePresenter.new(:as => :table) do |display| - selectable_column - id_column - resource_class.content_columns.each do |col| - column col.name.to_sym - end - default_actions - end - end - - # Returns the actual class for renderering the main content on the index - # page. To set this, use the :as option in the page_presenter block. - def find_index_renderer_class(klass) - klass.is_a?(Class) ? klass : - ::ActiveAdmin::Views.const_get("IndexAs" + klass.to_s.camelcase) - end - - def render_blank_slate - blank_slate_content = I18n.t("active_admin.blank_slate.content", :resource_name => active_admin_config.plural_resource_label) - if controller.action_methods.include?('new') - blank_slate_content += " " + link_to(I18n.t("active_admin.blank_slate.link"), new_resource_path) - end - insert_tag(view_factory.blank_slate, blank_slate_content) - end - - def render_empty_results - empty_results_content = I18n.t("active_admin.pagination.empty", :model => active_admin_config.plural_resource_label) - insert_tag(view_factory.blank_slate, empty_results_content) - end - - def render_index - renderer_class = find_index_renderer_class(config[:as]) - paginator = config[:paginator].nil? ? true : config[:paginator] - download_links = config[:download_links].nil? ? active_admin_config.namespace.download_links : config[:download_links] - - paginated_collection(collection, :entry_name => active_admin_config.resource_label, - :entries_name => active_admin_config.plural_resource_label(:count => collection_size), - :download_links => download_links, - :paginator => paginator) do - div :class => 'index_content' do - insert_tag(renderer_class, config, collection) - end - end - end - - end - end - end -end - diff --git a/lib/active_admin/views/pages/layout.rb b/lib/active_admin/views/pages/layout.rb deleted file mode 100644 index a03e58da316..00000000000 --- a/lib/active_admin/views/pages/layout.rb +++ /dev/null @@ -1,26 +0,0 @@ -module ActiveAdmin - module Views - module Pages - - # Acts as a standard rails Layout for use when logged - # out or when rendering custom actions. - class Layout < Base - - def title - assigns[:page_title] || I18n.t("active_admin.#{params[:action]}", :default => params[:action].to_s.titleize) - end - - # Render the content_for(:layout) into the main content area - def main_content - content_for_layout = content_for(:layout) - if content_for_layout.is_a?(Arbre::Element) - current_arbre_element.add_child content_for_layout.children - else - text_node content_for_layout - end - end - end - - end - end -end diff --git a/lib/active_admin/views/pages/page.rb b/lib/active_admin/views/pages/page.rb deleted file mode 100644 index bb22043dcae..00000000000 --- a/lib/active_admin/views/pages/page.rb +++ /dev/null @@ -1,30 +0,0 @@ -module ActiveAdmin - module Views - module Pages - class Page < Base - - def main_content - if page_presenter.block - instance_exec &page_presenter.block - else - nil - end - end - - protected - - def page_presenter - active_admin_config.get_page_presenter(:index) || ActiveAdmin::PagePresenter.new - end - - def title - if page_presenter[:title] - render_or_call_method_or_proc_on self, page_presenter[:title] - else - active_admin_config.name - end - end - end - end - end -end diff --git a/lib/active_admin/views/pages/show.rb b/lib/active_admin/views/pages/show.rb deleted file mode 100644 index fc257f35fd1..00000000000 --- a/lib/active_admin/views/pages/show.rb +++ /dev/null @@ -1,59 +0,0 @@ -module ActiveAdmin - module Views - module Pages - class Show < Base - - def config - active_admin_config.get_page_presenter(:show) || super - end - - def title - if config[:title] - render_or_call_method_or_proc_on(resource, config[:title]) - else - default_title - end - end - - def main_content - if config.block - # Eval the show config from the controller - instance_exec resource, &config.block - else - default_main_content - end - end - - def attributes_table(*args, &block) - panel(I18n.t('active_admin.details', :model => active_admin_config.resource_label)) do - attributes_table_for resource, *args, &block - end - end - - protected - - def default_title - title = display_name(resource) - - if title.nil? || title.empty? || title == resource.to_s - title = "#{active_admin_config.resource_label} ##{resource.id}" - end - - title - end - - module DefaultMainContent - def default_main_content - attributes_table *default_attribute_table_rows - end - - def default_attribute_table_rows - resource.class.columns.collect{|column| column.name.to_sym } - end - end - - include DefaultMainContent - end - end - end -end diff --git a/lib/active_admin/views/tabbed_navigation.rb b/lib/active_admin/views/tabbed_navigation.rb deleted file mode 100644 index c8a1bd16c4e..00000000000 --- a/lib/active_admin/views/tabbed_navigation.rb +++ /dev/null @@ -1,62 +0,0 @@ -module ActiveAdmin - module Views - - # Renders an ActiveAdmin::Menu as a set of unordered list items. - # - # This component takes cares of deciding which items should be - # displayed given the current context and renders them appropriately. - # - # The entire component is rendered within one ul element. - class TabbedNavigation < Component - - attr_reader :menu - - # Build a new tabbed navigation component. - # - # @param [ActiveAdmin::Menu] menu the Menu to render - # @param [Hash] options the options as passed to the underlying ul element. - # - def build(menu, options = {}) - @menu = menu - super(default_options.merge(options)) - build_menu - end - - # The top-level menu items that should be displayed. - def menu_items - menu.items(self) - end - - def tag_name - 'ul' - end - - private - - def build_menu - menu_items.each do |item| - build_menu_item(item) - end - end - - def build_menu_item(item) - li :id => item.id do |li| - li.add_class "current" if item.current? assigns[:current_tab] - - text_node link_to item.label(self), item.url(self), item.html_options - - if children = item.items(self).presence - li.add_class "has_nested" - ul do - children.each{ |child| build_menu_item child } - end - end - end - end - - def default_options - { :id => "tabs" } - end - end - end -end diff --git a/lib/active_admin/views/title_bar.rb b/lib/active_admin/views/title_bar.rb deleted file mode 100644 index dd8d5bc98ba..00000000000 --- a/lib/active_admin/views/title_bar.rb +++ /dev/null @@ -1,53 +0,0 @@ -module ActiveAdmin - module Views - class TitleBar < Component - - def build(title, action_items) - super(:id => "title_bar") - @title = title - @action_items = action_items - build_titlebar_left - build_titlebar_right - end - - private - - def build_titlebar_left - div :id => "titlebar_left" do - build_breadcrumb - build_title_tag - end - end - - def build_titlebar_right - div :id => "titlebar_right" do - build_action_items - end - end - - def build_breadcrumb(separator = "/") - links = if active_admin_config && active_admin_config.breadcrumb.present? - instance_exec(controller, &active_admin_config.breadcrumb) - else - breadcrumb_links - end - return unless links.present? && links.is_a?(::Array) - span :class => "breadcrumb" do - links.each do |link| - text_node link - span(separator, :class => "breadcrumb_sep") - end - end - end - - def build_title_tag - h2(@title, :id => 'page_title') - end - - def build_action_items - insert_tag(view_factory.action_items, @action_items) if @action_items.any? - end - - end - end -end diff --git a/lib/activeadmin.rb b/lib/activeadmin.rb index 2f7290fd575..9ebd064ec35 100644 --- a/lib/activeadmin.rb +++ b/lib/activeadmin.rb @@ -1 +1,2 @@ -require 'active_admin' +# frozen_string_literal: true +require_relative "active_admin" diff --git a/lib/generators/active_admin/assets/assets_generator.rb b/lib/generators/active_admin/assets/assets_generator.rb index 1ffcc1579a9..6773124496f 100644 --- a/lib/generators/active_admin/assets/assets_generator.rb +++ b/lib/generators/active_admin/assets/assets_generator.rb @@ -1,39 +1,15 @@ +# frozen_string_literal: true module ActiveAdmin module Generators class AssetsGenerator < Rails::Generators::Base - - class_option :bourbon, :type => :boolean, :default => true, - :desc => "Generate Bourbon scss files if using Rails 3.0.x" - - class_option :jquery, :type => :boolean, :default => true, - :desc => "Generate jQuery js files if using Rails 3.0.x" - - def self.source_root - @_active_admin_source_root ||= File.expand_path("../templates", __FILE__) - end + source_root File.expand_path("templates", __dir__) def install_assets - require 'rails' - require 'active_admin' - - if ActiveAdmin.use_asset_pipeline? - template '3.1/active_admin.js', 'app/assets/javascripts/active_admin.js' - template '3.1/active_admin.css.scss', 'app/assets/stylesheets/active_admin.css.scss' - else - template '3.0/active_admin.js', 'public/javascripts/active_admin.js' - directory '../../../../../app/assets/images/active_admin', 'public/images/active_admin' - generate "jquery:install --ui" if options.jquery? - install_bourbon if options.bourbon? - end + remove_file "app/assets/stylesheets/active_admin.scss" + remove_file "app/assets/javascripts/active_admin.js" + template "active_admin.css", "app/assets/stylesheets/active_admin.css" + template "tailwind.config.js", "tailwind-active_admin.config.js" end - - private - - def install_bourbon - rake "bourbon:install" - create_file "public/stylesheets/sass/_bourbon.scss", '@import "bourbon/bourbon"' - end - end end end diff --git a/lib/generators/active_admin/assets/templates/3.0/active_admin.js b/lib/generators/active_admin/assets/templates/3.0/active_admin.js deleted file mode 100644 index 6bec1fe4deb..00000000000 --- a/lib/generators/active_admin/assets/templates/3.0/active_admin.js +++ /dev/null @@ -1 +0,0 @@ -(function(){window.AA={}}).call(this),function(){window.AA.CheckboxToggler=AA.CheckboxToggler=function(){function t(t,e){var n;this.options=t,this.container=e,n={},this.options=$.extend({},n,t),this._init(),this._bind()}return t.prototype._init=function(){if(!this.container)throw new Error("Container element not found");if(this.$container=$(this.container),!this.$container.find(".toggle_all").length)throw new Error('"toggle all" checkbox not found');return this.toggle_all_checkbox=this.$container.find(".toggle_all"),this.checkboxes=this.$container.find(":checkbox").not(this.toggle_all_checkbox)},t.prototype._bind=function(){var t=this;return this.checkboxes.change(function(e){return t._didChangeCheckbox(e.target)}),this.toggle_all_checkbox.change(function(){return t._didChangeToggleAllCheckbox()})},t.prototype._didChangeCheckbox=function(){switch(this.checkboxes.filter(":checked").length){case this.checkboxes.length-1:return this.toggle_all_checkbox.prop({checked:null});case this.checkboxes.length:return this.toggle_all_checkbox.prop({checked:!0})}},t.prototype._didChangeToggleAllCheckbox=function(){var t,e=this;return t=this.toggle_all_checkbox.prop("checked")?!0:null,this.checkboxes.each(function(n,o){return $(o).prop({checked:t}),e._didChangeCheckbox(o)})},t}(),jQuery(function(t){return t.widget.bridge("checkboxToggler",AA.CheckboxToggler)})}.call(this),function(){window.AA.DropdownMenu=AA.DropdownMenu=function(){function t(t,e){var n;this.options=t,this.element=e,this.$element=$(this.element),n={fadeInDuration:20,fadeOutDuration:100,onClickActionItemCallback:null},this.options=$.extend({},n,t),this.$menuButton=this.$element.find(".dropdown_menu_button"),this.$menuList=this.$element.find(".dropdown_menu_list_wrapper"),this.isOpen=!1,this._buildMenuList(),this._bind()}return t.prototype.open=function(){return this.isOpen=!0,this.$menuList.fadeIn(this.options.fadeInDuration),this._positionMenuList(),this._positionNipple(),this},t.prototype.close=function(){return this.isOpen=!1,this.$menuList.fadeOut(this.options.fadeOutDuration),this},t.prototype.destroy=function(){return this.$element.unbind(),this.$element=null,this},t.prototype.isDisabled=function(){return this.$menuButton.hasClass("disabled")},t.prototype.disable=function(){return this.$menuButton.addClass("disabled")},t.prototype.enable=function(){return this.$menuButton.removeClass("disabled")},t.prototype.option=function(t,e){return $.isPlainObject(t)?this.options=$.extend(!0,this.options,t):null!=t?this.options[t]:this.options[t]=e},t.prototype._buildMenuList=function(){return this.$menuList.prepend(''),this.$menuList.hide()},t.prototype._bind=function(){var t=this;return $("body").bind("click",function(){return t.isOpen===!0?t.close():void 0}),this.$menuButton.bind("click",function(){return t.isDisabled()||(t.isOpen===!0?t.close():t.open()),!1})},t.prototype._positionMenuList=function(){var t,e,n;return t=this.$menuButton.position().left+this.$menuButton.outerWidth()/2,e=this.$menuList.outerWidth()/2,n=t-e,this.$menuList.css("left",n)},t.prototype._positionNipple=function(){var t,e,n,o,i;return n=this.$menuList.outerWidth()/2,e=this.$menuButton.position().top+this.$menuButton.outerHeight()+10,this.$menuList.css("top",e),t=this.$menuList.find(".dropdown_menu_nipple"),o=t.outerWidth()/2,i=n-o,t.css("left",i)},t}(),function(t){return t.widget.bridge("aaDropdownMenu",AA.DropdownMenu),t(function(){return t(".dropdown_menu").aaDropdownMenu()})}(jQuery)}.call(this),function(){window.AA.Popover=AA.Popover=function(){function t(t,e){var n;this.options=t,this.element=e,this.$element=$(this.element),n={fadeInDuration:20,fadeOutDuration:100,autoOpen:!0,pageWrapperElement:"#wrapper",onClickActionItemCallback:null},this.options=$.extend({},n,t),this.$popover=null,this.isOpen=!1,this.$popover=$(this.$element.attr("href")).length>0?$(this.$element.attr("href")):this.$element.next(".popover"),this._buildPopover(),this._bind()}return t.prototype.open=function(){return this.isOpen=!0,this.$popover.fadeIn(this.options.fadeInDuration),this._positionPopover(),this._positionNipple(),this},t.prototype.close=function(){return this.isOpen=!1,this.$popover.fadeOut(this.options.fadeOutDuration),this},t.prototype.destroy=function(){return this.$element.removeData("popover"),this.$element.unbind(),this.$element=null,this},t.prototype.option=function(){},t.prototype._buildPopover=function(){return this.$popover.prepend('
'),this.$popover.hide(),this.$popover.addClass("popover")},t.prototype._bind=function(){var t=this;return $(this.options.pageWrapperElement).bind("click",function(){return t.isOpen===!0?t.close():void 0}),this.options.autoOpen===!0?this.$element.bind("click",function(){return t.isOpen===!0?t.close():t.open(),!1}):void 0},t.prototype._positionPopover=function(){var t,e,n;return t=this.$element.offset().left+this.$element.outerWidth()/2,e=this.$popover.outerWidth()/2,n=t-e,this.$popover.css("left",n)},t.prototype._positionNipple=function(){var t,e,n,o,i;return n=this.$popover.outerWidth()/2,e=this.$element.offset().top+this.$element.outerHeight()+10,this.$popover.css("top",e),t=this.$popover.find(".popover_nipple"),o=t.outerWidth()/2,i=n-o,t.css("left",i)},t}(),function(t){return t.widget.bridge("popover",AA.Popover)}(jQuery)}.call(this),function(){var t,e={}.hasOwnProperty,n=function(t,n){function o(){this.constructor=t}for(var i in n)e.call(n,i)&&(t[i]=n[i]);return o.prototype=n.prototype,t.prototype=new o,t.__super__=n.prototype,t};window.AA.TableCheckboxToggler=AA.TableCheckboxToggler=function(e){function o(){return t=o.__super__.constructor.apply(this,arguments)}return n(o,e),o.prototype._init=function(){return o.__super__._init.apply(this,arguments)},o.prototype._bind=function(){var t=this;return o.__super__._bind.apply(this,arguments),this.$container.find("tbody td").click(function(e){return e.target.type!=="checkbox"?t._didClickCell(e.target):void 0})},o.prototype._didChangeCheckbox=function(t){var e;return o.__super__._didChangeCheckbox.apply(this,arguments),e=$(t).parents("tr"),t.checked?e.addClass("selected"):e.removeClass("selected")},o.prototype._didClickCell=function(t){return $(t).parent("tr").find(":checkbox").click()},o}(AA.CheckboxToggler),jQuery(function(t){return t.widget.bridge("tableCheckboxToggler",AA.TableCheckboxToggler)})}.call(this),function(){$(function(){return $(document).on("focus",".datepicker:not(.hasDatepicker)",function(){return $(this).datepicker({dateFormat:"yy-mm-dd"})}),$(".clear_filters_btn").click(function(){return window.location.search=""}),$(".dropdown_button").popover(),$("#q_search").submit(function(){return $(this).find(":input").filter(function(){return this.value===""}).prop("disabled",!0)})})}.call(this),function(){jQuery(function(t){return t(document).delegate("#batch_actions_selector li a","click.rails",function(){return t("#batch_action").val(t(this).attr("data-action")),t("#collection_selection").submit()}),t("#batch_actions_selector").length&&t(":checkbox.toggle_all").length?(t(".paginated_collection table.index_table").length?t(".paginated_collection table.index_table").tableCheckboxToggler():t(".paginated_collection").checkboxToggler(),t(".paginated_collection").find(":checkbox").bind("change",function(){return t(".paginated_collection").find(":checkbox").filter(":checked").length>0?t("#batch_actions_selector").aaDropdownMenu("enable"):t("#batch_actions_selector").aaDropdownMenu("disable")})):void 0})}.call(this); \ No newline at end of file diff --git a/lib/generators/active_admin/assets/templates/3.1/active_admin.css.scss b/lib/generators/active_admin/assets/templates/3.1/active_admin.css.scss deleted file mode 100644 index 90ba1d475e0..00000000000 --- a/lib/generators/active_admin/assets/templates/3.1/active_admin.css.scss +++ /dev/null @@ -1,17 +0,0 @@ -// SASS variable overrides must be declared before loading up Active Admin's styles. -// -// To view the variables that Active Admin provides, take a look at -// `app/assets/stylesheets/active_admin/mixins/_variables.css.scss` in the -// Active Admin source. -// -// For example, to change the sidebar width: -// $sidebar-width: 242px; - -// Active Admin's got SASS! -@import "active_admin/mixins"; -@import "active_admin/base"; - -// Overriding any non-variable SASS must be done after the fact. -// For example, to change the default status-tag color: -// -// .status_tag { background: #6090DB; } diff --git a/lib/generators/active_admin/assets/templates/3.1/active_admin.js b/lib/generators/active_admin/assets/templates/3.1/active_admin.js deleted file mode 100644 index d2b66c59f9f..00000000000 --- a/lib/generators/active_admin/assets/templates/3.1/active_admin.js +++ /dev/null @@ -1 +0,0 @@ -//= require active_admin/base diff --git a/lib/generators/active_admin/assets/templates/active_admin.css b/lib/generators/active_admin/assets/templates/active_admin.css new file mode 100644 index 00000000000..caa58b33487 --- /dev/null +++ b/lib/generators/active_admin/assets/templates/active_admin.css @@ -0,0 +1,3 @@ +@import "tailwindcss"; + +@config "../../../tailwind-active_admin.config.js"; diff --git a/lib/generators/active_admin/assets/templates/builds/.keep b/lib/generators/active_admin/assets/templates/builds/.keep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/generators/active_admin/assets/templates/tailwind.config.js b/lib/generators/active_admin/assets/templates/tailwind.config.js new file mode 100644 index 00000000000..55694b7a5e7 --- /dev/null +++ b/lib/generators/active_admin/assets/templates/tailwind.config.js @@ -0,0 +1,22 @@ +import { execSync } from 'child_process'; +import activeAdminPlugin from '@activeadmin/activeadmin/plugin'; + +// Always use the last line of output since Bundler's DEBUG env will print additional lines. +const activeAdminPath = execSync('bundle show activeadmin', { encoding: 'utf-8' }).trim().split(/\r?\n/).pop(); + +export default { + content: [ + `${activeAdminPath}/vendor/javascript/flowbite.js`, + `${activeAdminPath}/plugin.js`, + `${activeAdminPath}/app/views/**/*.{arb,erb,html,rb}`, + './app/admin/**/*.{arb,erb,html,rb}', + './app/views/active_admin/**/*.{arb,erb,html,rb}', + './app/views/admin/**/*.{arb,erb,html,rb}', + './app/views/layouts/active_admin*.{erb,html}', + './app/javascript/**/*.js' + ], + darkMode: "selector", + plugins: [ + activeAdminPlugin + ] +} diff --git a/lib/generators/active_admin/devise/devise_generator.rb b/lib/generators/active_admin/devise/devise_generator.rb index 8d17e2a7293..2e764a6d6e2 100644 --- a/lib/generators/active_admin/devise/devise_generator.rb +++ b/lib/generators/active_admin/devise/devise_generator.rb @@ -1,15 +1,34 @@ +# frozen_string_literal: true +require_relative "../../../active_admin/error" +require_relative "../../../active_admin/dependency" + module ActiveAdmin module Generators class DeviseGenerator < Rails::Generators::NamedBase desc "Creates an admin user and uses Devise for authentication" - argument :name, :type => :string, :default => "AdminUser" + argument :name, type: :string, default: "AdminUser" + + class_option :registerable, type: :boolean, default: false, + desc: "Should the generated resource be registerable?" + + RESERVED_NAMES = [:active_admin_user] - class_option :registerable, :type => :boolean, :default => false, - :desc => "Should the generated resource be registerable?" + class_option :default_user, type: :boolean, default: true, + desc: "Should a default user be created inside the migration?" def install_devise - require 'devise' - if File.exists?(File.join(destination_root, "config", "initializers", "devise.rb")) + begin + Dependency.devise! Dependency::Requirements::DEVISE + rescue DependencyError => e + raise ActiveAdmin::GeneratorError, "#{e.message} If you don't want to use devise, run the generator with --skip-users." + end + + require "devise" + + initializer_file = + File.join(destination_root, "config", "initializers", "devise.rb") + + if File.exist?(initializer_file) log :generate, "No need to install devise, already done." else log :generate, "devise:install" @@ -18,6 +37,9 @@ def install_devise end def create_admin_user + if RESERVED_NAMES.include?(name.underscore) + raise ActiveAdmin::GeneratorError, "The name #{name} is reserved by Active Admin" + end invoke "devise", [name] end @@ -30,28 +52,17 @@ def remove_registerable_from_model def set_namespace_for_path routes_file = File.join(destination_root, "config", "routes.rb") - gsub_file routes_file, /devise_for :#{plural_table_name}/, "devise_for :#{plural_table_name}, ActiveAdmin::Devise.config" + gsub_file routes_file, /devise_for :#{plural_table_name}$/, "devise_for :#{plural_table_name}, ActiveAdmin::Devise.config" end - def add_default_user_to_migration - # Don't assume that we have a migration! - devise_migration_file = Dir["db/migrate/*_devise_create_#{table_name}.rb"].first - return if devise_migration_file.nil? - - devise_migration_content = File.read(devise_migration_file) - - if devise_migration_content["def change"] - inject_into_file devise_migration_file, - "def migrate(direction)\n super\n # Create a default user\n #{class_name}.create!(:email => 'admin@example.com', :password => 'password', :password_confirmation => 'password') if direction == :up\n end\n\n ", - :before => "def change" - elsif devise_migration_content[/def (self.)?up/] - inject_into_file devise_migration_file, - "# Create a default user\n #{class_name}.create!(:email => 'admin@example.com', :password => 'password', :password_confirmation => 'password')\n\n ", - :before => "add_index :#{table_name}, :email" - else - puts devise_migration_content - raise "Failed to add default admin user to migration." - end + def add_default_user_to_seed + seeds_paths = Rails.application.paths["db/seeds.rb"] + seeds_file = seeds_paths.existent.first + return if seeds_file.nil? || !options[:default_user] + + create_user_code = "#{class_name}.create!(email: 'admin@example.com', password: 'password', password_confirmation: 'password') if Rails.env.development?" + + append_to_file seeds_file, create_user_code end end end diff --git a/lib/generators/active_admin/install/install_generator.rb b/lib/generators/active_admin/install/install_generator.rb index 4f05cb30ce4..fa1150e3fc9 100644 --- a/lib/generators/active_admin/install/install_generator.rb +++ b/lib/generators/active_admin/install/install_generator.rb @@ -1,36 +1,38 @@ -require 'rails/generators/active_record' +# frozen_string_literal: true +require "rails/generators/active_record" module ActiveAdmin module Generators class InstallGenerator < ActiveRecord::Generators::Base desc "Installs Active Admin and generates the necessary migrations" - argument :name, :type => :string, :default => "AdminUser" + argument :name, type: :string, default: "AdminUser" - hook_for :users, :default => "devise", :desc => "Admin user generator to run. Skip with --skip-users" + hook_for :users, default: "devise", desc: "Admin user generator to run. Skip with --skip-users" + class_option :skip_comments, type: :boolean, default: false, desc: "Skip installation of comments" - def self.source_root - @_active_admin_source_root ||= File.expand_path("../templates", __FILE__) - end + source_root File.expand_path("templates", __dir__) - def copy_initializer - @underscored_user_name = name.underscore - template 'active_admin.rb.erb', 'config/initializers/active_admin.rb' + def copy_initializer + @underscored_user_name = name.underscore.tr("/", "_") + @use_authentication_method = options[:users].present? + @skip_comments = options[:skip_comments] + template "active_admin.rb.erb", "config/initializers/active_admin.rb" end def setup_directory empty_directory "app/admin" - template 'dashboard.rb', 'app/admin/dashboard.rb' + template "dashboard.rb", "app/admin/dashboard.rb" if options[:users].present? @user_class = name - template 'admin_user.rb.erb', "app/admin/#{name.underscore}.rb" + template "admin_users.rb.erb", "app/admin/#{name.underscore.pluralize}.rb" end end def setup_routes - if ARGV.include? "--skip-users" + if options[:users] # Ensure Active Admin routes occur after Devise routes so that Devise has higher priority + inject_into_file "config/routes.rb", "\n ActiveAdmin.routes(self)", after: /devise_for .*, ActiveAdmin::Devise\.config/ + else route "ActiveAdmin.routes(self)" - else # Ensure Active Admin routes occur after Devise routes so that Devise has higher priority - inject_into_file "config/routes.rb", "\n ActiveAdmin.routes(self)", :after => /devise_for.*/ end end @@ -39,7 +41,9 @@ def create_assets end def create_migrations - migration_template 'migrations/create_active_admin_comments.rb', 'db/migrate/create_active_admin_comments.rb' + unless options[:skip_comments] + migration_template "migrations/create_active_admin_comments.rb.erb", "db/migrate/create_active_admin_comments.rb" + end end end end diff --git a/lib/generators/active_admin/install/templates/active_admin.rb.erb b/lib/generators/active_admin/install/templates/active_admin.rb.erb index 21a08cca904..1432cfc4dbe 100644 --- a/lib/generators/active_admin/install/templates/active_admin.rb.erb +++ b/lib/generators/active_admin/install/templates/active_admin.rb.erb @@ -1,23 +1,29 @@ ActiveAdmin.setup do |config| - # == Site Title # # Set the title that is displayed on the main layout - # for each of the active admin pages. + # for each of the active admin pages. Can also be customized + # by extracting the _site_header partial into your project + # to use your own logo, styles, etc. # config.site_title = "<%= Rails.application.class.name.split("::").first.titlecase %>" - # Set the link url for the title. For example, to take - # users to your main site. Defaults to no link. + # == Load Paths # - # config.site_title_link = "/" - - # Set an optional image to be displayed for the header - # instead of a string (overrides :site_title) + # By default Active Admin files go inside app/admin/. + # You can change this directory. + # + # eg: + # config.load_paths = [File.join(Rails.root, 'app', 'ui')] # - # Note: Recommended image height is 21px to properly fit in the header + # Or, you can also load more directories. + # Useful when setting namespaces with users that are not your main AdminUser entity. # - # config.site_title_image = "/images/logo.png" + # eg: + # config.load_paths = [ + # File.join(Rails.root, 'app', 'admin'), + # File.join(Rails.root, 'app', 'cashier') + # ] # == Default Namespace # @@ -54,9 +60,38 @@ ActiveAdmin.setup do |config| # ensure that there is a currently logged in admin user. # # This setting changes the method which Active Admin calls - # within the controller. - config.authentication_method = :authenticate_<%= @underscored_user_name %>! + # within the application controller. + <% unless @use_authentication_method %># <% end %>config.authentication_method = :authenticate_<%= @underscored_user_name %>! + + # == User Authorization + # + # Active Admin will automatically call an authorization + # method in a before filter of all controller actions to + # ensure that there is a user with proper rights. You can use + # CanCanAdapter, PunditAdapter, or make your own. Please + # refer to the documentation. + # config.authorization_adapter = ActiveAdmin::CanCanAdapter + # config.authorization_adapter = ActiveAdmin::PunditAdapter + + # In case you prefer Pundit over other solutions you can here pass + # the name of default policy class. This policy will be used in every + # case when Pundit is unable to find suitable policy. + # config.pundit_default_policy = "MyDefaultPunditPolicy" + # If you wish to maintain a separate set of Pundit policies for admin + # resources, you may set a namespace here that Pundit will search + # within when looking for a resource's policy. + # config.pundit_policy_namespace = :admin + + # You can customize your CanCan Ability class name here. + # config.cancan_ability_class = "Ability" + + # You can specify a method to be called on unauthorized access. + # This is necessary in order to prevent a redirect loop which happens + # because, by default, user gets redirected to Dashboard. If user + # doesn't have access to Dashboard, he'll end up in a redirect loop. + # Method provided here should be defined in application_controller.rb. + # config.on_unauthorized_access = :access_denied # == Current User # @@ -64,9 +99,8 @@ ActiveAdmin.setup do |config| # user performing them. # # This setting changes the method which Active Admin calls - # to return the currently logged in user. - config.current_user_method = :current_<%= @underscored_user_name %> - + # (within the application controller) to return the currently logged in user. + <% unless @use_authentication_method %># <% end %>config.current_user_method = :current_<%= @underscored_user_name %> # == Logging Out # @@ -74,19 +108,12 @@ ActiveAdmin.setup do |config| # settings configure the location and method used for the link. # # This setting changes the path where the link points to. If it's - # a string, the strings is used as the path. If it's a Symbol, we + # a string, the string is used as the path. If it's a Symbol, we # will call the method to return the path. # # Default: config.logout_link_path = :destroy_<%= @underscored_user_name %>_session_path - # This setting changes the http method used when rendering the - # link. For example :get, :delete, :put, etc.. - # - # Default: - # config.logout_link_method = :get - - # == Root # # Set the action to call for the root path. You can set different @@ -95,20 +122,25 @@ ActiveAdmin.setup do |config| # Default: # config.root_to = 'dashboard#index' - # == Admin Comments # # This allows your users to comment on any resource registered with Active Admin. # # You can completely disable comments: - # config.allow_comments = false - # - # You can disable the menu item for the comments index page: - # config.show_comments_in_menu = false + <% unless @skip_comments %># <% end %>config.comments = false # # You can change the name under which comments are registered: # config.comments_registration_name = 'AdminComment' - + # + # You can change the order for the comments and you can change the column + # to be used for ordering: + # config.comments_order = 'created_at ASC' + # + # You can disable the menu item for the comments index page: + # config.comments_menu = false + # + # You can customize the comment menu: + # config.comments_menu = { parent: 'Admin', priority: 1 } # == Batch Actions # @@ -116,62 +148,65 @@ ActiveAdmin.setup do |config| # config.batch_actions = true - # == Controller Filters # # You can add before, after and around filters to all of your # Active Admin resources and pages from here. # - # config.before_filter :do_something_awesome + # config.before_action :do_something_awesome + # == Attribute Filters + # + # You can exclude possibly sensitive model attributes from being displayed, + # added to forms, or exported by default by ActiveAdmin + # + config.filter_attributes = [:encrypted_password, :password, :password_confirmation] - # == Register Stylesheets & Javascripts + # == Localize Date/Time Format # - # We recommend using the built in Active Admin layout and loading - # up your own stylesheets / javascripts to customize the look - # and feel. + # Set the localize format to display dates and times. + # To understand how to localize your app with I18n, read more at + # https://guides.rubyonrails.org/i18n.html # - # To load a stylesheet: - # config.register_stylesheet 'my_stylesheet.css' + # You can run `bin/rails runner 'puts I18n.t("date.formats")'` to see the + # available formats in your application. # - # You can provide an options hash for more control, which is passed along to stylesheet_link_tag(): - # config.register_stylesheet 'my_print_stylesheet.css', :media => :print + config.localize_format = :long + + # == Removing Breadcrumbs + # + # Breadcrumbs are enabled by default. You can customize them for individual + # resources or you can disable them globally from here. # - # To load a javascript file: - # config.register_javascript 'my_javascript.js' + # config.breadcrumb = false + # == Create Another Checkbox + # + # Create another checkbox is disabled by default. You can customize it for individual + # resources or you can enable them globally from here. + # + # config.create_another = true # == CSV options # # Set the CSV builder separator - # config.csv_options = { :col_sep => ';' } + # config.csv_options = { col_sep: ';' } # # Force the use of quotes - # config.csv_options = { :force_quotes => true } - + # config.csv_options = { force_quotes: true } # == Menu System # # You can add a navigation menu to be used in your application, or configure a provided menu # - # To change the default utility navigation to show a link to your website & a logout btn - # - # config.namespace :admin do |admin| - # admin.build_menu :utility_navigation do |menu| - # menu.add label: "My Great Website", url: "http://www.mygreatwebsite.com", html_options: { target: :blank } - # admin.add_logout_button_to_menu menu - # end - # end - # # If you wanted to add a static menu item to the default menu provided: # # config.namespace :admin do |admin| # admin.build_menu :default do |menu| - # menu.add label: "My Great Website", url: "http://www.mygreatwebsite.com", html_options: { target: :blank } + # menu.add label: "My Great Website", url: "https://mygreatwebsite.example.com", html_options: { target: "_blank" } # end # end - # == Download Links # # You can disable download links on resource listing pages, @@ -184,26 +219,57 @@ ActiveAdmin.setup do |config| # # Disable the links entirely # admin.download_links = false # - # # Only show XML & PDF options + # # Only show XML & PDF options. You must register the format mime type with `Mime::Type.register`. # admin.download_links = [:xml, :pdf] # + # # Enable/disable the links based on block (for example, with cancan) + # admin.download_links = proc { can?(:view_download_links) } + # # end - # == Pagination # # Pagination is enabled by default for all resources. # You can control the default per page count for all resources here. # # config.default_per_page = 30 - + # + # You can control the max per page count too. + # + # config.max_per_page = 10_000 # == Filters # - # By default the index screen includes a “Filters” sidebar on the right + # By default the index screen includes a "Filters" sidebar on the right # hand side with a filter for each attribute of the registered model. # You can enable or disable them for all resources here. # # config.filters = true + # + # By default the filters include associations in a select, which means + # that every record will be loaded for each association (up + # to the value of config.maximum_association_filter_arity). + # You can enabled or disable the inclusion + # of those filters by default here. + # + # config.include_default_association_filters = true + # config.maximum_association_filter_arity = 256 # default value of :unlimited will change to 256 in a future version + # config.filter_columns_for_large_association = [ + # :display_name, + # :full_name, + # :name, + # :username, + # :login, + # :title, + # :email, + # ] + # config.filter_method_for_large_association = '_start' + + # == Sorting + # + # By default ActiveAdmin::OrderClause is used for sorting logic + # You can inherit it with own class and inject it for all resources + # + # config.order_clause = MyOrderClause end diff --git a/lib/generators/active_admin/install/templates/admin_user.rb.erb b/lib/generators/active_admin/install/templates/admin_users.rb.erb similarity index 55% rename from lib/generators/active_admin/install/templates/admin_user.rb.erb rename to lib/generators/active_admin/install/templates/admin_users.rb.erb index a0bb39105e8..9609db99af1 100644 --- a/lib/generators/active_admin/install/templates/admin_user.rb.erb +++ b/lib/generators/active_admin/install/templates/admin_users.rb.erb @@ -1,20 +1,28 @@ ActiveAdmin.register <%= @user_class %> do + permit_params :email, :password, :password_confirmation + index do + selectable_column + id_column column :email column :current_sign_in_at - column :last_sign_in_at column :sign_in_count - default_actions + column :created_at + actions end filter :email + filter :current_sign_in_at + filter :sign_in_count + filter :created_at form do |f| - f.inputs "Admin Details" do + f.inputs do f.input :email f.input :password f.input :password_confirmation end f.actions end + end diff --git a/lib/generators/active_admin/install/templates/dashboard.rb b/lib/generators/active_admin/install/templates/dashboard.rb index 60837575f3b..b57f92fdb0b 100644 --- a/lib/generators/active_admin/install/templates/dashboard.rb +++ b/lib/generators/active_admin/install/templates/dashboard.rb @@ -1,33 +1,16 @@ +# frozen_string_literal: true ActiveAdmin.register_page "Dashboard" do + menu priority: 1, label: proc { I18n.t("active_admin.dashboard") } - menu :priority => 1, :label => proc{ I18n.t("active_admin.dashboard") } - - content :title => proc{ I18n.t("active_admin.dashboard") } do - div :class => "blank_slate_container", :id => "dashboard_default_message" do - span :class => "blank_slate" do - span I18n.t("active_admin.dashboard_welcome.welcome") - small I18n.t("active_admin.dashboard_welcome.call_to_action") + content title: proc { I18n.t("active_admin.dashboard") } do + div class: "px-4 py-16 md:py-32 text-center m-auto max-w-3xl" do + h2 "Welcome to ActiveAdmin", class: "text-base font-semibold leading-7 text-indigo-600 dark:text-indigo-500" + para "This is the default page", class: "mt-2 text-3xl sm:text-4xl font-bold text-gray-900 dark:text-gray-200" + para class: "mt-6 text-xl leading-8 text-gray-700 dark:text-gray-400" do + text_node "To update the content, edit the" + strong "app/admin/dashboard.rb" + text_node "file to get started." end end - - # Here is an example of a simple dashboard with columns and panels. - # - # columns do - # column do - # panel "Recent Posts" do - # ul do - # Post.recent(5).map do |post| - # li link_to(post.title, admin_post_path(post)) - # end - # end - # end - # end - - # column do - # panel "Info" do - # para "Welcome to ActiveAdmin." - # end - # end - # end - end # content + end end diff --git a/lib/generators/active_admin/install/templates/migrations/create_active_admin_comments.rb b/lib/generators/active_admin/install/templates/migrations/create_active_admin_comments.rb deleted file mode 100644 index 90ed7b93a7a..00000000000 --- a/lib/generators/active_admin/install/templates/migrations/create_active_admin_comments.rb +++ /dev/null @@ -1,19 +0,0 @@ -class CreateActiveAdminComments < ActiveRecord::Migration - def self.up - create_table :active_admin_comments do |t| - t.string :namespace - t.text :body - t.string :resource_id, :null => false - t.string :resource_type, :null => false - t.references :author, :polymorphic => true - t.timestamps - end - add_index :active_admin_comments, [:namespace] - add_index :active_admin_comments, [:author_type, :author_id] - add_index :active_admin_comments, [:resource_type, :resource_id] - end - - def self.down - drop_table :active_admin_comments - end -end diff --git a/lib/generators/active_admin/install/templates/migrations/create_active_admin_comments.rb.erb b/lib/generators/active_admin/install/templates/migrations/create_active_admin_comments.rb.erb new file mode 100644 index 00000000000..0ce57948203 --- /dev/null +++ b/lib/generators/active_admin/install/templates/migrations/create_active_admin_comments.rb.erb @@ -0,0 +1,16 @@ +class CreateActiveAdminComments < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version.to_s %>] + def self.up + create_table :active_admin_comments do |t| + t.string :namespace + t.text :body + t.references :resource, polymorphic: true + t.references :author, polymorphic: true + t.timestamps + end + add_index :active_admin_comments, [:namespace] + end + + def self.down + drop_table :active_admin_comments + end +end diff --git a/lib/generators/active_admin/page/USAGE b/lib/generators/active_admin/page/USAGE new file mode 100644 index 00000000000..6e5b28e5e64 --- /dev/null +++ b/lib/generators/active_admin/page/USAGE @@ -0,0 +1,8 @@ +Description: + Registers pages with Active Admin + +Example: + rails generate active_admin:page Thing + + This will create: + app/admin/thing.rb diff --git a/lib/generators/active_admin/page/page_generator.rb b/lib/generators/active_admin/page/page_generator.rb new file mode 100644 index 00000000000..0330610cc17 --- /dev/null +++ b/lib/generators/active_admin/page/page_generator.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +module ActiveAdmin + module Generators + class PageGenerator < Rails::Generators::NamedBase + source_root File.expand_path("templates", __dir__) + + def generate_config_file + template "page.rb", "app/admin/#{file_path.tr('/', '_')}.rb" + end + end + end +end diff --git a/lib/generators/active_admin/page/templates/page.rb b/lib/generators/active_admin/page/templates/page.rb new file mode 100644 index 00000000000..638db219ed4 --- /dev/null +++ b/lib/generators/active_admin/page/templates/page.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +ActiveAdmin.register_page "<%= class_name %>" do + content do + # your content + end +end diff --git a/lib/generators/active_admin/resource/resource_generator.rb b/lib/generators/active_admin/resource/resource_generator.rb index a216b5493b9..87d1e2b4392 100644 --- a/lib/generators/active_admin/resource/resource_generator.rb +++ b/lib/generators/active_admin/resource/resource_generator.rb @@ -1,16 +1,64 @@ +# frozen_string_literal: true module ActiveAdmin module Generators class ResourceGenerator < Rails::Generators::NamedBase - desc "Installs ActiveAdmin in a rails 3 application" + desc "Registers resources with Active Admin" - def self.source_root - @_active_admin_source_root ||= File.expand_path("../templates", __FILE__) - end + source_root File.expand_path("templates", __dir__) def generate_config_file - template "admin.rb", "app/admin/#{file_path.gsub('/', '_')}.rb" + template "resource.rb.erb", "app/admin/#{file_path.tr('/', '_').pluralize}.rb" + end + + protected + + def attributes + @attributes ||= class_name.constantize.new.attributes.keys end + def primary_key + @primary_key ||= [class_name.constantize.primary_key].flatten + end + + def assignable_attributes + @assignable_attributes ||= attributes - primary_key - %w(created_at updated_at) + end + + def permit_params + assignable_attributes.map { |a| a.to_sym.inspect }.join(", ") + end + + def rows + attributes.map { |a| row(a) }.join("\n ") + end + + def row(name) + "row :#{name.gsub(/_id$/, '')}" + end + + def columns + (attributes - primary_key).map { |a| column(a) }.join("\n ") + end + + def column(name) + "column :#{name.gsub(/_id$/, '')}" + end + + def filters + attributes.map { |a| filter(a) }.join("\n ") + end + + def filter(name) + "filter :#{name.gsub(/_id$/, '')}" + end + + def form_inputs + assignable_attributes.map { |a| form_input(a) }.join("\n ") + end + + def form_input(name) + "f.input :#{name.gsub(/_id$/, '')}" + end end end end diff --git a/lib/generators/active_admin/resource/templates/admin.rb b/lib/generators/active_admin/resource/templates/admin.rb deleted file mode 100644 index f429bd1cd62..00000000000 --- a/lib/generators/active_admin/resource/templates/admin.rb +++ /dev/null @@ -1,3 +0,0 @@ -ActiveAdmin.register <%= class_name %> do - -end diff --git a/lib/generators/active_admin/resource/templates/resource.rb.erb b/lib/generators/active_admin/resource/templates/resource.rb.erb new file mode 100644 index 00000000000..57d1092e99c --- /dev/null +++ b/lib/generators/active_admin/resource/templates/resource.rb.erb @@ -0,0 +1,42 @@ +ActiveAdmin.register <%= class_name %> do + # Specify parameters which should be permitted for assignment + permit_params <%= permit_params %> + + # or consider: + # + # permit_params do + # permitted = [<%= permit_params %>] + # permitted << :other if params[:action] == 'create' && current_user.admin? + # permitted + # end + + # For security, limit the actions that should be available + actions :all, except: [] + + # Add or remove filters to toggle their visibility + <%= filters %> + + # Add or remove columns to toggle their visibility in the index action + index do + selectable_column + id_column + <%= columns %> + actions + end + + # Add or remove rows to toggle their visibility in the show action + show do + attributes_table_for(resource) do + <%= rows %> + end + end + + # Add or remove fields to toggle their visibility in the form + form do |f| + f.semantic_errors(*f.object.errors.attribute_names) + f.inputs do + <%= form_inputs %> + end + f.actions + end +end diff --git a/lib/generators/active_admin/views_generator.rb b/lib/generators/active_admin/views_generator.rb new file mode 100644 index 00000000000..d19e76cebdf --- /dev/null +++ b/lib/generators/active_admin/views_generator.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +module ActiveAdmin + module Generators + class ViewsGenerator < Rails::Generators::Base + source_root File.expand_path("../../../", __dir__) + + def copy_views + directory "app/views/layouts" + directory "app/views/active_admin", recursive: false + directory "app/views/active_admin/devise" + directory "app/views/active_admin/kaminari" + copy_file "app/views/active_admin/shared/_resource_comments.html.erb" + copy_file "app/views/active_admin/resource/_index_blank_slate.html.erb" + copy_file "app/views/active_admin/resource/_index_empty_results.html.erb" + end + end + end +end diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000000..2bb12200db8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4045 @@ +{ + "name": "@activeadmin/activeadmin", + "version": "4.0.0-beta23", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@activeadmin/activeadmin", + "version": "4.0.0-beta23", + "license": "MIT", + "dependencies": { + "@rails/ujs": "7.1.600", + "flowbite": "3.1.2" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@rollup/plugin-alias": "^6.0.0", + "eslint": "^10.11.0", + "gherkin-lint": "^4.2.4", + "rollup": "^4.63.5", + "tailwindcss": "^4.3.3", + "vitepress": "^2.0.0-alpha.20" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@docsearch/css": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.7.0.tgz", + "integrity": "sha512-Sk5xkdRFeE7PeWjG9l4AfTwdvMfr9wHiwNNCpHXT4v4SNyNMKdHGvEILc31BgaVFGDDNbv5u/a73tofRiwbEZw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@docsearch/js": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-4.7.0.tgz", + "integrity": "sha512-x5lCqu1tetgsJFkjQ6VSocbHldsRkGEgwg5N98Vx21sq/V5wcmj4u226PY9k+TEpIgQ772zlYbPLTPicWyGnpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@docsearch/sidepanel-js": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@docsearch/sidepanel-js/-/sidepanel-js-4.7.0.tgz", + "integrity": "sha512-A8r34jCU8kcIk2viECEn2msA28ojUF1BLi/3v5OWWc5G2N3jOuuumBXoeYjfr8dA0UxgFSy5R2bt12dnFJQSyA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.96", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.96.tgz", + "integrity": "sha512-/FmCYo/H5rIywTLV/P6OYsr0rg/31vuc8jYq0QnkOZl6m7PWIs+h5oAnSTT2cqRhOsMaKqbXcdkka6tnZ1WiUA==", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.149.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz", + "integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", + "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@rails/ujs": { + "version": "7.1.600", + "resolved": "https://registry.npmjs.org/@rails/ujs/-/ujs-7.1.600.tgz", + "integrity": "sha512-ntMYJ8++n1zcIJ1g75or7Va9IU3ueCHgp9IJk7Ny7eFEbu0Kx40EHZ2FNhoxoUEjlF5NifCFCbvoK5rDK5HpIQ==", + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz", + "integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz", + "integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz", + "integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz", + "integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz", + "integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz", + "integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz", + "integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz", + "integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz", + "integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz", + "integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz", + "integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz", + "integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz", + "integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz", + "integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz", + "integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-alias": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-6.0.0.tgz", + "integrity": "sha512-tPCzJOtS7uuVZd+xPhoy5W4vThe6KWXNmsFCNktaAh5RTqcLiSfT4huPQIXkgJ6YCOjJHvecOAzQxLFhPxKr+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "rollup": ">=4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.6.tgz", + "integrity": "sha512-G6xF9OVRWsbHadMfoPNUDwW/Jt70QYG+ZDUvtYCFxjf3SPhqVAO0u9ShXZ7Cf5silVoRWyN4bDjCEKnJkXkw0w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.6.tgz", + "integrity": "sha512-Us/kTH5e2anr1CQvO8MEq4eeCVcIXI3Ag7OFxDI30n2OW7Q7xWuxo8ItbYq28PG2Sm1NWmZzG9+SqVEaj25SFA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.6.tgz", + "integrity": "sha512-fwaSNrSHp9PJuEh4bKtd0mYC+G3FeVDjSTmsSiSlIYu7TFCpxrfB6lk7Ao8cIbswFyklPB1ZDUV8LkLBM15VWw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.6.tgz", + "integrity": "sha512-QR2tx26gCeGh4eAGnUasfnJuRc+d7Vc0Jo1/fYtKGuiKqCuhbVpbNEvqgYGBwGDZGyS04jj+zdHi9W8uvb1Sdw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.6.tgz", + "integrity": "sha512-7E1wRJEW8r7WlqmCy58ugndWHrzoEV/pRt2x0D9el7Inra62+Cw00lYW9XhW6iUCtFjhNw49McsSCIzJ/nG8uw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.6.tgz", + "integrity": "sha512-13KJeF+vDswMzEHmMIouCVcEQdov3CzAA3ZhM8QFmBJV/FZ/VMZI49UHDvPXTK+wokk86kYPmQ9Qb3xGGJbZoQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.6.tgz", + "integrity": "sha512-hV+W1r8HM84PER4If94QuzrrD0DNk4CxobNhVUiz9ZiiWHDzJss4Jra2AYGH1a7GOTzjS1sIyZ35yYEhw/aq+g==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.6.tgz", + "integrity": "sha512-5dWs/GENZufRph8PEfl0TrNH/R3DHJA+VxF/dqlfV2oUZfxRtN3qmFXg2DZj8DYM8d+Vd3dgZtuVmoBE/nb2YQ==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.6.tgz", + "integrity": "sha512-inQYLPVIUvYk+s7zpxQldqwrajUNs+t4TJjPGzsdPTvsT8gsQXVcW/sQFolHZ3YSWVk0nMMwU7TtRi0Ow+QMuw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.6.tgz", + "integrity": "sha512-sH+0MV1HmDC1q7+bQnlN9ywKBmHqkA2EdD/hHYrrz654ybc+vNC1FRNyzuYKicMQVtZZ4X2jz0osJYmsOtnj4g==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.6.tgz", + "integrity": "sha512-mCECvRr6HGekdBc6dvHzUWubqEti4JyOqlNePrOzl4Wimk26iW5Zy9rld2mQbC9id1NYjfF8nHLFq5fr3CuOKQ==", + "cpu": [ + "loong64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.6.tgz", + "integrity": "sha512-bb9rdoCPwM4DEsbOP3CJgFnvhSJ3XKCUYyUhxnlSs/XGT0DknpOYgLCPbK8VUzwl+cZGzFQm0KbBYRaSGZC2YQ==", + "cpu": [ + "loong64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.6.tgz", + "integrity": "sha512-aIJRoGCHev45JSu7JMzVrMzu1NX3nQrXx1Vb/6y4AblxW7JpCMpk4e8C6lHzvSElxi3UEcMdFJldsuWQjV1ddA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.6.tgz", + "integrity": "sha512-LvjnulezHjiaM5Ga9MPCDLTVddjvcrJhZc6vEz9fxeFRGPJsPAX9HuTuPTUQ3l8d+nYpZB/xWzUpuKJlLQm3VQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.6.tgz", + "integrity": "sha512-YmhSBeYZwJ937s8tKpYAhms48N+tj7e+oFkbkVVxfMRdAiynK2AxqRapJXB4utyXxc/iYTkMfPRQ+7ZP0IO78w==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.6.tgz", + "integrity": "sha512-9+YukhzqTvJvzfDXJJzkURK9TDY01nofNH3pyHGuucGC4xPvBVtIfCAqJjkjnmBEGJYNXB8sBc2EDt7xDgGenw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.6.tgz", + "integrity": "sha512-9G5AtEkpN/A89BILHHEL9aYRYTuFjwa8PUQqoEg8SDrwOZBKpwYqNlQCBNI5QPwGrf7qFlb6LNFilo8FCSA3Wg==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.6.tgz", + "integrity": "sha512-Ezx2E5D6Siz805j41x91JiGbzuCm8i2U98U37SQ7ytY737+wsJq+P9nPL6UmqDww491zO7MTivYOTrtmTTeCSw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.6.tgz", + "integrity": "sha512-8EZ1Q3PB7yUjvR4MbFAnCBF1wF0XRe5QQqbtlJXP7ZX5C8iYlSQJugUhj9oS0gbUP4jp9qGhaIgFnV9Nvod86Q==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.6.tgz", + "integrity": "sha512-3JI27TALItfZ/qaxXKdbRXfV6WepUoUa3IN07FE+1xxoRdl3JIyrp5PiDF0vOHsPgEiOFkF8s3B3BRymQkdwGA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.6.tgz", + "integrity": "sha512-BNOGeNFNRZDT7Hfc7b9JbTZFXkpAXeXl/CZWtxdd5CNfBaor/0RkJjg+q+E2gwVIvGqqp/NclThKFGrhuScVhw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.6.tgz", + "integrity": "sha512-D3eEyLSIim5WF5fHbwnx+ryk0delN7fJyBrDTO3wVCCNBZ/cA8aCGwLS3SiuK+vRI4E7dqikAlStGuR20dBMuw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.6.tgz", + "integrity": "sha512-xwbs9g8zLbHzj1poG1uh27xtubsu27Gk2nuN6kvvGDybjiTfwMksQAnF+phlvI5oeXkJWWDc6kg1t3XpgUYvrg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.6.tgz", + "integrity": "sha512-Tr6rhRNnFthvXr45zWVZQ5P8jZ5UwZnXNc7Qnu80cP/VVYtSioOPmvv7wP6UMgRfkBCJr2Pm60ua0YBDSJjgnw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.6.tgz", + "integrity": "sha512-0RAxO1pS/6lY1mSoD2Apm+6aPyfUlm+OWKgncIdj/eVN9I+pDY3lKgD7xsUNFei1u7MZQETGOo6m8POXMDp53A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.4.3.tgz", + "integrity": "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/primitive": "4.4.3", + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5", + "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.4.3.tgz", + "integrity": "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.4.3.tgz", + "integrity": "sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/langs": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.4.3.tgz", + "integrity": "sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/primitive": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.4.3.tgz", + "integrity": "sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/themes": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.4.3.tgz", + "integrity": "sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/transformers": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-4.4.3.tgz", + "integrity": "sha512-oJSARV6NaWd+rnNJbtnpAdj3Zg0ZVyzsnMgb3vi3HA+35y8lBWUCpOnWsmyiXZIikY+x1BDqrQUgmxfzWh7Jvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.4.3", + "@shikijs/types": "4.4.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/types": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.4.3.tgz", + "integrity": "sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.13.tgz", + "integrity": "sha512-pAeZeUbLE4Z9Vi9wsWV2bYPTweEHeJJy0G4pEjOA/FSvy1Ad5U5Km8iDV6TKre1mjBiVNfAdVHKruP8bAh4Q5A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz", + "integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.42.tgz", + "integrity": "sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.42", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.42.tgz", + "integrity": "sha512-qbhQZEFmycr+ni/qyuccS4sucNN7VAbDfbkvNxWOX2VfgFm90MNs3/UhRNKoPMEIVn0F8gdlYjLPvqxHwHeQOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.42.tgz", + "integrity": "sha512-fkCAFB4okcAANGMThboWnScp/gzWjU0ZSkVnjTIiplmMDq2uq0tIB3j+xVu4rhv5rvOgBySCysudmbMd6xRRqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.42", + "@vue/compiler-dom": "3.5.42", + "@vue/compiler-ssr": "3.5.42", + "@vue/shared": "3.5.42", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.42.tgz", + "integrity": "sha512-xmLk3wLkbizPAiLyomjgFFosf2ys9b5Ghb+oh/k2tnvipNz8OFrQOiTcWCzyK7MpBp9KkyGtfvgfLUivbmuGYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/devtools-api": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.2.1.tgz", + "integrity": "sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.2.1" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.2.1.tgz", + "integrity": "sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.2.1", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "perfect-debounce": "^2.0.0" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.2.1.tgz", + "integrity": "sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.42.tgz", + "integrity": "sha512-TzNNfKpb7hDxbQltwAut8VDQA5YP+BuRlxntHUuRjyKwlMvmAPbs3unhCvieijifY6vFfVBwsS7wG/C7uq+bEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.42.tgz", + "integrity": "sha512-9uACtuHs7vJGkm5Bp3xu4xRDLFTIYy5DgxpToVjqGIAhAEKwQfsaLvKINhM6nFVp6bZPRFGdDqd1g52MqKsotA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.42.tgz", + "integrity": "sha512-rsCmhiWLaRxGltLwhlCWyYkFn7WAbKRh0q17eZ1A6Dq6eqc2ACQ61IIryxz0LrsvCzHSilLA9JHovVwM8CNE2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.42", + "@vue/runtime-core": "3.5.42", + "@vue/shared": "3.5.42", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.42.tgz", + "integrity": "sha512-2++5dUyYS4gvo7xQXSECUDhB7TS0aOl5SeVfC5qSq1Jgfhjvegw1zqhwTIR3imZ+QYPJQw9gfcFvXGAjGZ7ajQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.42", + "@vue/runtime-dom": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.42.tgz", + "integrity": "sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "14.4.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.4.0.tgz", + "integrity": "sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.4.0", + "@vueuse/shared": "14.4.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/integrations": { + "version": "14.4.0", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-14.4.0.tgz", + "integrity": "sha512-oJz9qTgczvA7L1nXQFRU7h8tQbOCoiceqvMMhT9XYMyOGTqLJ2rEa09PON+nD2t48sZUfeOmg4eaWJXV4sZb/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vueuse/core": "14.4.0", + "@vueuse/shared": "14.4.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7 || ^8", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7 || ^8", + "vue": "^3.5.0" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.4.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.4.0.tgz", + "integrity": "sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.4.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.4.0.tgz", + "integrity": "sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", + "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js": { + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.33.1.tgz", + "integrity": "sha512-qVSq3s+d4+GsqN0teRCJtM6tdEEXyWxjzbhVrCHmBS5ZTM0FS2MOS0D13dUXAWDUN6a+lHI/N1hF9Ytz6iLl9Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cucumber-messages": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/cucumber-messages/-/cucumber-messages-8.0.0.tgz", + "integrity": "sha512-lUnWRMjwA9+KhDec/5xRZV3Du67ISumHnVLywWQXyvzmc4P+Eqx8CoeQrBQoau3Pw1hs4kJLTDyV85hFBF00SQ==", + "deprecated": "This package is now published under @cucumber/messages", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/uuid": "^3.4.6", + "protobufjs": "^6.8.8", + "uuid": "^3.3.3" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.11.0.tgz", + "integrity": "sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/flowbite": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/flowbite/-/flowbite-3.1.2.tgz", + "integrity": "sha512-MkwSgbbybCYgMC+go6Da5idEKUFfMqc/AmSjm/2ZbdmvoKf5frLPq/eIhXc9P+rC8t9boZtUXzHDgt5whZ6A/Q==", + "license": "MIT", + "dependencies": { + "@popperjs/core": "^2.9.3", + "flowbite-datepicker": "^1.3.1", + "mini-svg-data-uri": "^1.4.3", + "postcss": "^8.5.1" + } + }, + "node_modules/flowbite-datepicker": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/flowbite-datepicker/-/flowbite-datepicker-1.3.2.tgz", + "integrity": "sha512-6Nfm0MCVX3mpaR7YSCjmEO2GO8CDt6CX8ZpQnGdeu03WUCWtEPQ/uy0PUiNtIJjJZWnX0Cm3H55MOhbD1g+E/g==", + "license": "MIT", + "dependencies": { + "@rollup/plugin-node-resolve": "^15.2.3", + "flowbite": "^2.0.0" + } + }, + "node_modules/flowbite-datepicker/node_modules/flowbite": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/flowbite/-/flowbite-2.5.2.tgz", + "integrity": "sha512-kwFD3n8/YW4EG8GlY3Od9IoKND97kitO+/ejISHSqpn3vw2i5K/+ZI8Jm2V+KC4fGdnfi0XZ+TzYqQb4Q1LshA==", + "license": "MIT", + "dependencies": { + "@popperjs/core": "^2.9.3", + "flowbite-datepicker": "^1.3.0", + "mini-svg-data-uri": "^1.4.3" + } + }, + "node_modules/focus-trap": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-8.2.2.tgz", + "integrity": "sha512-qV0g8hRYBqgACcFOH3f9wXc4zPKhr/0z9RI2a6ZijZ72EeBi4g8oBy8zAWuUR1TsMpOzwpUMFvjdasrC41Joug==", + "dev": true, + "license": "MIT", + "dependencies": { + "tabbable": "^6.5.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gherkin": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/gherkin/-/gherkin-9.0.0.tgz", + "integrity": "sha512-6xoAepoxo5vhkBXjB4RCfVnSKHu5z9SqXIQVUyj+Jw8BQX8odATlee5otXgdN8llZvyvHokuvNiBeB3naEnnIQ==", + "deprecated": "This package is now published under @cucumber/gherkin", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^4.0.1", + "cucumber-messages": "8.0.0", + "source-map-support": "^0.5.16" + }, + "bin": { + "gherkin-javascript": "bin/gherkin" + } + }, + "node_modules/gherkin-lint": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/gherkin-lint/-/gherkin-lint-4.2.4.tgz", + "integrity": "sha512-iM+ECIHOF6Wh94YIF1hSHA6JH9rzcgozlMLHA/uCzGtQiMjb/uL093eh1nTpfoJ/38veL7Jfh4yY2inu7uUoFA==", + "dev": true, + "license": "ISC", + "dependencies": { + "commander": "11.0.0", + "core-js": "3.33.1", + "gherkin": "9.0.0", + "glob": "7.1.6", + "lodash": "4.17.21", + "strip-json-comments": "3.0.1", + "xml-js": "^1.6.11" + }, + "bin": { + "gherkin-lint": "dist/main.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/gherkin/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "license": "MIT", + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/oniguruma-parser": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", + "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", + "dev": true, + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", + "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.2", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/protobufjs": { + "version": "6.11.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.6.tgz", + "integrity": "sha512-k8BHqgPBOtrlougZZqF2uUk5Z7bN8f0wj+3e8M3hvtSv0NBAz4VBy5f6R5Nxq/l+i7mRFTgNZb2trxqTpHNY/A==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rolldown": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz", + "integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.149.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.8", + "@rolldown/binding-android-arm64": "1.2.8", + "@rolldown/binding-darwin-arm64": "1.2.8", + "@rolldown/binding-darwin-x64": "1.2.8", + "@rolldown/binding-freebsd-x64": "1.2.8", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", + "@rolldown/binding-linux-arm64-gnu": "1.2.8", + "@rolldown/binding-linux-arm64-musl": "1.2.8", + "@rolldown/binding-linux-ppc64-gnu": "1.2.8", + "@rolldown/binding-linux-s390x-gnu": "1.2.8", + "@rolldown/binding-linux-x64-gnu": "1.2.8", + "@rolldown/binding-linux-x64-musl": "1.2.8", + "@rolldown/binding-openharmony-arm64": "1.2.8", + "@rolldown/binding-win32-arm64-msvc": "1.2.8", + "@rolldown/binding-win32-x64-msvc": "1.2.8" + } + }, + "node_modules/rollup": { + "version": "4.63.6", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.6.tgz", + "integrity": "sha512-w4+GKyTBqshj84F1GF3s2zxAPFZPEmNkCbQtqilsYS7GsksmX6VoV7UggfZg8sUHCCOkBe7UtQp6fZwbH311FQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.6", + "@rollup/rollup-android-arm64": "4.63.6", + "@rollup/rollup-darwin-arm64": "4.63.6", + "@rollup/rollup-darwin-x64": "4.63.6", + "@rollup/rollup-freebsd-arm64": "4.63.6", + "@rollup/rollup-freebsd-x64": "4.63.6", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.6", + "@rollup/rollup-linux-arm-musleabihf": "4.63.6", + "@rollup/rollup-linux-arm64-gnu": "4.63.6", + "@rollup/rollup-linux-arm64-musl": "4.63.6", + "@rollup/rollup-linux-loong64-gnu": "4.63.6", + "@rollup/rollup-linux-loong64-musl": "4.63.6", + "@rollup/rollup-linux-ppc64-gnu": "4.63.6", + "@rollup/rollup-linux-ppc64-musl": "4.63.6", + "@rollup/rollup-linux-riscv64-gnu": "4.63.6", + "@rollup/rollup-linux-riscv64-musl": "4.63.6", + "@rollup/rollup-linux-s390x-gnu": "4.63.6", + "@rollup/rollup-linux-x64-gnu": "4.63.6", + "@rollup/rollup-linux-x64-musl": "4.63.6", + "@rollup/rollup-openbsd-x64": "4.63.6", + "@rollup/rollup-openharmony-arm64": "4.63.6", + "@rollup/rollup-win32-arm64-msvc": "4.63.6", + "@rollup/rollup-win32-ia32-msvc": "4.63.6", + "@rollup/rollup-win32-x64-gnu": "4.63.6", + "@rollup/rollup-win32-x64-msvc": "4.63.6", + "fsevents": "~2.3.2" + } + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shiki": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.4.3.tgz", + "integrity": "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.4.3", + "@shikijs/engine-javascript": "4.4.3", + "@shikijs/engine-oniguruma": "4.4.3", + "@shikijs/langs": "4.4.3", + "@shikijs/themes": "4.4.3", + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-json-comments": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", + "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tabbable": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", + "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", + "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.6", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitepress": { + "version": "2.0.0-alpha.20", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-2.0.0-alpha.20.tgz", + "integrity": "sha512-uQWKmP9PaBkf9zxHn0iOml/ZPPDaGKqQXVMBHz3yjwyyd9Eg+BKLGxbqmOGlfOMCt6Fj8TLiAnE59qlKZ0A3xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/css": "^4.7.0", + "@docsearch/js": "^4.7.0", + "@docsearch/sidepanel-js": "^4.7.0", + "@iconify-json/simple-icons": "^1.2.93", + "@shikijs/transformers": "^4.4.3", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^6.0.8", + "@vue/devtools-api": "^8.2.1", + "@vue/shared": "^3.5.41", + "@vueuse/core": "^14.4.0", + "@vueuse/integrations": "^14.4.0", + "focus-trap": "^8.2.2", + "mark.js": "8.11.1", + "minisearch": "^7.2.0", + "shiki": "^4.4.3", + "vite": "^8.2.1", + "vue": "^3.5.41" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.42.tgz", + "integrity": "sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.42", + "@vue/compiler-sfc": "3.5.42", + "@vue/runtime-dom": "3.5.42", + "@vue/server-renderer": "3.5.42", + "@vue/shared": "3.5.42" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000000..2c14ec5c405 --- /dev/null +++ b/package.json @@ -0,0 +1,51 @@ +{ + "name": "@activeadmin/activeadmin", + "version": "4.0.0-beta23", + "description": "The administration framework for Ruby on Rails.", + "main": "dist/active_admin.js", + "type": "module", + "files": [ + "dist/**/*.js", + "plugin.js", + "vendor/javascript/*.js" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/activeadmin/activeadmin.git" + }, + "keywords": [ + "administration", + "administrative", + "rails" + ], + "author": "David Rodríguez ", + "license": "MIT", + "bugs": { + "url": "https://github.com/activeadmin/activeadmin/issues" + }, + "homepage": "https://activeadmin.info", + "devDependencies": { + "@eslint/js": "^10.0.1", + "@rollup/plugin-alias": "^6.0.0", + "eslint": "^10.11.0", + "gherkin-lint": "^4.2.4", + "rollup": "^4.63.5", + "tailwindcss": "^4.3.3", + "vitepress": "^2.0.0-alpha.20" + }, + "scripts": { + "gherkin-lint": "gherkin-lint", + "lint": "eslint .", + "prebuild": "npm run lint && rm -rf dist", + "build": "rollup --config rollup.config.js", + "prepublishOnly": "npm run build", + "docs:dev": "vitepress dev docs", + "docs:build": "vitepress build docs", + "docs:test": "npm run docs:build && node --test docs/.vitepress/site.test.js", + "docs:preview": "vitepress preview docs" + }, + "dependencies": { + "@rails/ujs": "7.1.600", + "flowbite": "3.1.2" + } +} diff --git a/plugin.js b/plugin.js new file mode 100644 index 00000000000..ba890a7e6d2 --- /dev/null +++ b/plugin.js @@ -0,0 +1,424 @@ +import plugin from 'tailwindcss/plugin'; +import defaultTheme from 'tailwindcss/defaultTheme'; +import colors from 'tailwindcss/colors'; +const { spacing, borderWidth, borderRadius } = defaultTheme; + +// https://github.com/tailwindlabs/tailwindcss/discussions/9336 +// https://github.com/tailwindlabs/tailwindcss/discussions/2049 +// https://github.com/tailwindlabs/tailwindcss/discussions/2049#discussioncomment-45546 + +const svgToTinyDataUri = (() => { + // Source: https://github.com/tigt/mini-svg-data-uri + const reWhitespace = /\s+/g, + reUrlHexPairs = /%[\dA-F]{2}/g, + hexDecode = { '%20': ' ', '%3D': '=', '%3A': ':', '%2F': '/' }, + specialHexDecode = match => hexDecode[match] || match.toLowerCase(), + svgToTinyDataUri = svg => { + svg = String(svg); + if (svg.charCodeAt(0) === 0xfeff) svg = svg.slice(1); + svg = svg + .trim() + .replace(reWhitespace, ' ') + .replaceAll('"', '\''); + svg = encodeURIComponent(svg); + svg = svg.replace(reUrlHexPairs, specialHexDecode); + return 'data:image/svg+xml,' + svg; + }; + svgToTinyDataUri.toSrcset = svg => svgToTinyDataUri(svg).replace(/ /g, '%20'); + return svgToTinyDataUri; +})(); + +export default plugin( + function({ addBase, addComponents, theme }) { + addBase({ + [[ + "[type='text']", + "[type='email']", + "[type='url']", + "[type='password']", + "[type='number']", + "[type='date']", + "[type='datetime-local']", + "[type='month']", + "[type='search']", + "[type='tel']", + "[type='time']", + "[type='week']", + 'textarea', + 'select', + ]]: { + '@apply dark:scheme-dark': {}, + 'appearance': 'none', + 'padding': `${spacing[2]} ${spacing[3]}`, + '--tw-shadow': '0 0 #0000', + '&:focus': { + outline: '2px solid transparent', + 'outline-offset': '2px', + '--tw-ring-inset': 'var(--tw-empty,/*!*/ /*!*/)', + '--tw-ring-offset-width': '0px', + '--tw-ring-offset-color': '#fff', + '--tw-ring-color': theme( + 'colors.blue.600', + colors.blue[600] + ), + '--tw-ring-offset-shadow': `var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)`, + '--tw-ring-shadow': `var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)`, + 'box-shadow': `var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)`, + 'border-color': theme('colors.blue.600', colors.blue[600]), + }, + }, + ['::-webkit-datetime-edit']: { + display: 'inline-flex', + }, + [[ + '::-webkit-datetime-edit', + '::-webkit-datetime-edit-year-field', + '::-webkit-datetime-edit-month-field', + '::-webkit-datetime-edit-day-field', + '::-webkit-datetime-edit-hour-field', + '::-webkit-datetime-edit-minute-field', + '::-webkit-datetime-edit-second-field', + '::-webkit-datetime-edit-millisecond-field', + '::-webkit-datetime-edit-meridiem-field', + ]]: { + 'padding-bottom': '0', + 'padding-top': '0', + }, + ['::-webkit-date-and-time-value']: { + 'min-height': '1.5em', + 'text-align': 'inherit', + }, + ['select']: { + 'background-image': `url("${svgToTinyDataUri( + `` + )}")`, + 'background-position': `right ${spacing[3]} center`, + 'background-repeat': `no-repeat`, + 'background-size': `0.75em 0.75em`, + 'padding-inline-end': spacing[8], + 'print-color-adjust': `exact`, + }, + [':is(:where([dir=rtl]) select)']: { + 'background-position': `left ${spacing[3]} center`, + }, + ['[multiple]']: { + 'background-image': 'initial', + 'background-position': 'initial', + 'background-repeat': 'unset', + 'background-size': 'initial', + 'padding-inline-end': spacing[3], + 'print-color-adjust': 'unset', + }, + [[`[type='checkbox']`, `[type='radio']`]]: { + appearance: 'none', + 'background-origin': 'border-box', + color: theme('colors.blue.600', colors.blue[600]), + display: 'inline-block', + 'flex-shrink': '0', + 'print-color-adjust': 'exact', + 'user-select': 'none', + 'vertical-align': 'middle', + '--tw-shadow': '0 0 #0000', + }, + [[`[type='checkbox']:focus`, `[type='radio']:focus`]]: { + outline: '2px solid transparent', + 'outline-offset': '2px', + '--tw-ring-inset': 'var(--tw-empty,/*!*/ /*!*/)', + '--tw-ring-offset-width': '2px', + '--tw-ring-offset-color': '#fff', + '--tw-ring-color': theme('colors.blue.600', colors.blue[600]), + '--tw-ring-offset-shadow': `var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)`, + '--tw-ring-shadow': `var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)`, + 'box-shadow': `var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)`, + }, + [[ + `[type='checkbox']:checked`, + `[type='checkbox']:indeterminate`, + `[type='radio']:checked`, + ]]: { + 'background-color': `currentColor`, + 'background-position': `center`, + 'background-repeat': `no-repeat`, + 'background-size': `100% 100%`, + 'border-color': `transparent`, + }, + [`[type='checkbox']:checked`]: { + 'background-image': `url("${svgToTinyDataUri( + ` + + ` + )}")`, + 'print-color-adjust': `exact`, + }, + [`[type='radio']:checked`]: { + 'background-image': `url("${svgToTinyDataUri( + `` + )}")`, + }, + [`[type='checkbox']:indeterminate`]: { + 'background-image': `url("${svgToTinyDataUri( + ` + + ` + )}")`, + 'print-color-adjust': `exact`, + }, + [[ + `button:not(:disabled)`, + `[role='button']:not(:disabled)`, + `[type='file']`, + ]]: { + cursor: 'pointer', + }, + [`[type=file]::file-selector-button`]: { + 'background-color': theme('colors.gray.100', colors.gray[100]), + 'border': `${borderWidth['DEFAULT']} solid ${theme('colors.gray.200', colors.gray[200])}`, + 'border-radius': borderRadius['DEFAULT'], + cursor: 'pointer', + 'padding': `${spacing[2]} ${spacing[3]}`, + '&:hover': { + 'background-color': theme('colors.gray.200', colors.gray[200]), + }, + }, + [`.dark [type=file]::file-selector-button`]: { + '@apply bg-white/5 border-white/5 text-white hover:bg-white/10': {} + }, + '[type=checkbox]': { + '@apply w-4 h-4 bg-gray-100 border border-gray-300 rounded-sm focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-white/5 dark:border-white/10': {} + }, + '[type=radio]': { + '@apply w-4 h-4 bg-gray-100 border border-gray-300 rounded-full focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-600 dark:focus:bg-blue-600 dark:bg-white/5 dark:border-white/10': {} + }, + [['[type=datetime-local]', '[type=month]', '[type=week]', '[type=search]', '[type=date]', '[type=email]', '[type=number]', '[type=password]', '[type=tel]', '[type=text]', '[type=time]', '[type=url]', 'select', 'textarea']]: { + '@apply bg-gray-50 border border-gray-300 text-gray-900 placeholder:text-gray-400 rounded-md focus:ring-blue-500 focus:border-blue-500 w-full dark:bg-white/5 dark:border-white/10 dark:text-white dark:placeholder:text-gray-500 dark:focus:ring-blue-500 dark:focus:border-blue-500': {} + }, + [':where(select:not([multiple]))']: { + 'option, optgroup': { + '@apply dark:bg-gray-800': {} + } + }, + 'a': { + '@apply text-blue-600 dark:text-blue-500 underline underline-offset-[.2rem]': {} + }, + }); + addComponents({ + '.action-item-button': { + '@apply py-2 px-3 text-sm font-medium no-underline text-gray-900 focus:outline-hidden bg-white rounded-md border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-700 dark:hover:text-white dark:hover:bg-gray-700': {} + }, + '.index-data-table-toolbar': { + '@apply flex flex-col lg:flex-row gap-4 mb-4': {} + }, + '.scopes': { + '@apply flex flex-wrap gap-1.5': {} + }, + '.index-button-group': { + '@apply inline-flex flex-wrap items-stretch rounded-md': {} + }, + // Prevent double borders when buttons are next to each other + '.index-button-group > :where(*:not(:first-child))': { + '@apply -ms-px my-0': {} + }, + '.index-button': { + '@apply inline-flex items-center justify-center px-3 py-2 text-sm font-medium no-underline text-gray-900 bg-white border border-gray-200 hover:bg-gray-100 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 first:rounded-s-md last:rounded-e-md dark:bg-gray-900 dark:border-gray-700 dark:text-gray-100 dark:hover:text-gray-200 dark:hover:bg-gray-800 dark:focus:ring-blue-500 dark:focus:text-white': {} + }, + '.index-button-selected': { + '@apply bg-gray-100 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-800': {} + }, + '.scopes-count': { + '@apply inline-flex items-center justify-center rounded-full bg-indigo-100 text-indigo-700 dark:bg-indigo-800/60 dark:text-indigo-400 px-1.5 py-1 text-xs font-normal ms-2 leading-none': {} + }, + '.paginated-collection': { + '@apply border border-gray-200 dark:border-gray-800 rounded-md shadow-xs overflow-hidden': {} + }, + '.paginated-collection-contents': { + '@apply overflow-x-auto': {} + }, + '.paginated-collection-pagination': { + '@apply p-2 lg:p-3 flex flex-col-reverse lg:flex-row gap-4 items-center justify-between border-t border-gray-200 dark:border-gray-800': {} + }, + '.paginated-collection-footer': { + '@apply p-3 flex gap-2 items-center justify-between text-sm border-t border-gray-200 dark:border-gray-800': {} + }, + '.pagination-per-page': { + '@apply text-sm py-1 pe-7 w-auto w-min': {} + }, + '.index-as-table': { + '@apply relative overflow-x-auto': {} + }, + '.data-table': { + '@apply w-full text-sm text-gray-800 dark:text-gray-300': {} + }, + '.data-table :where(thead > tr > th)': { + '@apply px-3 py-3.5 font-semibold text-start text-xs uppercase border-b border-gray-200 text-gray-700 bg-gray-50 dark:bg-gray-950/50 dark:border-gray-800 dark:text-white': {} + }, + '.data-table :where(thead > tr > th > a)': { + '@apply text-inherit no-underline inline-flex items-center gap-2': {} + }, + '.data-table-sorted-icon': { + '@apply invisible w-[8px] h-[5px]': {} + }, + ':where(th[data-sort-direction]) .data-table-sorted-icon': { + '@apply visible': {} + }, + ':where(th[data-sort-direction="asc"]) .data-table-sorted-icon': { + '@apply rotate-180': {} + }, + '.data-table :where(tbody > tr)': { + '@apply border-b border-gray-200 dark:border-gray-800 last:border-b-0': {} + }, + '.data-table :where(td)': { + '@apply px-3 py-4': {} + }, + '.data-table-resource-actions': { + '@apply flex gap-2': {} + }, + '.filters-form': { + '@apply text-sm mb-6': {} + }, + '.filters-form-title': { + '@apply text-gray-700 dark:text-gray-200 font-bold text-lg mb-4': {} + }, + '.filters-form :where(.label)': { + '@apply block mb-1.5 text-sm': {} + }, + '.filters-form-input-group': { + '@apply grid grid-cols-2 gap-2': {} + }, + '.filters-form-field': { + '@apply mb-4': {} + }, + '.filters-form-field :where(.choices > label)': { + '@apply flex gap-2 items-center mb-1': {} + }, + '.filters-form-buttons': { + '@apply flex gap-2 items-center': {} + }, + '.filters-form-submit': { + '@apply min-w-24 font-bold text-white bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:outline-hidden focus:ring-blue-300 rounded-md px-3 py-2 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 cursor-pointer': {} + }, + '.filters-form-clear': { + '@apply rounded-md px-3 py-2 font-semibold text-gray-700 hover:bg-gray-100 no-underline dark:text-gray-400 dark:hover:bg-inherit dark:hover:text-gray-100 dark:focus:ring-blue-800': {} + }, + '.active-filters-title': { + '@apply text-gray-700 dark:text-gray-200 font-bold text-lg mb-4': {} + }, + '.active-filters-list': { + '@apply ps-5 list-disc space-y-1 text-gray-700 dark:text-gray-200': {} + }, + '.batch-actions-dropdown': { + '@apply relative': {} + }, + '.batch-actions-dropdown-toggle': { + '@apply transition-opacity rounded-md inline-flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:hover:text-white dark:hover:bg-gray-700 dark:focus:ring-blue-500 dark:focus:text-white disabled:text-gray-400 disabled:border-gray-200/70 dark:disabled:bg-gray-900 dark:disabled:text-gray-700 dark:disabled:border-gray-800 disabled:pointer-events-none': {} + }, + '.batch-actions-dropdown-arrow': { + '@apply w-2.5 h-2.5': {} + }, + '.batch-actions-dropdown-menu': { + '@apply z-10 hidden min-w-28 bg-white rounded-md shadow-lg ring-1 ring-black/5 focus:outline-hidden dark:bg-gray-800 py-1 text-sm text-gray-700 dark:text-gray-200': {} + }, + '.batch-actions-dropdown-menu :where(li > a)': { + '@apply block px-2.5 py-2 no-underline text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-white dark:hover:bg-gray-700 dark:hover:text-white': {} + }, + '.panel': { + '@apply mb-6 border border-gray-200 rounded-md shadow-xs dark:border-gray-800': {} + }, + '.panel-title': { + '@apply font-bold bg-gray-100 dark:bg-gray-950/50 rounded-t-md p-3': {} + }, + '.panel-body': { + '@apply py-5 px-3': {} + }, + '.attributes-table': { + '@apply overflow-hidden mb-6 border border-gray-200 rounded-md shadow-xs dark:border-gray-800': {} + }, + '.attributes-table > :where(table)': { + '@apply w-full text-sm text-gray-800 dark:text-gray-300': {} + }, + '.attributes-table :where(tbody > tr)': { + '@apply border-b border-gray-200 dark:border-gray-800 last:border-b-0 align-baseline': {} + }, + '.attributes-table :where(tbody > tr > th)': { + '@apply w-32 sm:w-40 text-start text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-950/50 dark:text-gray-300': {} + }, + '.attributes-table :where(tbody > tr > th, tbody > tr > td)': { + '@apply p-3': {} + }, + '.attributes-table-empty-value': { + '@apply text-gray-400/50 dark:text-gray-600/50 text-xs uppercase font-semibold': {} + }, + '.status-tag': { + '@apply bg-gray-200 text-gray-600 dark:bg-gray-400/20 dark:text-gray-400 inline-flex items-center rounded-full text-sm font-medium px-2.5 py-0.5 whitespace-nowrap': {} + }, + '.status-tag:where([data-status=yes])': { + '@apply bg-green-100 text-green-700 dark:bg-green-400/20 dark:text-green-400': {} + }, + // Forms + '.formtastic': { + '@apply text-sm': {} + }, + '.formtastic :where(.inputs,.has-many-fields)': { + '@apply mb-6': {} + }, + '.formtastic :where(.fieldset-title, .has-many-fields-title)': { + '@apply block w-full mb-3 border-b border-gray-200 dark:border-gray-800 font-bold text-lg': {} + }, + '.formtastic :where(.label)': { + '@apply block mb-1.5': {} + }, + '.formtastic :where(.label abbr)': { + '@apply ms-1 no-underline': {} + }, + '.formtastic :where(.input)': { + '@apply py-3': {} + }, + '.formtastic :where(.choice)': { + '@apply mb-1': {} + }, + '.formtastic :where(.boolean label, .choice label)': { + '@apply flex gap-2 items-center': {} + }, + '.formtastic :where(.fragments-group)': { + '@apply inline-flex flex-wrap gap-1': {} + }, + '.formtastic :where(.fragment label)': { + '@apply sr-only': {} + }, + '.formtastic :where(.inline-hints)': { + '@apply text-gray-500 dark:text-gray-400 mt-2': {} + }, + '.formtastic :where(.errors)': { + '@apply p-4 mb-6 rounded-md space-y-2 bg-red-50 text-red-800 dark:bg-red-500/15 dark:text-red-200': {} + }, + '.formtastic :where(.errors > li)': { + '@apply list-disc ms-4': {} + }, + '.formtastic :where(.inline-errors)': { + '@apply font-bold mt-2 text-red-600 dark:text-red-400': {} + }, + '.formtastic :where(.error [type=email], .error [type=number], .error [type=password], .error [type=tel], .error [type=text], .error [type=url], .error select, .error textarea)': { + '@apply border-red-500/50': {} + }, + '.formtastic :where(.buttons, .actions)': { + '@apply mt-3': {} + }, + '.formtastic :where(.actions > ol)': { + '@apply flex items-center gap-6': {} + }, + '.formtastic :where([type=submit], [type=button], button)': { + '@apply font-bold text-white bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:outline-hidden focus:ring-blue-300 rounded-lg px-4 py-2 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 cursor-pointer': {} + }, + '.formtastic :where(.actions .cancel-link)': { + '@apply font-semibold leading-6 text-gray-900 dark:text-white no-underline': {} + }, + '.formtastic :where(.has-many-add)': { + '@apply inline-block py-3 mb-3': {} + }, + '.formtastic :where(.has-many-fields)': { + '@apply ps-3 border-s-4 border-s-gray-200 dark:border-s-gray-700': {} + } + }); + } +) diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 00000000000..fe904e03bf7 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,40 @@ +import path from 'node:path'; +import { URL, fileURLToPath } from 'node:url'; +import { readFileSync } from 'node:fs'; +import alias from '@rollup/plugin-alias'; + +const packageJson = JSON.parse( + readFileSync(new URL('./package.json', import.meta.url)) +); + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const projectRootDir = path.resolve(__dirname); +const assetsDir = path.resolve(projectRootDir, 'app/javascript'); + + +/** + * @type {import('rollup').RollupOptions} + */ +export default [ + // build dist folder with all files from app/javascript using relative imports. + // let bundler tools like webpack or rollup to process our package + { + input: ['app/javascript/active_admin.js'], + output: { + format: 'es', + dir: 'dist', + preserveModules: true, + }, + external: Object.keys(packageJson.dependencies), + plugins: [ + alias({ + entries: [ + { + find: 'active_admin', + replacement: path.join(assetsDir, 'active_admin'), + }, + ] + }) + ] + } +]; diff --git a/script/local b/script/local deleted file mode 100755 index 905b3a65921..00000000000 --- a/script/local +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env ruby - -require File.expand_path('../../spec/support/detect_rails_version', __FILE__) - -unless ARGV[0] - puts <<-EOF -Usage: ./script/#{__FILE__} COMMAND [ARGS] - -The command will be run in the context of the local rails -app stored in test-rails-app. - -Examples: - -./script/local server -./script/local c -./script/local rake db:migrate - EOF - exit(1) -end - -# Set up some variables -rails_version = detect_rails_version || '3.0.0' - -test_app_dir = ".test-rails-apps" -test_app_path = "#{test_app_dir}/test-rails-app-#{rails_version}" - -# Ensure .test-rails-apps is created -system "mkdir #{test_app_dir}" unless File.exists?(test_app_dir) - -# Create the sample rails app if it doesn't already exist -unless File.exists? test_app_path - system "RAILS='#{rails_version}' bundle exec rails new #{test_app_path} -m spec/support/rails_template_with_data.rb" -end - -# Link this rails app -system "rm test-rails-app" -system "ln -s #{test_app_path} test-rails-app" - -# If it's a rails command, auto add the rails script -RAILS_COMMANDS = %w{generate console server dbconsole g c s runner} -args = RAILS_COMMANDS.include?(ARGV[0]) ? ["rails", ARGV].flatten : ARGV - -# Run the command -exec "cd test-rails-app && GEMFILE=../Gemfile bundle exec #{args.join(" ")}" diff --git a/script/use_rails b/script/use_rails deleted file mode 100755 index fcf0ff079af..00000000000 --- a/script/use_rails +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env ruby -# -# Switches the development environment to use the given -# version of rails. Caches the Gemfile.locks so that -# switching it very fast. -# -require File.expand_path("../../spec/support/detect_rails_version", __FILE__) - -def cmd(command) - puts command - exit 1 unless system command -end - -version = ARGV[0] - -unless version - puts "USAGE: ./script/#{__FILE__} VERSION [OPTIONS]" - puts - puts "Options:" - puts " --clobber Add this flag to remove the existing Gemfile.lock before running" - exit(1) -end - -def file_or_symlink?(path) - File.exist?(path) || File.symlink?(path) -end - -gem_lock_dir = ".gemfile-locks" -gem_lock_file = "#{gem_lock_dir}/Gemfile-#{version}.lock" - -# Ensure our lock dir is created -cmd "mkdir #{gem_lock_dir}" unless File.exists?(gem_lock_dir) - -# --clobber passed in -if File.exists?(gem_lock_file) && ARGV.include?('--clobber') - cmd "rm #{gem_lock_file}" -end - -write_rails_version(version) - -# Ensure that bundler installs -ENV['RUBYOPT'] = '' - -if File.exists?(gem_lock_file) - cmd("rm Gemfile.lock") if file_or_symlink?("Gemfile.lock") - cmd("ln -s #{gem_lock_file} Gemfile.lock") - cmd("bundle") -else - cmd "rm Gemfile.lock" if file_or_symlink?("Gemfile.lock") - cmd "bundle install" - cmd "mv Gemfile.lock #{gem_lock_file}" - cmd("ln -s #{gem_lock_file} Gemfile.lock") -end diff --git a/spec/helpers/auto_link_helper_spec.rb b/spec/helpers/auto_link_helper_spec.rb new file mode 100644 index 00000000000..5bf2407c281 --- /dev/null +++ b/spec/helpers/auto_link_helper_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe ActiveAdmin::AutoLinkHelper, type: :helper do + let(:linked_post) { helper.auto_link(post) } + + let(:active_admin_namespace) { ActiveAdmin.application.namespace(:admin) } + let(:post) { Post.create! title: "Hello World" } + + before do + helper.class.send(:include, ActiveAdmin::DisplayHelper) + helper.class.send(:include, ActiveAdmin::LayoutHelper) + helper.class.send(:include, MethodOrProcHelper) + allow(helper).to receive(:authorized?).and_return(true) + allow(helper).to receive(:active_admin_namespace).and_return(active_admin_namespace) + allow(helper).to receive(:url_options).and_return({}) + end + + context "when the resource is not registered" do + before do + load_resources {} + end + + it "should return the display name of the object" do + expect(linked_post).to eq "Hello World" + end + end + + context "when the resource is registered" do + before do + load_resources do + active_admin_namespace.register Post + end + end + + it "should return a link with the display name of the object" do + expect(linked_post).to \ + match(%r{Hello World}) + end + + it "should keep locale in the url if present" do + expect(helper).to receive(:url_options).and_return(locale: "en") + + expect(linked_post).to \ + match(%r{Hello World}) + end + + context "but the user doesn't have access" do + before do + allow(helper).to receive(:authorized?).and_return(false) + end + + it "should return the display name of the object" do + expect(linked_post).to eq "Hello World" + end + end + end + + context "when the resource is registered with the show action disabled" do + before do + load_resources do + active_admin_namespace.register(Post) { actions :all, except: :show } + end + end + + it "should fallback to edit" do + expect(linked_post).to \ + match(%r{Hello World}) + end + + it "should keep locale in the url if present" do + expect(helper).to receive(:url_options).and_return(locale: "en") + + expect(linked_post).to \ + match(%r{Hello World}) + end + end + + context "when the resource is registered with the show & edit actions disabled" do + before do + load_resources do + active_admin_namespace.register(Post) do + actions :all, except: [:show, :edit] + end + end + end + + it "should return the display name of the object" do + expect(linked_post).to eq "Hello World" + end + end +end diff --git a/spec/helpers/breadcrumb_helper_spec.rb b/spec/helpers/breadcrumb_helper_spec.rb new file mode 100644 index 00000000000..13c05201ac7 --- /dev/null +++ b/spec/helpers/breadcrumb_helper_spec.rb @@ -0,0 +1,281 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe ActiveAdmin::BreadcrumbHelper, type: :helper do + describe "generating a trail from paths" do + let(:actions) { ActiveAdmin::BaseController::ACTIVE_ADMIN_ACTIONS } + + let(:user) { double display_name: "Jane Doe" } + let(:user_config) do + double find_resource: user, resource_name: double(route_key: "users"), + defined_actions: actions + end + let(:post) { double display_name: "Hello World" } + let(:post_config) do + double find_resource: post, resource_name: double(route_key: "posts"), + defined_actions: actions, breadcrumb: true, belongs_to_config: double(target: user_config) + end + + let :active_admin_config do + post_config + end + + let(:trail) do + helper.class.send(:include, ActiveAdmin::DisplayHelper) + helper.class.send(:include, ActiveAdmin::LayoutHelper) + helper.class.send(:include, MethodOrProcHelper) + allow(helper).to receive(:link_to) { |name, url| { name: name, path: url } } + allow(helper).to receive(:active_admin_config).and_return(active_admin_config) + helper.build_breadcrumb_links(path) + end + + context "when request '/admin'" do + let(:path) { "/admin" } + + it "should not have any items" do + expect(trail.size).to eq 0 + end + end + + context "when path 'admin/users'" do + let(:path) { "admin/users" } + + it "should have one item" do + expect(trail.size).to eq 1 + end + + it "should have a link to /admin" do + expect(trail[0][:name]).to eq "Admin" + expect(trail[0][:path]).to eq "/admin" + end + end + + context "when path '/admin/users'" do + let(:path) { "/admin/users" } + + it "should have one item" do + expect(trail.size).to eq 1 + end + + it "should have a link to /admin" do + expect(trail[0][:name]).to eq "Admin" + expect(trail[0][:path]).to eq "/admin" + end + end + + context "when path '/admin/users/1'" do + let(:path) { "/admin/users/1" } + + it "should have 2 items" do + expect(trail.size).to eq 2 + end + + it "should have a link to /admin" do + expect(trail[0][:name]).to eq "Admin" + expect(trail[0][:path]).to eq "/admin" + end + + it "should have a link to /admin/users" do + expect(trail[1][:name]).to eq "Users" + expect(trail[1][:path]).to eq "/admin/users" + end + end + + context "when path '/admin/users/1/posts'" do + let(:path) { "/admin/users/1/posts" } + + it "should have 3 items" do + expect(trail.size).to eq 3 + end + + it "should have a link to /admin" do + expect(trail[0][:name]).to eq "Admin" + expect(trail[0][:path]).to eq "/admin" + end + + it "should have a link to /admin/users" do + expect(trail[1][:name]).to eq "Users" + expect(trail[1][:path]).to eq "/admin/users" + end + + context "when User.find(1) doesn't exist" do + before { allow(user_config).to receive(:find_resource) } + it "should have a link to /admin/users/1" do + expect(trail[2][:name]).to eq "1" + expect(trail[2][:path]).to eq "/admin/users/1" + end + end + + context "when User.find(1) does exist" do + it "should have a link to /admin/users/1 using display name" do + expect(trail[2][:name]).to eq "Jane Doe" + expect(trail[2][:path]).to eq "/admin/users/1" + end + end + end + + context "when path '/admin/users/4e24d6249ccf967313000000/posts'" do + let(:path) { "/admin/users/4e24d6249ccf967313000000/posts" } + + it "should have 3 items" do + expect(trail.size).to eq 3 + end + + it "should have a link to /admin" do + expect(trail[0][:name]).to eq "Admin" + expect(trail[0][:path]).to eq "/admin" + end + + it "should have a link to /admin/users" do + expect(trail[1][:name]).to eq "Users" + expect(trail[1][:path]).to eq "/admin/users" + end + + context "when User.find(4e24d6249ccf967313000000) doesn't exist" do + before { allow(user_config).to receive(:find_resource) } + it "should have a link to /admin/users/4e24d6249ccf967313000000" do + expect(trail[2][:name]).to eq "4e24d6249ccf967313000000" + expect(trail[2][:path]).to eq "/admin/users/4e24d6249ccf967313000000" + end + end + + context "when User.find(4e24d6249ccf967313000000) does exist" do + before do + display_name = double(display_name: "Hello :)") + allow(user_config).to receive(:find_resource).and_return(display_name) + end + it "should have a link to /admin/users/4e24d6249ccf967313000000 using display name" do + expect(trail[2][:name]).to eq "Hello :)" + expect(trail[2][:path]).to eq "/admin/users/4e24d6249ccf967313000000" + end + end + end + + context "when path '/admin/users/2b2f0fc2-9a0d-41b8-b39d-aa21963aaee4/posts'" do + let(:path) { "/admin/users/2b2f0fc2-9a0d-41b8-b39d-aa21963aaee4/posts" } + + it "should have 3 items" do + expect(trail.size).to eq 3 + end + + it "should have a link to /admin" do + expect(trail[0][:name]).to eq "Admin" + expect(trail[0][:path]).to eq "/admin" + end + + it "should have a link to /admin/users" do + expect(trail[1][:name]).to eq "Users" + expect(trail[1][:path]).to eq "/admin/users" + end + + context "when User.find(2b2f0fc2-9a0d-41b8-b39d-aa21963aaee4) doesn't exist" do + before { allow(user_config).to receive(:find_resource) } + it "should have a link to /admin/users/2b2f0fc2-9a0d-41b8-b39d-aa21963aaee4" do + expect(trail[2][:name]).to eq "2b2f0fc2-9a0d-41b8-b39d-aa21963aaee4".titlecase + expect(trail[2][:path]).to eq "/admin/users/2b2f0fc2-9a0d-41b8-b39d-aa21963aaee4" + end + end + + context "when User.find(2b2f0fc2-9a0d-41b8-b39d-aa21963aaee4) does exist" do + before do + display_name = double(display_name: "Hello :)") + allow(user_config).to receive(:find_resource).and_return(display_name) + end + it "should have a link to /admin/users/2b2f0fc2-9a0d-41b8-b39d-aa21963aaee4 using display name" do + expect(trail[2][:name]).to eq "Hello :)" + expect(trail[2][:path]).to eq "/admin/users/2b2f0fc2-9a0d-41b8-b39d-aa21963aaee4" + end + end + end + + context "when path '/admin/users/1/posts/1'" do + let(:path) { "/admin/users/1/posts/1" } + + it "should have 4 items" do + expect(trail.size).to eq 4 + end + + it "should have a link to /admin" do + expect(trail[0][:name]).to eq "Admin" + expect(trail[0][:path]).to eq "/admin" + end + + it "should have a link to /admin/users" do + expect(trail[1][:name]).to eq "Users" + expect(trail[1][:path]).to eq "/admin/users" + end + + it "should have a link to /admin/users/1" do + expect(trail[2][:name]).to eq "Jane Doe" + expect(trail[2][:path]).to eq "/admin/users/1" + end + + it "should have a link to /admin/users/1/posts" do + expect(trail[3][:name]).to eq "Posts" + expect(trail[3][:path]).to eq "/admin/users/1/posts" + end + end + + context "when path '/admin/users/1/posts/1/edit'" do + let(:path) { "/admin/users/1/posts/1/edit" } + + it "should have 5 items" do + expect(trail.size).to eq 5 + end + + it "should have a link to /admin" do + expect(trail[0][:name]).to eq "Admin" + expect(trail[0][:path]).to eq "/admin" + end + + it "should have a link to /admin/users" do + expect(trail[1][:name]).to eq "Users" + expect(trail[1][:path]).to eq "/admin/users" + end + + it "should have a link to /admin/users/1" do + expect(trail[2][:name]).to eq "Jane Doe" + expect(trail[2][:path]).to eq "/admin/users/1" + end + + it "should have a link to /admin/users/1/posts" do + expect(trail[3][:name]).to eq "Posts" + expect(trail[3][:path]).to eq "/admin/users/1/posts" + end + + it "should have a link to /admin/users/1/posts/1" do + expect(trail[4][:name]).to eq "Hello World" + expect(trail[4][:path]).to eq "/admin/users/1/posts/1" + end + end + + context "when the 'show' action is disabled" do + let(:post_config) do + double find_resource: post, resource_name: double(route_key: "posts"), + defined_actions: actions - [:show], # this is the change + breadcrumb: true, + belongs_to_config: double(target: user_config) + end + + let(:path) { "/admin/posts/1/edit" } + + it "should have 3 items" do + expect(trail.size).to eq 3 + end + + it "should have a link to /admin" do + expect(trail[0][:name]).to eq "Admin" + expect(trail[0][:path]).to eq "/admin" + end + + it "should have a link to /admin/posts" do + expect(trail[1][:name]).to eq "Posts" + expect(trail[1][:path]).to eq "/admin/posts" + end + + it "should not link to the show view for the post" do + expect(trail[2]).to eq "Hello World" + end + end + end +end diff --git a/spec/helpers/display_helper_spec.rb b/spec/helpers/display_helper_spec.rb new file mode 100644 index 00000000000..9792d20437c --- /dev/null +++ b/spec/helpers/display_helper_spec.rb @@ -0,0 +1,400 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe ActiveAdmin::DisplayHelper, type: :helper do + let(:active_admin_namespace) { helper.active_admin_application.namespaces[:admin] } + let(:displayed_name) { helper.display_name(resource) } + + before do + helper.class.send(:include, ActiveAdmin::LayoutHelper) + helper.class.send(:include, ActiveAdmin::AutoLinkHelper) + helper.class.send(:include, MethodOrProcHelper) + allow(helper).to receive(:authorized?).and_return(true) + allow(helper).to receive(:active_admin_namespace).and_return(active_admin_namespace) + allow(helper).to receive(:url_options).and_return(locale: nil) + + load_resources do + ActiveAdmin.register(User) + ActiveAdmin.register(Post) { belongs_to :user, optional: true } + end + end + + describe "display name fallback constant" do + let(:fallback_proc) { described_class::DISPLAY_NAME_FALLBACK } + + it "sets the proc to be inspectable" do + expect(fallback_proc.inspect).to eq "DISPLAY_NAME_FALLBACK" + end + + it "returns a primary key only if class has no model name" do + resource_class = Class.new do + def self.primary_key + :id + end + + def id + 123 + end + end + + expect(helper.render_in_context(resource_class.new, fallback_proc)).to eq " #123" + end + end + + describe "#display_name" do + let(:resource) { klass.new } + + ActiveAdmin::Application.new.display_name_methods.map(&:to_s).each do |m| + context "when it is the identity" do + let(:klass) do + Class.new do + define_method(m) { m } + end + end + + it "should return #{m}" do + expect(displayed_name).to eq m + end + end + + context "when it includes js" do + let(:klass) do + Class.new do + define_method(m) { "" } + end + end + + it "should sanitize the result of #{m}" do + expect(displayed_name).to eq "<script>alert(1)</script>" + end + end + end + + describe "memoization" do + let(:klass) { Class.new } + + it "should memoize the result for the class" do + expect(resource).to receive(:name).and_return "My Name" + expect(displayed_name).to eq "My Name" + + expect(ActiveAdmin.application).to_not receive(:display_name_methods) + expect(displayed_name).to eq "My Name" + end + + it "should not call a method if it's an association" do + allow(klass).to receive(:reflect_on_all_associations).and_return [ double(name: :login) ] + allow(resource).to receive :login + expect(resource).to_not receive :login + allow(resource).to receive(:email).and_return "foo@bar.baz" + + expect(displayed_name).to eq "foo@bar.baz" + end + end + + context "when the passed object is `nil`" do + let(:resource) { nil } + + it "should return `nil` when the passed object is `nil`" do + expect(displayed_name).to eq nil + end + end + + context "when the passed object is `false`" do + let(:resource) { false } + + it "should return 'false' when the passed object is `false`" do + expect(displayed_name).to eq "false" + end + end + + describe "default implementation" do + let(:klass) { Class.new } + + it "should default to `to_s`" do + result = resource.to_s + + expect(displayed_name).to eq ERB::Util.html_escape(result) + end + end + + context "when no display name method is defined" do + context "when no ID" do + let(:resource) do + class ThisModel + extend ActiveModel::Naming + end + + ThisModel.new + end + + it "should show the model name" do + expect(displayed_name).to eq "This model" + end + end + + context "when ID" do + let(:resource) { Tagging.create! } + + it "should show the model name, plus the ID if in use" do + expect(displayed_name).to eq "Tagging #1" + end + + it "should translate the model name" do + with_translation %i[activerecord models tagging one], "Different" do + expect(displayed_name).to eq "Different #1" + end + end + end + end + end + + describe "#format_attribute" do + it "calls the provided block to format the value" do + value = helper.format_attribute double(foo: 2), ->r { r.foo + 1 } + + expect(value).to eq "3" + end + + it "finds values as methods" do + value = helper.format_attribute double(name: "Joe"), :name + + expect(value).to eq "Joe" + end + + it "finds values from hashes" do + value = helper.format_attribute({ id: 100 }, :id) + + expect(value).to eq "100" + end + + [1, 1.2, :a_symbol].each do |val| + it "calls to_s to format the value of type #{val.class}" do + value = helper.format_attribute double(foo: val), :foo + + expect(value).to eq val.to_s + end + end + + it "localizes dates" do + date = Date.parse "2016/02/28" + + value = helper.format_attribute double(date: date), :date + + expect(value).to eq "February 28, 2016" + end + + it "localizes times" do + time = Time.parse "2016/02/28 9:34 PM" + + value = helper.format_attribute double(time: time), :time + + expect(value).to eq "February 28, 2016 21:34" + end + + it "uses a display_name method for arbitrary objects" do + object = double to_s: :wrong, display_name: :right + + value = helper.format_attribute double(object: object), :object + + expect(value).to eq "right" + end + + it "auto-links ActiveRecord records by association with display name fallback" do + post = Post.create! author: User.new(first_name: "", last_name: "") + + value = helper.format_attribute post, :author + + expect(value).to match(/User \#\d+<\/a>/) + end + + it "auto-links ActiveRecord records & uses a display_name method" do + post = Post.create! author: User.new(first_name: "A", last_name: "B") + + value = helper.format_attribute post, :author + + expect(value).to match(/A B<\/a>/) + end + + it "calls status_tag for boolean values" do + post = Post.new starred: true + + value = helper.format_attribute post, :starred + + expect(value.to_s).to eq "Yes\n" + end + + context "with non-database boolean attribute" do + let(:model_class) do + Class.new(Post) do + attribute :a_virtual_attribute, :boolean + end + end + + it "calls status_tag even when attribute is nil" do + post = model_class.new a_virtual_attribute: nil + + value = helper.format_attribute post, :a_virtual_attribute + + expect(value.to_s).to eq "Unknown\n" + end + end + + it "calls status_tag for boolean non-database values" do + post = Post.new + post.define_singleton_method(:true_method) do + true + end + post.define_singleton_method(:false_method) do + false + end + true_value = helper.format_attribute post, :true_method + expect(true_value.to_s).to eq "Yes\n" + false_value = helper.format_attribute post, :false_method + expect(false_value.to_s).to eq "No\n" + end + + it "renders ActiveRecord relations as a list" do + tags = (1..3).map do |i| + Tag.create!(name: "abc#{i}") + end + post = Post.create!(tags: tags) + + value = helper.format_attribute post, :tags + + expect(value.to_s).to eq "abc1, abc2, abc3" + end + + it "renders arrays as a list" do + items = (1..3).map { |i| "abc#{i}" } + post = Post.create! + allow(post).to receive(:items).and_return(items) + + value = helper.format_attribute post, :items + + expect(value.to_s).to eq "abc1, abc2, abc3" + end + end + + describe "#pretty_format" do + let(:formatted_obj) { helper.pretty_format(obj) } + + shared_examples_for "an object convertible to string" do + it "should call `to_s` on the given object" do + expect(formatted_obj).to eq obj.to_s + end + end + + context "when given a string" do + let(:obj) { "hello" } + + it_behaves_like "an object convertible to string" + end + + context "when given an integer" do + let(:obj) { 23 } + + it_behaves_like "an object convertible to string" + end + + context "when given a float" do + let(:obj) { 5.67 } + + it_behaves_like "an object convertible to string" + end + + context "when given an exponential" do + let(:obj) { 10**30 } + + it_behaves_like "an object convertible to string" + end + + context "when given a symbol" do + let(:obj) { :foo } + + it_behaves_like "an object convertible to string" + end + + context "when given an arbre element" do + let(:obj) { Arbre::Element.new.br } + + it_behaves_like "an object convertible to string" + end + + shared_examples_for "a time-ish object" do + it "formats it with the default long format" do + expect(formatted_obj).to eq "February 28, 1985 20:15" + end + + it "formats it with a customized long format" do + with_translation %i[time formats long], "%B %d, %Y, %l:%M%P" do + expect(formatted_obj).to eq "February 28, 1985, 8:15pm" + end + end + + context "with a custom localize format" do + around do |example| + previous_localize_format = ActiveAdmin.application.localize_format + ActiveAdmin.application.localize_format = :short + example.call + ActiveAdmin.application.localize_format = previous_localize_format + end + + it "formats it with the default custom format" do + expect(formatted_obj).to eq "28 Feb 20:15" + end + + it "formats it with i18n custom format" do + with_translation %i[time formats short], "%-m %d %Y" do + expect(formatted_obj).to eq "2 28 1985" + end + end + end + + context "with non-English locale" do + around do |example| + I18n.with_locale(:es, &example) + end + + it "formats it with the default long format" do + expect(formatted_obj).to eq "28 de febrero de 1985 20:15" + end + + it "formats it with a customized long format" do + with_translation %i[time formats long], "El %d de %B de %Y a las %H horas y %M minutos" do + expect(formatted_obj).to eq "El 28 de febrero de 1985 a las 20 horas y 15 minutos" + end + end + end + end + + context "when given a Time in utc" do + let(:obj) { Time.utc(1985, "feb", 28, 20, 15, 1) } + + it_behaves_like "a time-ish object" + end + + context "when given a DateTime" do + let(:obj) { DateTime.new(1985, 2, 28, 20, 15, 1) } + + it_behaves_like "a time-ish object" + end + + context "given an ActiveRecord object" do + let(:obj) { Post.new } + + it "should delegate to auto_link" do + expect(view).to receive(:auto_link).with(obj).and_return("model name") + expect(formatted_obj).to eq "model name" + end + end + + context "given an arbitrary object" do + let(:obj) { Class.new.new } + + it "should delegate to `display_name`" do + expect(view).to receive(:display_name).with(obj) { "I'm not famous" } + expect(formatted_obj).to eq "I'm not famous" + end + end + end +end diff --git a/spec/helpers/filter_form_helper_spec.rb b/spec/helpers/filter_form_helper_spec.rb new file mode 100644 index 00000000000..218db44f03c --- /dev/null +++ b/spec/helpers/filter_form_helper_spec.rb @@ -0,0 +1,798 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe ActiveAdmin::FormHelper, type: :helper do + def render_filter(search, filters) + allow(helper).to receive(:collection_path).and_return("/posts") + allow(helper).to receive(:a_helper_method).and_return("A Helper Method") + render_arbre_component({ filter_args: [search, filters] }, helper) do + args = assigns[:filter_args] + kwargs = args.pop if args.last.is_a?(Hash) + text_node active_admin_filters_form_for(*args, **kwargs) + end.to_s + end + + def filter(name, options = {}) + Capybara.string(render_filter(scope, name => options)) + end + + let(:scope) { Post.ransack } + + before do + helper.class.send(:include, MethodOrProcHelper) + end + + describe "the form in general" do + let(:body) { filter :title } + + it "should generate a form which submits via get" do + expect(body).to have_css("form.filters-form[method=get]") + end + + it "should generate a filter button" do + expect(body).to have_button('Filter') + end + + it "should only generate the form once" do + expect(body).to have_css("form", count: 1) + end + + it "should generate a clear filters link" do + expect(body).to have_link("Clear Filters", class: "filters-form-clear") + end + + describe "label as proc" do + let(:body) { filter :title, label: proc { "Title from proc" } } + + it "should render proper label" do + expect(body).to have_css("label", text: "Title from proc") + end + end + + describe "input html as proc" do + let(:body) { filter :title, as: :select, input_html: proc { { 'data-ajax-url': "/" } } } + + it "should render proper label" do + expect(body).to have_css('select[data-ajax-url="/"]') + end + end + end + + describe "string attribute" do + let(:body) { filter :title } + + it "should generate a select option for starts with" do + expect(body).to have_css("option[value=title_start]", text: "Starts with") + end + + it "should generate a select option for ends with" do + expect(body).to have_css("option[value=title_end]", text: "Ends with") + end + + it "should generate a select option for contains" do + expect(body).to have_css("option[value=title_cont]", text: "Contains") + end + + it "should generate a text field for input" do + expect(body).to have_field("q[title_cont]") + end + + it "should have a proper label" do + expect(body).to have_css("label", text: "Title") + end + + it "should translate the label for text field" do + with_translation %i[activerecord attributes post title], "Name" do + expect(body).to have_css("label", text: "Name") + end + end + + it "should select the option which is currently being filtered" do + scope = Post.ransack title_start: "foo" + body = Capybara.string(render_filter scope, title: {}) + expect(body).to have_css("option[value=title_start][selected=selected]", text: "Starts with") + end + + context "with filters options" do + let(:body) { filter :title, filters: [:cont, :start] } + + it "should generate provided options for filter select" do + expect(body).to have_css("option[value=title_cont]", text: "Contains") + expect(body).to have_css("option[value=title_start]", text: "Starts with") + end + + it "should not generate a select option for ends with" do + expect(body).to have_no_css("option[value=title_end]") + end + end + + context "with predicate" do + %w[eq cont start end].each do |predicate| + describe "'#{predicate}'" do + let(:body) { filter :"title_#{predicate}" } + + it "shouldn't include a select field" do + expect(body).to have_no_select + end + + it "should build correctly" do + expect(body).to have_field("q[title_#{predicate}]") + end + end + end + end + + context "with namespace-level string_input_filters" do + let(:body) do + admin = ActiveAdmin.application.namespace(:admin) + admin.string_input_filters = [:eq, :cont] + allow(helper).to receive(:active_admin_config) + .and_return(double(string_input_filters: nil, namespace: admin)) + filter :title + ensure + admin.string_input_filters = nil + end + + it "uses the namespace operators (subset and order) instead of the global default" do + expect(body).to have_css("option[value=title_eq]", text: "Equals") + expect(body).to have_css("option[value=title_cont]", text: "Contains") + expect(body).to have_no_css("option[value=title_start]") + expect(body).to have_no_css("option[value=title_end]") + expect(helper).to have_received(:active_admin_config).at_least(:once) + end + end + + context "with resource-level string_input_filters" do + let(:body) do + allow(helper).to receive(:active_admin_config) + .and_return(double(string_input_filters: [:eq, :cont], namespace: nil)) + filter :title + end + + it "uses the resource operators (subset and order) instead of the global default" do + expect(body).to have_css("option[value=title_eq]", text: "Equals") + expect(body).to have_css("option[value=title_cont]", text: "Contains") + expect(body).to have_no_css("option[value=title_start]") + expect(body).to have_no_css("option[value=title_end]") + expect(helper).to have_received(:active_admin_config).at_least(:once) + end + end + + context "when active_admin_config is nil" do + let(:body) do + allow(helper).to receive(:active_admin_config).and_return(nil) + filter :title + end + + it "falls back to the global default" do + expect(body).to have_css("option[value=title_cont]", text: "Contains") + expect(body).to have_css("option[value=title_eq]", text: "Equals") + expect(body).to have_css("option[value=title_start]", text: "Starts with") + expect(body).to have_css("option[value=title_end]", text: "Ends with") + expect(helper).to have_received(:active_admin_config).at_least(:once) + end + end + + context "when active_admin_config is a Page (no string_input_filters accessor)" do + let(:body) do + admin = ActiveAdmin.application.namespace(:admin) + page_config = ActiveAdmin::Page.new(admin, "Report", {}) + allow(helper).to receive(:active_admin_config).and_return(page_config) + filter :title + end + + it "falls back to the global default" do + expect(body).to have_css("option[value=title_cont]", text: "Contains") + expect(body).to have_css("option[value=title_eq]", text: "Equals") + expect(body).to have_css("option[value=title_start]", text: "Starts with") + expect(body).to have_css("option[value=title_end]", text: "Ends with") + expect(helper).to have_received(:active_admin_config).at_least(:once) + end + end + + context "with both resource-level and namespace-level string_input_filters" do + let(:body) do + admin = ActiveAdmin.application.namespace(:admin) + admin.string_input_filters = [:cont, :start, :end] + allow(helper).to receive(:active_admin_config) + .and_return(double(string_input_filters: [:eq, :cont], namespace: admin)) + filter :title + ensure + admin.string_input_filters = nil + end + + it "lets the resource setting take priority over the namespace setting" do + expect(body).to have_css("option[value=title_eq]", text: "Equals") + expect(body).to have_css("option[value=title_cont]", text: "Contains") + expect(body).to have_no_css("option[value=title_start]") + expect(body).to have_no_css("option[value=title_end]") + expect(helper).to have_received(:active_admin_config).at_least(:once) + end + end + end + + describe "string attribute ended with ransack predicate" do + let(:scope) { User.ransack } + let(:body) { filter :reason_of_sign_in } + + it "should generate a select options" do + expect(body).to have_css("option[value=reason_of_sign_in_start]") + expect(body).to have_css("option[value=reason_of_sign_in_end]") + expect(body).to have_css("option[value=reason_of_sign_in_cont]") + end + end + + describe "text attribute" do + let(:body) { filter :body } + + it "should generate a search field for a text attribute" do + expect(body).to have_field("q[body_cont]") + end + + it "should have a proper label" do + expect(body).to have_css("label", text: "Body") + end + end + + describe "string attribute, as a select" do + let(:body) { filter :title, as: :select } + let(:builder) { ActiveAdmin::Inputs::Filters::SelectInput } + + context "when loading collection from DB" do + it "should use pluck for efficiency" do + expect_any_instance_of(builder).to receive(:pluck_column) { [] } + body + end + + it "should remove original ordering to prevent PostgreSQL error" do + expect(scope.object.klass).to receive(:reorder).with("title asc") { + m = double distinct: double(pluck: ["A Title"]) + expect(m.distinct).to receive(:pluck).with :title + m + } + body + end + + context "and a statement timeout error occurs" do + let(:body) { filter :title, as: :select, collection: ["foo"] } + let(:input_super_class) { Formtastic::Inputs::Base::Collections } + let(:db_timeout_exception) { ActiveRecord::QueryCanceled.new("ERROR: canceling statement due to statement timeout") } + let(:expected_exception_message) { "ERROR: canceling statement due to statement timeout while querying the values for the ActiveAdmin :title filter" } + + before do + expect_any_instance_of(input_super_class).to receive(:collection).and_raise(db_timeout_exception) + end + + it "should raise a database timeout error with a message indicating which filter was the cause" do + expect { body }.to raise_error(ActiveRecord::QueryCanceled, expected_exception_message) + end + end + end + end + + describe "enum attribute, as check boxes" do + let(:resource_klass) do + stub_const( + "EnumPost", + Class.new(Post) do + enum :position, { draft: 0, published: 1, archived: 2 } + end + ) + end + + let(:selected_values) { ["1"] } + let(:scope) { resource_klass.ransack(position_in: selected_values) } + let(:body) { filter :position, as: :check_boxes, collection: proc { EnumPost.positions } } + + it "preserves the selected checkbox from submitted string parameters" do + expect(body).to have_checked_field("q[position_in][]", with: "1") + expect(body).to have_unchecked_field("q[position_in][]", with: "0") + expect(body).to have_unchecked_field("q[position_in][]", with: "2") + end + + it "reads the selected values only once when rendering the checkbox collection" do + expect(scope).to receive(:position_in).once.and_call_original + + expect(body).to have_checked_field("q[position_in][]", with: "1") + end + + it "preserves the filtered results when the rendered form is submitted again" do + resource_klass.create!(position: :draft) + published_post = resource_klass.create!(position: :published) + resource_klass.create!(position: :archived) + expect(scope.result).to contain_exactly(published_post) + + values = body.all('input[type="checkbox"][checked]').map { |checkbox| checkbox[:value] } + resubmitted_scope = resource_klass.ransack(position_in: values) + + expect(resubmitted_scope.result).to contain_exactly(published_post) + end + + context "with string-valued collection options" do + let(:body) { filter :position, as: :check_boxes, collection: proc { EnumPost.positions.transform_values(&:to_s) } } + + it "preserves the selected checkbox" do + expect(body).to have_checked_field("q[position_in][]", with: "1") + expect(body).to have_unchecked_field("q[position_in][]", with: "0") + expect(body).to have_unchecked_field("q[position_in][]", with: "2") + end + end + + context "with multiple selected values" do + let(:selected_values) { ["0", "2"] } + + it "preserves every selected checkbox, including zero" do + expect(body).to have_checked_field("q[position_in][]", with: "0") + expect(body).to have_unchecked_field("q[position_in][]", with: "1") + expect(body).to have_checked_field("q[position_in][]", with: "2") + end + end + + context "without selected values" do + let(:selected_values) { [] } + + it "leaves every checkbox unchecked" do + expect(scope).to receive(:position_in).once.and_call_original + + expect(body).to have_unchecked_field("q[position_in][]", with: "0") + expect(body).to have_unchecked_field("q[position_in][]", with: "1") + expect(body).to have_unchecked_field("q[position_in][]", with: "2") + end + end + end + + describe "date attribute" do + let(:body) { filter :published_date } + + it "should generate a date greater than" do + expect(body).to have_field("q[published_date_gteq]", class: "datepicker") + end + + it "should generate a date less than" do + expect(body).to have_field("q[published_date_lteq]", class: "datepicker") + end + + it "should generate two inputs with different ids" do + ids = body.find_css("input.datepicker").to_a.map { |n| n[:id] } + expect(ids).to contain_exactly("q_published_date_lteq", "q_published_date_gteq") + end + + it "should generate one label without for attribute" do + label = body.find_css("label") + expect(label.length).to be(1) + expect(label.attr("for")).to be_nil + end + + context "with input_html" do + let(:body) { filter :published_date, input_html: { 'autocomplete': "off" } } + + it "should generate provided input html for both ends of date range" do + expect(body).to have_css("input.datepicker[name='q[published_date_gteq]'][autocomplete=off]") + expect(body).to have_css("input.datepicker[name='q[published_date_lteq]'][autocomplete=off]") + end + end + + context "with input_html overriding the defaults" do + let(:body) { filter :published_date, input_html: { 'class': "custom_class" } } + + it "should override the default attribute values for both ends of date range" do + expect(body).to have_field("q[published_date_gteq]", class: "custom_class") + expect(body).to have_field("q[published_date_lteq]", class: "custom_class") + end + end + end + + describe "datetime attribute" do + let(:body) { filter :created_at } + + it "should generate a date greater than" do + expect(body).to have_field("q[created_at_gteq]", class: "datepicker") + end + + it "should generate a date less than" do + expect(body).to have_field("q[created_at_lteq]", class: "datepicker") + end + + context "with input_html" do + let(:body) { filter :created_at, input_html: { 'autocomplete': "off" } } + + it "should generate provided input html for both ends of date range" do + expect(body).to have_css("input.datepicker[name='q[created_at_gteq]'][autocomplete=off]") + expect(body).to have_css("input.datepicker[name='q[created_at_lteq]'][autocomplete=off]") + end + end + + context "with input_html overriding the defaults" do + let(:body) { filter :created_at, input_html: { 'class': "custom_class" } } + + it "should override the default attribute values for both ends of date range" do + expect(body).to have_field("q[created_at_gteq]", class: "custom_class") + expect(body).to have_field("q[created_at_lteq]", class: "custom_class") + end + end + end + + describe "integer attribute" do + context "without options" do + let(:body) { filter :id } + + it "should generate a select option for equal to" do + expect(body).to have_css("option[value=id_eq]", text: "Equals") + end + + it "should generate a select option for greater than" do + expect(body).to have_css("option[value=id_gt]", text: "Greater than") + end + + it "should generate a select option for less than" do + expect(body).to have_css("option[value=id_lt]", text: "Less than") + end + + it "should generate a text field for input" do + expect(body).to have_field("q[id_eq]") + end + + it "should select the option which is currently being filtered" do + scope = Post.ransack id_gt: 1 + body = Capybara.string(render_filter scope, id: {}) + expect(body).to have_css("option[value=id_gt][selected=selected]", text: "Greater than") + end + end + + context "with filters options" do + let(:body) { filter :id, filters: [:eq, :gt] } + + it "should generate provided options for filter select" do + expect(body).to have_css("option[value=id_eq]", text: "Equals") + expect(body).to have_css("option[value=id_gt]", text: "Greater than") + end + + it "should not generate a select option for less than" do + expect(body).to have_no_css("option[value=id_lt]") + end + end + + context "with string_input_filters set" do + let(:body) do + admin = ActiveAdmin.application.namespace(:admin) + admin.string_input_filters = [:eq, :cont] + filter :id + ensure + admin.string_input_filters = nil + end + + it "keeps the numeric operators unaffected" do + expect(body).to have_select(options: ["Equals", "Greater than", "Less than"]) + end + end + end + + describe "boolean attribute" do + context "boolean datatypes" do + let(:body) { filter :starred } + + it "should generate a select" do + expect(body).to have_select("q[starred_eq]") + end + + it "should set the default text to 'Any'" do + expect(body).to have_css("option[value='']", text: "Any") + end + + it "should create an option for true and false" do + expect(body).to have_css("option[value=true]", text: "Yes") + expect(body).to have_css("option[value=false]", text: "No") + end + + it "should translate the label for boolean field" do + with_translation %i[activerecord attributes post starred], "Faved" do + expect(body).to have_css("label", text: "Faved") + end + end + end + + context "non-boolean data types" do + let(:body) { filter :title_present, as: :boolean } + + it "should generate a select" do + expect(body).to have_select("q[title_present]") + end + + it "should set the default text to 'Any'" do + expect(body).to have_css("option[value='']", text: "Any") + end + + it "should create an option for true and false" do + expect(body).to have_css("option[value=true]", text: "Yes") + expect(body).to have_css("option[value=false]", text: "No") + end + end + end + + describe "belongs_to" do + before do + @john = User.create first_name: "John", last_name: "Doe", username: "john_doe" + @jane = User.create first_name: "Jane", last_name: "Doe", username: "jane_doe" + end + + context "when given as the _id attribute name" do + let(:body) { filter :author_id } + + it "should generate a numeric filter" do + expect(body).to have_css("label", text: "Author") # really this should be Author ID :/) + expect(body).to have_css("option[value=author_id_lt]") + expect(body).to have_field("q[author_id_eq]", id: "q_author_id") + end + end + + context "when given as the name of the relationship" do + let(:body) { filter :author } + + it "should generate a select" do + expect(body).to have_select("q[author_id_eq]") + end + + it "should set the default text to 'Any'" do + expect(body).to have_css("option[value='']", text: "Any") + end + + it "should create an option for each related object" do + expect(body).to have_css("option[value='#{@john.id}']", text: "John Doe") + expect(body).to have_css("option[value='#{@jane.id}']", text: "Jane Doe") + end + + context "with a proc" do + let :body do + filter :title, as: :select, collection: proc { ["Title One", "Title Two"] } + end + + it "should use call the proc as the collection" do + expect(body).to have_css("option", text: "Title One") + expect(body).to have_css("option", text: "Title Two") + end + + it "should render the collection in the context of the view" do + body = filter :title, as: :select, collection: proc { [a_helper_method] } + expect(body).to have_css("option", text: "A Helper Method") + end + end + end + + context "when given the name of relationship with a primary key other than id" do + let(:resource_klass) do + Class.new(Post) do + belongs_to :kategory, class_name: "Category", primary_key: :name, foreign_key: :title + + def self.name + "SuperPost" + end + end + end + + let(:scope) do + resource_klass.ransack + end + + let(:body) { filter :kategory } + + it "should use the association primary key" do + expect(body).to have_select("q[kategory_name_eq]") + end + end + + context "as check boxes" do + let(:body) { filter :author, as: :check_boxes } + + it "should create a check box for each related object" do + expect(body).to have_field("q[author_id_in][]", type: :checkbox, with: @jane.id) + expect(body).to have_field("q[author_id_in][]", type: :checkbox, with: @jane.id) + end + + context "with a selected association" do + let(:scope) { Post.ransack(author_id_in: [@jane.id.to_s]) } + + it "preserves the selected checkbox" do + expect(body).to have_checked_field("q[author_id_in][]", with: @jane.id.to_s) + expect(body).to have_unchecked_field("q[author_id_in][]", with: @john.id.to_s) + end + end + end + + context "when polymorphic relationship" do + let(:scope) { ActiveAdmin::Comment.ransack } + it "should raise an error if a collection isn't provided" do + expect { filter :resource }.to raise_error \ + Formtastic::PolymorphicInputWithoutCollectionError + end + end + + context "when using a custom foreign key" do + let(:scope) { Post.ransack } + let(:body) { filter :category } + it "should ignore that foreign key and let Ransack handle it" do + expect(Post.reflect_on_association(:category).foreign_key.to_sym).to eq :custom_category_id + expect(body).to have_select("q[category_id_eq]") + end + end + end # belongs to + + describe "has_and_belongs_to_many" do + # skip "add HABTM models so this can be tested" + end + + describe "has_many :through" do + let(:scope) { Category.ransack } + + let!(:john) { User.create first_name: "John", last_name: "Doe", username: "john_doe" } + let!(:jane) { User.create first_name: "Jane", last_name: "Doe", username: "jane_doe" } + + context "when given as the name of the relationship" do + let(:body) { filter :authors } + + it "should generate a select" do + expect(body).to have_select("q[posts_author_id_eq]") + end + + it "should set the default text to 'Any'" do + expect(body).to have_css("option[value='']", text: "Any") + end + + it "should create an option for each related object" do + expect(body).to have_css("option[value='#{john.id}']", text: "John Doe") + expect(body).to have_css("option[value='#{jane.id}']", text: "Jane Doe") + end + end + + context "as check boxes" do + let(:body) { filter :authors, as: :check_boxes } + + it "should create a check box for each related object" do + expect(body).to have_field("q[posts_author_id_in][]", type: "checkbox", with: john.id) + expect(body).to have_field("q[posts_author_id_in][]", type: "checkbox", with: jane.id) + end + end + end + + describe "conditional display" do + [:if, :unless].each do |verb| + should = verb == :if ? "should" : "shouldn't" + if_true = verb == :if ? :to : :to_not + if_false = verb == :if ? :to_not : :to + context "with #{verb.inspect} proc" do + it "#{should} be displayed if true" do + body = filter :body, verb => proc { true } + expect(body).send if_true, have_field("q[body_cont]") + end + + it "#{should} be displayed if false" do + body = filter :body, verb => proc { false } + expect(body).send if_false, have_field("q[body_cont]") + end + + it "should still be hidden on the second render" do + filters = { body: { verb => proc { verb == :unless } } } + 2.times do + body = Capybara.string(render_filter scope, filters) + expect(body).to have_no_field("q[body_cont]") + end + end + + it "should successfully keep rendering other filters after one is hidden" do + filters = { body: { verb => proc { verb == :unless } }, author: {} } + body = Capybara.string(render_filter scope, filters) + expect(body).to have_no_field("q[body_cont]") + expect(body).to have_select("q[author_id_eq]") + end + end + end + end + + describe "custom search methods" do + it "should use the default type of the ransacker" do + body = filter :custom_searcher_numeric + expect(body).to have_css("option[value=custom_searcher_numeric_eq]") + expect(body).to have_css("option[value=custom_searcher_numeric_gt]") + expect(body).to have_css("option[value=custom_searcher_numeric_lt]") + end + + it "should work as select" do + body = filter :custom_title_searcher, as: :select, collection: ["foo"] + expect(body).to have_select("q[custom_title_searcher_eq]") + end + + it "should work as string" do + body = filter :custom_title_searcher, as: :string + expect(body).to have_css("option[value=custom_title_searcher_cont]") + expect(body).to have_css("option[value=custom_title_searcher_start]") + end + + describe "custom date range search" do + let(:gteq) { "2010-10-01" } + let(:lteq) { "2010-10-02" } + let(:scope) { Post.ransack custom_created_at_searcher_gteq: gteq, custom_created_at_searcher_lteq: lteq } + let(:body) { filter :custom_created_at_searcher, as: :date_range } + + it "should work as date_range" do + expect(body).to have_field("q[custom_created_at_searcher_gteq]", with: "2010-10-01") + expect(body).to have_field("q[custom_created_at_searcher_lteq]", with: "2010-10-02") + end + + context "filter value can't be casted to date" do + let(:gteq) { "Ooops" } + let(:lteq) { "Ooops" } + + it "should work display empty filter values" do + expect(body).to have_field("q[custom_created_at_searcher_gteq]", with: "") + expect(body).to have_field("q[custom_created_at_searcher_lteq]", with: "") + end + end + end + end + + describe "does not support some filter inputs" do + it "should fallback to use formtastic inputs" do + body = filter :custom_title_searcher, as: :text + expect(body).to have_css("textarea[name='q[custom_title_searcher]']") + end + end + + describe "blank option" do + context "for a select filter" do + it "should be there by default" do + body = filter :author + expect(body).to have_css("option", text: "Any") + end + + it "should be able to be disabled" do + body = filter :author, include_blank: false + expect(body).to have_no_css("option", text: "Any") + end + end + + context "for a multi-select filter" do + it "should not be there by default" do + body = filter :author, multiple: true + expect(body).to have_no_css("option", text: "Any") + end + + it "should be able to be enabled" do + body = filter :author, multiple: true, include_blank: true + expect(body).to have_css("option", text: "Any") + end + end + end + + describe "filters with a non-Ransack search object" do + let(:scope) do + search_class = stub_const( + "NonActiveRecordFilterSearch", + Data.define(:status_eq, :status_in) + ) + search_class.new(status_eq: "inactive", status_in: ["inactive"]) + end + + it "renders checkboxes and preserves the selected values" do + body = filter :status, as: :check_boxes, collection: %w[active inactive] + + expect(body).to have_checked_field("q[status_in][]", with: "inactive") + end + + it "renders a select without an explicit predicate" do + body = filter :status, as: :select, collection: %w[active inactive] + + expect(body).to have_select("q[status_eq]", selected: "inactive") + end + + it "renders a multi-select without an explicit predicate" do + body = filter :status, as: :select, multiple: true, collection: %w[active inactive] + + expect(body).to have_select("q[status_in][]", multiple: true, selected: ["inactive"]) + end + + it "renders a select with an explicit predicate" do + body = filter :status_eq, as: :select, collection: %w[active inactive] + + expect(body).to have_select("q[status_eq]", selected: "inactive") + end + end +end diff --git a/spec/helpers/form_helper_spec.rb b/spec/helpers/form_helper_spec.rb new file mode 100644 index 00000000000..cc888b91ed8 --- /dev/null +++ b/spec/helpers/form_helper_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe ActiveAdmin::FormHelper, type: :helper do + describe ".active_admin_form_for" do + let(:resource) { double "resource" } + + it "calls semantic_form_for with the ActiveAdmin form builder" do + expect(helper).to receive(:semantic_form_for).with(resource, { builder: ActiveAdmin::FormBuilder }) + helper.active_admin_form_for(resource) + end + + it "allows the form builder to be customized" do + # We can't use a stub here because options gets marshalled, and a new + # instance built. Any constant will work. + custom_builder = Object + expect(helper).to receive(:semantic_form_for).with(resource, { builder: custom_builder }) + helper.active_admin_form_for(resource, builder: custom_builder) + end + end + + describe ".hidden_field_tags_for" do + it "should render hidden field tags for params" do + params = ActionController::Parameters.new(scope: "All", filter: "None") + html = Capybara.string helper.hidden_field_tags_for(params) + expect(html).to have_field("scope", id: "hidden_active_admin_scope", type: :hidden, with: "All") + expect(html).to have_field("filter", id: "hidden_active_admin_filter", type: :hidden, with: "None") + end + + it "should generate not default id for hidden input" do + params = ActionController::Parameters.new(scope: "All") + expect(helper.hidden_field_tags_for(params)[/id="([^"]+)"/, 1]).to_not eq "scope" + end + + it "should filter out the field passed via the option :except" do + params = ActionController::Parameters.new(scope: "All", filter: "None") + html = Capybara.string helper.hidden_field_tags_for(params, except: :filter) + expect(html).to have_field("scope", id: "hidden_active_admin_scope", type: :hidden, with: "All") + end + end + + describe ".fields_for_params" do + it "should skip :action, :controller and :commit" do + expect(helper.fields_for_params(scope: "All", action: "index", controller: "PostController", commit: "Filter", utf8: "Yes!")).to eq [ { scope: "All" } ] + end + + it "should skip the except" do + expect(helper.fields_for_params({ scope: "All", name: "Greg" }, except: :name)).to eq [ { scope: "All" } ] + end + + it "should allow an array for the except" do + expect(helper.fields_for_params({ scope: "All", name: "Greg", age: "12" }, except: [:name, :age])).to eq [ { scope: "All" } ] + end + + it "should work with hashes" do + params = helper.fields_for_params(filters: { name: "John", age: "12" }) + + expect(params.size).to eq 2 + expect(params).to include({ "filters[name]" => "John" }) + expect(params).to include({ "filters[age]" => "12" }) + end + + it "should work with nested hashes" do + expect(helper.fields_for_params(filters: { user: { name: "John" } })).to eq [ { "filters[user][name]" => "John" } ] + end + + it "should work with arrays" do + expect(helper.fields_for_params(people: ["greg", "emily", "philippe"])). + to eq [ { "people[]" => "greg" }, + { "people[]" => "emily" }, + { "people[]" => "philippe" } ] + end + + it "should work with symbols" do + expect(helper.fields_for_params(filter: :id)).to eq [ { filter: "id" } ] + end + + it "should work with booleans" do + expect(helper.fields_for_params(booleantest: false)).to eq [ { booleantest: false } ] + end + + it "should work with nil" do + expect(helper.fields_for_params(a: nil)).to eq [ { a: "" } ] + end + + it "should raise an error with an unsupported type" do + expect { helper.fields_for_params(a: 1) }.to raise_error(TypeError, "Cannot convert Integer value: 1") + end + end +end diff --git a/spec/helpers/index_helper_spec.rb b/spec/helpers/index_helper_spec.rb new file mode 100644 index 00000000000..aa882a67abc --- /dev/null +++ b/spec/helpers/index_helper_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe ActiveAdmin::IndexHelper, type: :helper do + describe "#collection_size" do + before do + Post.create!(title: "A post") + Post.create!(title: "A post") + Post.create!(title: "Another post") + end + + it "should take the defined collection by default" do + expect(helper).to receive(:collection).and_return(Post.where(nil)) + expect(helper.collection_size).to eq 3 + + expect(helper).to receive(:collection).and_return(Post.where(title: "Another post")) + expect(helper.collection_size).to eq 1 + + expect(helper).to receive(:collection).and_return(Post.where(title: "A post").to_a) + expect(helper.collection_size).to eq 2 + end + + context "with argument" do + it "should return the collection size for an ActiveRecord class" do + expect(helper.collection_size(Post.where(nil))).to eq 3 + end + + it "should return the collection size for an ActiveRecord::Relation" do + expect(helper.collection_size(Post.where(title: "A post"))).to eq 2 + end + + it "should return the collection size for a collection with group by" do + expect(helper.collection_size(Post.group(:title))).to eq 2 + end + + it "should return the collection size for a collection with group by, select and custom order" do + expect(helper.collection_size(Post.select("title, count(*) as nb_posts").group(:title).order("nb_posts"))).to eq 2 + end + + it "should return the collection size for an Array" do + expect(helper.collection_size(Post.where(title: "A post").to_a)).to eq 2 + end + end + end + + describe "#collection_empty?" do + it "should take the defined collection by default" do + expect(helper).to receive(:collection).twice.and_return(Post.all) + + expect(helper.collection_empty?).to eq true + + Post.create!(title: "Title") + expect(helper.collection_empty?).to eq false + end + + context "with argument" do + before do + Post.create!(title: "A post") + Post.create!(title: "Another post") + end + + it "should return true when the collection is empty" do + expect(helper.collection_empty?(Post.where(title: "Non existing post"))).to eq true + end + + it "should return false when the collection is not empty" do + expect(helper.collection_empty?(Post.where(title: "A post"))).to eq false + end + end + end +end diff --git a/spec/helpers/layout_helper_spec.rb b/spec/helpers/layout_helper_spec.rb new file mode 100644 index 00000000000..0edbe8507a6 --- /dev/null +++ b/spec/helpers/layout_helper_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe ActiveAdmin::LayoutHelper, type: :helper do + describe "active_admin_application" do + it "returns the application instance" do + expect(helper.active_admin_application).to eq ActiveAdmin.application + end + end + + describe "set_page_title" do + it "sets the @page_title variable" do + helper.set_page_title("Sample Page") + expect(helper.instance_variable_get(:@page_title)).to eq "Sample Page" + end + end + + describe "html_head_site_title" do + before do + expect(helper).to receive(:site_title).and_return("MyAdmin") + allow(helper).to receive(:page_title).and_return("Users") + end + + it "returns title in default format" do + expect(helper.html_head_site_title).to eq "Users - MyAdmin" + end + + it "returns title with custom separator" do + expect(helper.html_head_site_title(separator: "|")).to eq "Users | MyAdmin" + end + + it "returns title with @page_title override" do + helper.set_page_title("Posts") + expect(helper.html_head_site_title).to eq "Posts - MyAdmin" + end + end + + describe "html_head_site_title escaping" do + it "does not double-escape an already html_safe page title" do + expect(helper).to receive(:site_title).and_return("MyAdmin") + expect(helper).to receive(:page_title).and_return(ERB::Util.html_escape("Smith & Sons")) + + result = helper.html_head_site_title + + expect(result).to be_html_safe + expect(result).to eq "Smith & Sons - MyAdmin" + expect(result).to_not include("&amp;") + end + end + + describe "skip_sidebar?" do + it "should return true if skipped" do + helper.skip_sidebar! + expect(helper.skip_sidebar?).to eq true + end + + it "should return false if not skipped" do + expect(helper.skip_sidebar?).to eq false + end + end + + describe ".flash_messages" do + it "should not include 'timedout' flash messages by default" do + expect(helper).to receive(:active_admin_application).and_return(ActiveAdmin::Application.new) + + flash[:alert] = "Alert" + flash[:timedout] = true + expect(helper.flash_messages).to include "alert" + expect(helper.flash_messages).to_not include "timedout" + end + + it "should not return flash messages included in flash_keys_to_except config" do + config = double(flash_keys_to_except: ["hideme"]) + expect(helper).to receive(:active_admin_application).and_return(config) + + flash[:alert] = "Alert" + flash[:hideme] = "Do not show" + expect(helper.flash_messages).to include "alert" + expect(helper.flash_messages).to_not include "hideme" + end + end +end diff --git a/spec/integration/default_namespace_spec.rb b/spec/integration/default_namespace_spec.rb deleted file mode 100644 index d0a06830fbd..00000000000 --- a/spec/integration/default_namespace_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -require 'spec_helper' - -describe ActiveAdmin::Application do - - include Rails.application.routes.url_helpers - - [false, nil].each do |value| - - describe "with a #{value} default namespace" do - - before(:all) do - @__original_application = ActiveAdmin.application - application = ActiveAdmin::Application.new - application.default_namespace = value - ActiveAdmin.application = application - load_defaults! - reload_routes! - end - - after(:all) do - ActiveAdmin.application = @__original_application - end - - it "should generate a log out path" do - destroy_admin_user_session_path.should == "/admin_users/logout" - end - - it "should generate a log in path" do - new_admin_user_session_path.should == "/admin_users/login" - end - - end - - end - - describe "with a test default namespace" do - - before(:all) do - @__original_application = ActiveAdmin.application - application = ActiveAdmin::Application.new - application.default_namespace = :test - ActiveAdmin.application = application - load_defaults! - reload_routes! - end - - after(:all) do - ActiveAdmin.application = @__original_application - end - - it "should generate a log out path" do - destroy_admin_user_session_path.should == "/test/logout" - end - - it "should generate a log in path" do - new_admin_user_session_path.should == "/test/login" - end - - end - -end diff --git a/spec/integration/javascript_spec.rb b/spec/integration/javascript_spec.rb deleted file mode 100644 index 92f66fe47ad..00000000000 --- a/spec/integration/javascript_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'spec_helper' -require 'jslint' - -%x[which java] -if $? == 0 # Only run the JS Lint test if Java is installed - describe "Javascript" do - before do - @lint = JSLint::Lint.new( - :paths => ['public/javascripts/**/*.js'], - :exclude_paths => ['public/javascripts/vendor/**/*.js'], - :config_path => 'spec/support/jslint.yml' - ) - end - - it "should not have any syntax errors" do - @lint.run - end - end -end - diff --git a/spec/integration/memory_spec.rb b/spec/integration/memory_spec.rb deleted file mode 100644 index 9f5359a8450..00000000000 --- a/spec/integration/memory_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -require 'spec_helper' - -# These tests show the memory leak that is currently in Active Admin. They -# are all marked as pending until they pass. To work on them, comment out the -# pending method call in #it_should_not_leak -describe "Memory Leak" do - - def count_instances_of(klass) - ObjectSpace.each_object(klass) { } - end - - def self.it_should_not_leak(klass) - it "should not leak #{klass}" do - previously_disabled = GC.enable # returns true if the garbage collector was disabled - GC.start - count = count_instances_of(klass) - - load_defaults! - - GC.start - GC.disable if previously_disabled - count_instances_of(klass).should <= count - end - end - - it_should_not_leak ActiveAdmin::Namespace - it_should_not_leak ActiveAdmin::Resource - -end diff --git a/spec/integration/stylesheets_spec.rb b/spec/integration/stylesheets_spec.rb deleted file mode 100644 index 760a5cb3fae..00000000000 --- a/spec/integration/stylesheets_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -require 'spec_helper' - -describe "Stylesheets" do - if Rails.version[0..2] == '3.1' - require "sprockets" - context "when Rails 3.1.x" do - let(:css) do - assets = Rails.application.assets - assets.find_asset("active_admin.css") - end - it "should successfully render the scss stylesheets using sprockets" do - css.should_not be_nil - end - it "should not have any syntax errors" do - css.to_s.should_not include("Syntax error:") - end - end - end - - if Rails.version[0..2] == '3.0' - context "when Rails 3.0.x" do - let(:stylesheet_path) do - Rails.root + 'public/stylesheets/active_admin.css' - end - - before do - "rm #{stylesheet_path}" if File.exists?(stylesheet_path) - Sass::Plugin.force_update_stylesheets - end - - it "should render the scss stylesheets using SASS" do - File.exists?(stylesheet_path).should be_true - end - - it "should not have any syntax errors" do - css = File.read(stylesheet_path) - css.should_not include("Syntax error:") - end - end - end -end diff --git a/spec/javascripts/coffeescripts/jquery.aa.checkbox-toggler-spec.js.coffee b/spec/javascripts/coffeescripts/jquery.aa.checkbox-toggler-spec.js.coffee deleted file mode 100644 index 755761caba1..00000000000 --- a/spec/javascripts/coffeescripts/jquery.aa.checkbox-toggler-spec.js.coffee +++ /dev/null @@ -1,50 +0,0 @@ -describe "AA.CheckboxToggler", -> - - beforeEach -> - loadFixtures('checkboxes.html') - - @collection = $("#collection") - @toggle_all = @collection.find(".toggle_all") - - @checkboxes = @collection.find(":checkbox").not(@toggle_all) - - - new AA.CheckboxToggler({}, @collection) - - describe "on init", -> - it "should raise an error if container not found", -> - expect( => new AA.CheckboxToggler({}) ).toThrow("Container element not found") - - it "should raise an error if 'toggle all' checkbox not found", -> - @toggle_all.remove() - expect( => new AA.CheckboxToggler({}, @collection); ).toThrow("'toggle all' checkbox not found") - - describe "'toggle all' checkbox", -> - it "should check all checkboxes when checked", -> - @toggle_all.trigger("click") - expect(@checkboxes).toHaveAttr("checked") - - it "should uncheck all checkboxes when unchecked", -> - @toggle_all.trigger("click") - @toggle_all.trigger("click") - expect(@checkboxes).not.toHaveAttr("checked") - - describe "individual checkboxes", -> - - describe "when all checkboxes are selected and one is unchecked", -> - beforeEach -> - @collection.find(":checkbox").attr("checked", "checked") - @collection.find("#item_1").trigger("click") - - it "should uncheck the 'toggle all' checkbox", -> - expect(@toggle_all).not.toHaveAttr("checked") - - describe "when the last checkbox is checked", -> - beforeEach -> - @checkboxes.attr("checked", "checked") - @collection.find("#item_1").removeAttr("checked") - @collection.find("#item_1").trigger("click") - - it "should check the 'toggle all' checkbox", -> - expect(@toggle_all).toHaveAttr("checked") - diff --git a/spec/javascripts/coffeescripts/jquery.aa.popover-spec.js.coffee b/spec/javascripts/coffeescripts/jquery.aa.popover-spec.js.coffee deleted file mode 100644 index 8e672f4c67d..00000000000 --- a/spec/javascripts/coffeescripts/jquery.aa.popover-spec.js.coffee +++ /dev/null @@ -1,82 +0,0 @@ -describe "AA.Popover", -> - $wrapper = undefined - $button = undefined - $popover = undefined - popover = undefined - beforeEach -> - $wrapper = $(inject(id: "wrapper")) - $button = $(inject( - el: "a" - id: "my_popover_button" - attrs: - href: "#my_popover" - )) - $popover = $(inject(id: "my_popover")) - $button.popover - fadeInDuration: 0 - fadeOutDuration: 0 - - popover = $button.data("popover") - - describe "opening button / link is pressed", -> - it "should open the popover", -> - $button.trigger "click" - expect($("#my_popover")).toBeVisible() - - describe "when initiated", -> - it "should be hidden", -> - expect($popover).toBeHidden() - - it "should be have class popover", -> - expect($popover).toHaveClass "popover" - - describe "nipple", -> - it "should exist", -> - expect($popover).toContain ".popover_nipple" - - describe "when open is called", -> - beforeEach -> - expect($popover).toBeHidden() - $button.popover "open" - - it "should be open", -> - expect($popover).toBeVisible() - - describe "when destroy is called", -> - beforeEach -> - $button.popover "destroy" - - it "should not have popover stored as a data attr", -> - expect($button.data("popover")).toEqual `undefined` - - it "should not be bound to any event listeners", -> - expect($button.data("events")).toEqual `undefined` - - describe "when it's already open", -> - beforeEach -> - $button.popover "open" - - describe "when close is called", -> - beforeEach -> - $button.popover "close" - - it "should close", -> - expect($popover).toBeHidden() - - describe "when user clicks outside", -> - beforeEach -> - $button.popover "open" - $("#wrapper").trigger "click" - - it "should close", -> - expect($popover).toBeHidden() - - describe "options", -> - describe "autoOpen set to false", -> - beforeEach -> - $button.popover "destroy" - $button.popover autoOpen: false - - it "should not open when the link is clicked", -> - $button.trigger "click" - expect($("#my_popover")).toBeHidden() diff --git a/spec/javascripts/coffeescripts/jquery.aa.table-checkbox-toggler-spec.js.coffee b/spec/javascripts/coffeescripts/jquery.aa.table-checkbox-toggler-spec.js.coffee deleted file mode 100644 index 9ba8d1d87c8..00000000000 --- a/spec/javascripts/coffeescripts/jquery.aa.table-checkbox-toggler-spec.js.coffee +++ /dev/null @@ -1,34 +0,0 @@ -describe "AA.TableCheckboxToggler", -> - - beforeEach -> - loadFixtures('table_checkboxes.html'); - - @collection = $("#collection") - @toggle_all = @collection.find(".toggle_all") - - @checkboxes = @collection.find(":checkbox").not(@toggle_all) - - new AA.TableCheckboxToggler({}, @collection) - - describe "'selected' class for table row", -> - it "should add the class 'selected' to rows when their checkbox is checked ", -> - checkbox = $("#item_1") - checkbox.attr("checked", "checked") - checkbox.trigger("change") - - expect(checkbox.parents("tr")).toHaveAttr("class", "selected") - - it "should remove the class 'selected' from rows when their checkbox is unchecked ", -> - checkbox = $("#item_1") - checkbox.trigger("change") - - expect(checkbox.parents("tr")).not.toHaveAttr("class", "selected") - - describe "clicking a cell", -> - it "should toggle the checkbox when a cell is clicked", -> - checkbox = $("#item_1") - row = checkbox.parents("td") - $(row).trigger("click") - - expect(checkbox).toHaveAttr("checked", "checked") - diff --git a/spec/javascripts/compiled/jquery.aa.checkbox-toggler-spec.js b/spec/javascripts/compiled/jquery.aa.checkbox-toggler-spec.js deleted file mode 100644 index ee2dcb72853..00000000000 --- a/spec/javascripts/compiled/jquery.aa.checkbox-toggler-spec.js +++ /dev/null @@ -1,60 +0,0 @@ -(function() { - - describe("AA.CheckboxToggler", function() { - beforeEach(function() { - loadFixtures('checkboxes.html'); - this.collection = $("#collection"); - this.toggle_all = this.collection.find(".toggle_all"); - this.checkboxes = this.collection.find(":checkbox").not(this.toggle_all); - return new AA.CheckboxToggler({}, this.collection); - }); - describe("on init", function() { - it("should raise an error if container not found", function() { - var _this = this; - return expect(function() { - return new AA.CheckboxToggler({}); - }).toThrow("Container element not found"); - }); - return it("should raise an error if 'toggle all' checkbox not found", function() { - var _this = this; - this.toggle_all.remove(); - return expect(function() { - return new AA.CheckboxToggler({}, _this.collection); - }).toThrow("'toggle all' checkbox not found"); - }); - }); - describe("'toggle all' checkbox", function() { - it("should check all checkboxes when checked", function() { - this.toggle_all.trigger("click"); - return expect(this.checkboxes).toHaveAttr("checked"); - }); - return it("should uncheck all checkboxes when unchecked", function() { - this.toggle_all.trigger("click"); - this.toggle_all.trigger("click"); - return expect(this.checkboxes).not.toHaveAttr("checked"); - }); - }); - return describe("individual checkboxes", function() { - describe("when all checkboxes are selected and one is unchecked", function() { - beforeEach(function() { - this.collection.find(":checkbox").attr("checked", "checked"); - return this.collection.find("#item_1").trigger("click"); - }); - return it("should uncheck the 'toggle all' checkbox", function() { - return expect(this.toggle_all).not.toHaveAttr("checked"); - }); - }); - return describe("when the last checkbox is checked", function() { - beforeEach(function() { - this.checkboxes.attr("checked", "checked"); - this.collection.find("#item_1").removeAttr("checked"); - return this.collection.find("#item_1").trigger("click"); - }); - return it("should check the 'toggle all' checkbox", function() { - return expect(this.toggle_all).toHaveAttr("checked"); - }); - }); - }); - }); - -}).call(this); diff --git a/spec/javascripts/compiled/jquery.aa.popover-spec.js b/spec/javascripts/compiled/jquery.aa.popover-spec.js deleted file mode 100644 index d4b82b02c0b..00000000000 --- a/spec/javascripts/compiled/jquery.aa.popover-spec.js +++ /dev/null @@ -1,106 +0,0 @@ -(function() { - - describe("AA.Popover", function() { - var $button, $popover, $wrapper, popover; - $wrapper = void 0; - $button = void 0; - $popover = void 0; - popover = void 0; - beforeEach(function() { - $wrapper = $(inject({ - id: "wrapper" - })); - $button = $(inject({ - el: "a", - id: "my_popover_button", - attrs: { - href: "#my_popover" - } - })); - $popover = $(inject({ - id: "my_popover" - })); - $button.popover({ - fadeInDuration: 0, - fadeOutDuration: 0 - }); - return popover = $button.data("popover"); - }); - describe("opening button / link is pressed", function() { - return it("should open the popover", function() { - $button.trigger("click"); - return expect($("#my_popover")).toBeVisible(); - }); - }); - describe("when initiated", function() { - it("should be hidden", function() { - return expect($popover).toBeHidden(); - }); - it("should be have class popover", function() { - return expect($popover).toHaveClass("popover"); - }); - return describe("nipple", function() { - return it("should exist", function() { - return expect($popover).toContain(".popover_nipple"); - }); - }); - }); - describe("when open is called", function() { - beforeEach(function() { - expect($popover).toBeHidden(); - return $button.popover("open"); - }); - return it("should be open", function() { - return expect($popover).toBeVisible(); - }); - }); - describe("when destroy is called", function() { - beforeEach(function() { - return $button.popover("destroy"); - }); - it("should not have popover stored as a data attr", function() { - return expect($button.data("popover")).toEqual(undefined); - }); - return it("should not be bound to any event listeners", function() { - return expect($button.data("events")).toEqual(undefined); - }); - }); - describe("when it's already open", function() { - beforeEach(function() { - return $button.popover("open"); - }); - describe("when close is called", function() { - beforeEach(function() { - return $button.popover("close"); - }); - return it("should close", function() { - return expect($popover).toBeHidden(); - }); - }); - return describe("when user clicks outside", function() { - beforeEach(function() { - $button.popover("open"); - return $("#wrapper").trigger("click"); - }); - return it("should close", function() { - return expect($popover).toBeHidden(); - }); - }); - }); - return describe("options", function() { - return describe("autoOpen set to false", function() { - beforeEach(function() { - $button.popover("destroy"); - return $button.popover({ - autoOpen: false - }); - }); - return it("should not open when the link is clicked", function() { - $button.trigger("click"); - return expect($("#my_popover")).toBeHidden(); - }); - }); - }); - }); - -}).call(this); diff --git a/spec/javascripts/compiled/jquery.aa.table-checkbox-toggler-spec.js b/spec/javascripts/compiled/jquery.aa.table-checkbox-toggler-spec.js deleted file mode 100644 index f412a5ae1e3..00000000000 --- a/spec/javascripts/compiled/jquery.aa.table-checkbox-toggler-spec.js +++ /dev/null @@ -1,37 +0,0 @@ -(function() { - - describe("AA.TableCheckboxToggler", function() { - beforeEach(function() { - loadFixtures('table_checkboxes.html'); - this.collection = $("#collection"); - this.toggle_all = this.collection.find(".toggle_all"); - this.checkboxes = this.collection.find(":checkbox").not(this.toggle_all); - return new AA.TableCheckboxToggler({}, this.collection); - }); - describe("'selected' class for table row", function() { - it("should add the class 'selected' to rows when their checkbox is checked ", function() { - var checkbox; - checkbox = $("#item_1"); - checkbox.attr("checked", "checked"); - checkbox.trigger("change"); - return expect(checkbox.parents("tr")).toHaveAttr("class", "selected"); - }); - return it("should remove the class 'selected' from rows when their checkbox is unchecked ", function() { - var checkbox; - checkbox = $("#item_1"); - checkbox.trigger("change"); - return expect(checkbox.parents("tr")).not.toHaveAttr("class", "selected"); - }); - }); - return describe("clicking a cell", function() { - return it("should toggle the checkbox when a cell is clicked", function() { - var checkbox, row; - checkbox = $("#item_1"); - row = checkbox.parents("td"); - $(row).trigger("click"); - return expect(checkbox).toHaveAttr("checked", "checked"); - }); - }); - }); - -}).call(this); diff --git a/spec/javascripts/fixtures/checkboxes.html b/spec/javascripts/fixtures/checkboxes.html deleted file mode 100644 index 245b3d0d600..00000000000 --- a/spec/javascripts/fixtures/checkboxes.html +++ /dev/null @@ -1,9 +0,0 @@ -
- - - - - - -
- diff --git a/spec/javascripts/fixtures/table_checkboxes.html b/spec/javascripts/fixtures/table_checkboxes.html deleted file mode 100644 index 1c438e53dba..00000000000 --- a/spec/javascripts/fixtures/table_checkboxes.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - -
- -
- - diff --git a/spec/javascripts/helpers/SpecHelper.js b/spec/javascripts/helpers/SpecHelper.js deleted file mode 100644 index e759ff01d02..00000000000 --- a/spec/javascripts/helpers/SpecHelper.js +++ /dev/null @@ -1,3 +0,0 @@ -beforeEach(function() { - window.inject = $.jasmine.inject; -}); diff --git a/spec/javascripts/helpers/vendor/jasmine-fixture-0.0.5.js b/spec/javascripts/helpers/vendor/jasmine-fixture-0.0.5.js deleted file mode 100644 index c5d9a62e169..00000000000 --- a/spec/javascripts/helpers/vendor/jasmine-fixture-0.0.5.js +++ /dev/null @@ -1,108 +0,0 @@ -/* jasmine-fixture Makes injecting HTML snippets into the DOM easy & clean! - * site: https://github.com/searls/jasmine-fixture */ -(function($){ - var originalJasmineFixture = window.jasmineFixture, - defaultConfiguration = { - el:'div', - cssClass:'', - id:'', - text: '', - html: '', - defaultAttribute: 'class', - attrs: {} - }; - - window.jasmineFixture = function($) { - var isReady = false, - rootId = 'specContainer', - defaults = $.extend({},defaultConfiguration); - - $.jasmine = { - inject: function(arg,context) { - if(isReady !== true) init(); - var parent = context ? context : $('#'+rootId), - $toInject; - if(itLooksLikeHtml(arg)) { - $toInject = $(arg); - } else { - var config = $.extend({},defaults,arg,{ userString: arg }); - $toInject = $('<'+config.el+'>'); - applyAttributes($toInject,config); - injectContents($toInject,config); - } - return $toInject.appendTo(parent); - }, - configure: function(config) { - $.extend(defaults,config); - }, - restoreDefaults: function(){ - defaults = $.extend({},defaultConfiguration); - }, - noConflict: function() { - window.jasmineFixture = originalJasmineFixture; - return this; - } - }; - - $.fn.inject = function(html){ - return $.jasmine.inject(html,$(this)); - }; - - var applyAttributes = function($html,config) { - var attrs = $.extend({},{ - id: config.id, - 'class': config['class'] || config.cssClass - }, config.attrs); - if(isString(config.userString)) { - attrs[config.defaultAttribute] = config.userString; - } - for(var key in attrs) { - if(attrs[key]) { - $html.attr(key,attrs[key]); - } - } - }; - - var injectContents = function($el,config){ - if(config.text && config.html) { - throw "Error: because they conflict, you may only configure inject() to set `html` or `text`, not both! \n\nHTML was: "+config.html+" \n\n Text was: "+config.text - } else if(config.text) { - $el.text(config.text); - } else if(config.html) { - $el.html(config.html); - } - } - - var itLooksLikeHtml = function(arg) { - return isString(arg) && arg.indexOf('<') !== -1 - }; - - var isString = function(arg) { - return arg && arg.constructor === String; - }; - - var init = function() { - $('body').append('
'); - isReady = true; - }; - - var tidyUp = function() { - $('#'+rootId).remove(); - isReady = false; - }; - - $(function(jQuery){ - init(); - }); - afterEach(function(){ - tidyUp(); - }); - - return $.jasmine; - }; - - if(jQuery) { - var jasmineFixture = window.jasmineFixture(jQuery); - window.inject = window.inject || jasmineFixture.inject; - } -})(jQuery); \ No newline at end of file diff --git a/spec/javascripts/helpers/vendor/jasmine-jquery.js b/spec/javascripts/helpers/vendor/jasmine-jquery.js deleted file mode 100644 index e41001b64e1..00000000000 --- a/spec/javascripts/helpers/vendor/jasmine-jquery.js +++ /dev/null @@ -1,288 +0,0 @@ -var readFixtures = function() { - return jasmine.getFixtures().proxyCallTo_('read', arguments); -}; - -var preloadFixtures = function() { - jasmine.getFixtures().proxyCallTo_('preload', arguments); -}; - -var loadFixtures = function() { - jasmine.getFixtures().proxyCallTo_('load', arguments); -}; - -var setFixtures = function(html) { - jasmine.getFixtures().set(html); -}; - -var sandbox = function(attributes) { - return jasmine.getFixtures().sandbox(attributes); -}; - -var spyOnEvent = function(selector, eventName) { - jasmine.JQuery.events.spyOn(selector, eventName); -} - -jasmine.getFixtures = function() { - return jasmine.currentFixtures_ = jasmine.currentFixtures_ || new jasmine.Fixtures(); -}; - -jasmine.Fixtures = function() { - this.containerId = 'jasmine-fixtures'; - this.fixturesCache_ = {}; - this.fixturesPath = 'spec/javascripts/fixtures'; -}; - -jasmine.Fixtures.prototype.set = function(html) { - this.cleanUp(); - this.createContainer_(html); -}; - -jasmine.Fixtures.prototype.preload = function() { - this.read.apply(this, arguments); -}; - -jasmine.Fixtures.prototype.load = function() { - this.cleanUp(); - this.createContainer_(this.read.apply(this, arguments)); -}; - -jasmine.Fixtures.prototype.read = function() { - var htmlChunks = []; - - var fixtureUrls = arguments; - for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) { - htmlChunks.push(this.getFixtureHtml_(fixtureUrls[urlIndex])); - } - - return htmlChunks.join(''); -}; - -jasmine.Fixtures.prototype.clearCache = function() { - this.fixturesCache_ = {}; -}; - -jasmine.Fixtures.prototype.cleanUp = function() { - jQuery('#' + this.containerId).remove(); -}; - -jasmine.Fixtures.prototype.sandbox = function(attributes) { - var attributesToSet = attributes || {}; - return jQuery('
').attr(attributesToSet); -}; - -jasmine.Fixtures.prototype.createContainer_ = function(html) { - var container; - if(html instanceof jQuery) { - container = jQuery('
'); - container.html(html); - } else { - container = '
' + html + '
' - } - jQuery('body').append(container); -}; - -jasmine.Fixtures.prototype.getFixtureHtml_ = function(url) { - if (typeof this.fixturesCache_[url] == 'undefined') { - this.loadFixtureIntoCache_(url); - } - return this.fixturesCache_[url]; -}; - -jasmine.Fixtures.prototype.loadFixtureIntoCache_ = function(relativeUrl) { - var self = this; - var url = this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl; - jQuery.ajax({ - async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded - cache: false, - dataType: 'html', - url: url, - success: function(data) { - self.fixturesCache_[relativeUrl] = data; - }, - error: function(jqXHR, status, errorThrown) { - throw Error('Fixture could not be loaded: ' + url + ' (status: ' + status + ', message: ' + errorThrown.message + ')'); - } - }); -}; - -jasmine.Fixtures.prototype.proxyCallTo_ = function(methodName, passedArguments) { - return this[methodName].apply(this, passedArguments); -}; - - -jasmine.JQuery = function() {}; - -jasmine.JQuery.browserTagCaseIndependentHtml = function(html) { - return jQuery('
').append(html).html(); -}; - -jasmine.JQuery.elementToString = function(element) { - return jQuery('
').append(element.clone()).html(); -}; - -jasmine.JQuery.matchersClass = {}; - -(function(namespace) { - var data = { - spiedEvents: {}, - handlers: [] - }; - - namespace.events = { - spyOn: function(selector, eventName) { - var handler = function(e) { - data.spiedEvents[[selector, eventName]] = e; - }; - jQuery(selector).bind(eventName, handler); - data.handlers.push(handler); - }, - - wasTriggered: function(selector, eventName) { - return !!(data.spiedEvents[[selector, eventName]]); - }, - - cleanUp: function() { - data.spiedEvents = {}; - data.handlers = []; - } - } -})(jasmine.JQuery); - -(function(){ - var jQueryMatchers = { - toHaveClass: function(className) { - return this.actual.hasClass(className); - }, - - toBeVisible: function() { - return this.actual.is(':visible'); - }, - - toBeHidden: function() { - return this.actual.is(':hidden'); - }, - - toBeSelected: function() { - return this.actual.is(':selected'); - }, - - toBeChecked: function() { - return this.actual.is(':checked'); - }, - - toBeEmpty: function() { - return this.actual.is(':empty'); - }, - - toExist: function() { - return this.actual.size() > 0; - }, - - toHaveAttr: function(attributeName, expectedAttributeValue) { - return hasProperty(this.actual.attr(attributeName), expectedAttributeValue); - }, - - toHaveId: function(id) { - return this.actual.attr('id') == id; - }, - - toHaveHtml: function(html) { - return this.actual.html() == jasmine.JQuery.browserTagCaseIndependentHtml(html); - }, - - toHaveText: function(text) { - if (text && jQuery.isFunction(text.test)) { - return text.test(this.actual.text()); - } else { - return this.actual.text() == text; - } - }, - - toHaveValue: function(value) { - return this.actual.val() == value; - }, - - toHaveData: function(key, expectedValue) { - return hasProperty(this.actual.data(key), expectedValue); - }, - - toBe: function(selector) { - return this.actual.is(selector); - }, - - toContain: function(selector) { - return this.actual.find(selector).size() > 0; - }, - - toBeDisabled: function(selector){ - return this.actual.is(':disabled'); - }, - - // tests the existence of a specific event binding - toHandle: function(eventName) { - var events = this.actual.data("events"); - return events && events[eventName].length > 0; - }, - - // tests the existence of a specific event binding + handler - toHandleWith: function(eventName, eventHandler) { - var stack = this.actual.data("events")[eventName]; - var i; - for (i = 0; i < stack.length; i++) { - if (stack[i].handler == eventHandler) { - return true; - } - } - return false; - } - }; - - var hasProperty = function(actualValue, expectedValue) { - if (expectedValue === undefined) { - return actualValue !== undefined; - } - return actualValue == expectedValue; - }; - - var bindMatcher = function(methodName) { - var builtInMatcher = jasmine.Matchers.prototype[methodName]; - - jasmine.JQuery.matchersClass[methodName] = function() { - if (this.actual instanceof jQuery) { - var result = jQueryMatchers[methodName].apply(this, arguments); - this.actual = jasmine.JQuery.elementToString(this.actual); - return result; - } - - if (builtInMatcher) { - return builtInMatcher.apply(this, arguments); - } - - return false; - }; - }; - - for(var methodName in jQueryMatchers) { - bindMatcher(methodName); - } -})(); - -beforeEach(function() { - this.addMatchers(jasmine.JQuery.matchersClass); - this.addMatchers({ - toHaveBeenTriggeredOn: function(selector) { - this.message = function() { - return [ - "Expected event " + this.actual + " to have been triggered on" + selector, - "Expected event " + this.actual + " not to have been triggered on" + selector - ]; - }; - return jasmine.JQuery.events.wasTriggered(selector, this.actual); - } - }) -}); - -afterEach(function() { - jasmine.getFixtures().cleanUp(); - jasmine.JQuery.events.cleanUp(); -}); \ No newline at end of file diff --git a/spec/javascripts/helpers/vendor/sinon-1.2.0.js b/spec/javascripts/helpers/vendor/sinon-1.2.0.js deleted file mode 100644 index 3f6188070ad..00000000000 --- a/spec/javascripts/helpers/vendor/sinon-1.2.0.js +++ /dev/null @@ -1,2915 +0,0 @@ -/** - * Sinon.JS 1.2.0, 2011/09/27 - * - * @author Christian Johansen (christian@cjohansen.no) - * - * (The BSD License) - * - * Copyright (c) 2010-2011, Christian Johansen, christian@cjohansen.no - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * - * * Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * * Neither the name of Christian Johansen nor the names of his contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF - * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -"use strict"; -/*jslint eqeqeq: false, onevar: false, forin: true, nomen: false, regexp: false, plusplus: false*/ -/*global module, require, __dirname, document*/ -/** - * Sinon core utilities. For internal use only. - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -var sinon = (function () { - var div = typeof document != "undefined" && document.createElement("div"); - - function isNode(obj) { - var success = false; - - try { - obj.appendChild(div); - success = div.parentNode == obj; - } catch (e) { - return false; - } finally { - try { - obj.removeChild(div); - } catch (e) {} - } - - return success; - } - - function isElement(obj) { - return div && obj && obj.nodeType === 1 && isNode(obj); - } - - return { - wrapMethod: function wrapMethod(object, property, method) { - if (!object) { - throw new TypeError("Should wrap property of object"); - } - - if (typeof method != "function") { - throw new TypeError("Method wrapper should be function"); - } - - var wrappedMethod = object[property]; - var type = typeof wrappedMethod; - - if (type != "function") { - throw new TypeError("Attempted to wrap " + type + " property " + property + - " as function"); - } - - if (wrappedMethod.restore && wrappedMethod.restore.sinon) { - throw new TypeError("Attempted to wrap " + property + " which is already wrapped"); - } - - if (wrappedMethod.calledBefore) { - var verb = !!wrappedMethod.returns ? "stubbed" : "spied on"; - throw new TypeError("Attempted to wrap " + property + " which is already " + verb); - } - - var owned = object.hasOwnProperty(property); - object[property] = method; - method.displayName = property; - - method.restore = function () { - if(owned) { - object[property] = wrappedMethod; - } else { - delete object[property]; - } - }; - - method.restore.sinon = true; - - return method; - }, - - extend: function extend(target) { - for (var i = 1, l = arguments.length; i < l; i += 1) { - for (var prop in arguments[i]) { - if (arguments[i].hasOwnProperty(prop)) { - target[prop] = arguments[i][prop]; - } - - // DONT ENUM bug, only care about toString - if (arguments[i].hasOwnProperty("toString") && - arguments[i].toString != target.toString) { - target.toString = arguments[i].toString; - } - } - } - - return target; - }, - - create: function create(proto) { - var F = function () {}; - F.prototype = proto; - return new F(); - }, - - deepEqual: function deepEqual(a, b) { - if (typeof a != "object" || typeof b != "object") { - return a === b; - } - - if (isElement(a) || isElement(b)) { - return a === b; - } - - if (a === b) { - return true; - } - - if (Object.prototype.toString.call(a) == "[object Array]") { - if (a.length !== b.length) { - return false; - } - - for (var i = 0, l = a.length; i < l; i += 1) { - if (!deepEqual(a[i], b[i])) { - return false; - } - } - - return true; - } - - var prop, aLength = 0, bLength = 0; - - for (prop in a) { - aLength += 1; - - if (!deepEqual(a[prop], b[prop])) { - return false; - } - } - - for (prop in b) { - bLength += 1; - } - - if (aLength != bLength) { - return false; - } - - return true; - }, - - functionName: function functionName(func) { - var name = func.displayName || func.name; - - // Use function decomposition as a last resort to get function - // name. Does not rely on function decomposition to work - if it - // doesn't debugging will be slightly less informative - // (i.e. toString will say 'spy' rather than 'myFunc'). - if (!name) { - var matches = func.toString().match(/function ([^\s\(]+)/); - name = matches && matches[1]; - } - - return name; - }, - - functionToString: function toString() { - if (this.getCall && this.callCount) { - var thisValue, prop, i = this.callCount; - - while (i--) { - thisValue = this.getCall(i).thisValue; - - for (prop in thisValue) { - if (thisValue[prop] === this) { - return prop; - } - } - } - } - - return this.displayName || "sinon fake"; - }, - - getConfig: function (custom) { - var config = {}; - custom = custom || {}; - var defaults = sinon.defaultConfig; - - for (var prop in defaults) { - if (defaults.hasOwnProperty(prop)) { - config[prop] = custom.hasOwnProperty(prop) ? custom[prop] : defaults[prop]; - } - } - - return config; - }, - - format: function (val) { - return "" + val; - }, - - defaultConfig: { - injectIntoThis: true, - injectInto: null, - properties: ["spy", "stub", "mock", "clock", "server", "requests"], - useFakeTimers: true, - useFakeServer: true - }, - - timesInWords: function timesInWords(count) { - return count == 1 && "once" || - count == 2 && "twice" || - count == 3 && "thrice" || - (count || 0) + " times"; - }, - - calledInOrder: function (spies) { - for (var i = 1, l = spies.length; i < l; i++) { - if (!spies[i - 1].calledBefore(spies[i])) { - return false; - } - } - - return true; - }, - - orderByFirstCall: function (spies) { - return spies.sort(function (a, b) { - // uuid, won't ever be equal - return a.getCall(0).callId < b.getCall(0).callId ? -1 : 1; - }); - } - }; -}()); - -if (typeof module == "object" && typeof require == "function") { - module.exports = sinon; - module.exports.spy = require("./sinon/spy"); - module.exports.stub = require("./sinon/stub"); - module.exports.mock = require("./sinon/mock"); - module.exports.collection = require("./sinon/collection"); - module.exports.assert = require("./sinon/assert"); - module.exports.sandbox = require("./sinon/sandbox"); - module.exports.test = require("./sinon/test"); - module.exports.testCase = require("./sinon/test_case"); - module.exports.assert = require("./sinon/assert"); -} - -/* @depend ../sinon.js */ -/*jslint eqeqeq: false, onevar: false, plusplus: false*/ -/*global module, require, sinon*/ -/** - * Spy functions - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -(function (sinon) { - var commonJSModule = typeof module == "object" && typeof require == "function"; - var spyCall; - var callId = 0; - var push = [].push; - - if (!sinon && commonJSModule) { - sinon = require("../sinon"); - } - - if (!sinon) { - return; - } - - function spy(object, property) { - if (!property && typeof object == "function") { - return spy.create(object); - } - - if (!object || !property) { - return spy.create(function () {}); - } - - var method = object[property]; - return sinon.wrapMethod(object, property, spy.create(method)); - } - - sinon.extend(spy, (function () { - var slice = Array.prototype.slice; - - function delegateToCalls(api, method, matchAny, actual, notCalled) { - api[method] = function () { - if (!this.called) { - return !!notCalled; - } - - var currentCall; - var matches = 0; - - for (var i = 0, l = this.callCount; i < l; i += 1) { - currentCall = this.getCall(i); - - if (currentCall[actual || method].apply(currentCall, arguments)) { - matches += 1; - - if (matchAny) { - return true; - } - } - } - - return matches === this.callCount; - }; - } - - function matchingFake(fakes, args, strict) { - if (!fakes) { - return; - } - - var alen = args.length; - - for (var i = 0, l = fakes.length; i < l; i++) { - if (fakes[i].matches(args, strict)) { - return fakes[i]; - } - } - } - - var uuid = 0; - - // Public API - var spyApi = { - reset: function () { - this.called = false; - this.calledOnce = false; - this.calledTwice = false; - this.calledThrice = false; - this.callCount = 0; - this.args = []; - this.returnValues = []; - this.thisValues = []; - this.exceptions = []; - this.callIds = []; - }, - - create: function create(func) { - var name; - - if (typeof func != "function") { - func = function () {}; - } else { - name = sinon.functionName(func); - } - - function proxy() { - return proxy.invoke(func, this, slice.call(arguments)); - } - - sinon.extend(proxy, spy); - delete proxy.create; - sinon.extend(proxy, func); - - proxy.reset(); - proxy.prototype = func.prototype; - proxy.displayName = name || "spy"; - proxy.toString = sinon.functionToString; - proxy._create = sinon.spy.create; - proxy.id = "spy#" + uuid++; - - return proxy; - }, - - invoke: function invoke(func, thisValue, args) { - var matching = matchingFake(this.fakes, args); - var exception, returnValue; - this.called = true; - this.callCount += 1; - this.calledOnce = this.callCount == 1; - this.calledTwice = this.callCount == 2; - this.calledThrice = this.callCount == 3; - push.call(this.thisValues, thisValue); - push.call(this.args, args); - push.call(this.callIds, callId++); - - try { - if (matching) { - returnValue = matching.invoke(func, thisValue, args); - } else { - returnValue = (this.func || func).apply(thisValue, args); - } - } catch (e) { - push.call(this.returnValues, undefined); - exception = e; - throw e; - } finally { - push.call(this.exceptions, exception); - } - - push.call(this.returnValues, returnValue); - - return returnValue; - }, - - getCall: function getCall(i) { - if (i < 0 || i >= this.callCount) { - return null; - } - - return spyCall.create(this, this.thisValues[i], this.args[i], - this.returnValues[i], this.exceptions[i], - this.callIds[i]); - }, - - calledBefore: function calledBefore(spyFn) { - if (!this.called) { - return false; - } - - if (!spyFn.called) { - return true; - } - - return this.callIds[0] < spyFn.callIds[0]; - }, - - calledAfter: function calledAfter(spyFn) { - if (!this.called || !spyFn.called) { - return false; - } - - return this.callIds[this.callCount - 1] > spyFn.callIds[spyFn.callCount - 1]; - }, - - withArgs: function () { - var args = slice.call(arguments); - - if (this.fakes) { - var match = matchingFake(this.fakes, args, true); - - if (match) { - return match; - } - } else { - this.fakes = []; - } - - var original = this; - var fake = this._create(); - fake.matchingAguments = args; - push.call(this.fakes, fake); - - fake.withArgs = function () { - return original.withArgs.apply(original, arguments); - }; - - return fake; - }, - - matches: function (args, strict) { - var margs = this.matchingAguments; - - if (margs.length <= args.length && - sinon.deepEqual(margs, args.slice(0, margs.length))) { - return !strict || margs.length == args.length; - } - }, - - printf: function (format) { - var spy = this; - var args = [].slice.call(arguments, 1); - var formatter; - - return (format || "").replace(/%(.)/g, function (match, specifyer) { - formatter = spyApi.formatters[specifyer]; - - if (typeof formatter == "function") { - return formatter.call(null, spy, args); - } else if (!isNaN(parseInt(specifyer), 10)) { - return sinon.format(args[specifyer - 1]); - } - - return "%" + specifyer; - }); - } - }; - - delegateToCalls(spyApi, "calledOn", true); - delegateToCalls(spyApi, "alwaysCalledOn", false, "calledOn"); - delegateToCalls(spyApi, "calledWith", true); - delegateToCalls(spyApi, "alwaysCalledWith", false, "calledWith"); - delegateToCalls(spyApi, "calledWithExactly", true); - delegateToCalls(spyApi, "alwaysCalledWithExactly", false, "calledWithExactly"); - delegateToCalls(spyApi, "neverCalledWith", false, "notCalledWith", true); - delegateToCalls(spyApi, "threw", true); - delegateToCalls(spyApi, "alwaysThrew", false, "threw"); - delegateToCalls(spyApi, "returned", true); - delegateToCalls(spyApi, "alwaysReturned", false, "returned"); - delegateToCalls(spyApi, "calledWithNew", true); - delegateToCalls(spyApi, "alwaysCalledWithNew", false, "calledWithNew"); - - spyApi.formatters = { - "c": function (spy) { - return sinon.timesInWords(spy.callCount); - }, - - "n": function (spy) { - return spy.toString(); - }, - - "C": function (spy) { - var calls = []; - - for (var i = 0, l = spy.callCount; i < l; ++i) { - push.call(calls, " " + spy.getCall(i).toString()); - } - - return calls.length > 0 ? "\n" + calls.join("\n") : ""; - }, - - "t": function (spy) { - var objects = []; - - for (var i = 0, l = spy.callCount; i < l; ++i) { - push.call(objects, sinon.format(spy.thisValues[i])); - } - - return objects.join(", "); - }, - - "*": function (spy, args) { - return args.join(", "); - } - }; - - return spyApi; - }())); - - spyCall = (function () { - return { - create: function create(spy, thisValue, args, returnValue, exception, id) { - var proxyCall = sinon.create(spyCall); - delete proxyCall.create; - proxyCall.proxy = spy; - proxyCall.thisValue = thisValue; - proxyCall.args = args; - proxyCall.returnValue = returnValue; - proxyCall.exception = exception; - proxyCall.callId = typeof id == "number" && id || callId++; - - return proxyCall; - }, - - calledOn: function calledOn(thisValue) { - return this.thisValue === thisValue; - }, - - calledWith: function calledWith() { - for (var i = 0, l = arguments.length; i < l; i += 1) { - if (!sinon.deepEqual(arguments[i], this.args[i])) { - return false; - } - } - - return true; - }, - - calledWithExactly: function calledWithExactly() { - return arguments.length == this.args.length && - this.calledWith.apply(this, arguments); - }, - - notCalledWith: function notCalledWith() { - for (var i = 0, l = arguments.length; i < l; i += 1) { - if (!sinon.deepEqual(arguments[i], this.args[i])) { - return true; - } - } - return false; - }, - - returned: function returned(value) { - return this.returnValue === value; - }, - - threw: function threw(error) { - if (typeof error == "undefined" || !this.exception) { - return !!this.exception; - } - - if (typeof error == "string") { - return this.exception.name == error; - } - - return this.exception === error; - }, - - calledWithNew: function calledWithNew(thisValue) { - return this.thisValue instanceof this.proxy; - }, - - calledBefore: function (other) { - return this.callId < other.callId; - }, - - calledAfter: function (other) { - return this.callId > other.callId; - }, - - toString: function () { - var callStr = this.proxy.toString() + "("; - var args = []; - - for (var i = 0, l = this.args.length; i < l; ++i) { - push.call(args, sinon.format(this.args[i])); - } - - callStr = callStr + args.join(", ") + ")"; - - if (typeof this.returnValue != "undefined") { - callStr += " => " + sinon.format(this.returnValue); - } - - if (this.exception) { - callStr += " !" + this.exception.name; - - if (this.exception.message) { - callStr += "(" + this.exception.message + ")"; - } - } - - return callStr; - } - }; - }()); - - spy.spyCall = spyCall; - - // This steps outside the module sandbox and will be removed - sinon.spyCall = spyCall; - - if (commonJSModule) { - module.exports = spy; - } else { - sinon.spy = spy; - } -}(typeof sinon == "object" && sinon || null)); - -/** - * @depend ../sinon.js - * @depend spy.js - */ -/*jslint eqeqeq: false, onevar: false*/ -/*global module, require, sinon*/ -/** - * Stub functions - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -(function (sinon) { - var commonJSModule = typeof module == "object" && typeof require == "function"; - - if (!sinon && commonJSModule) { - sinon = require("../sinon"); - } - - if (!sinon) { - return; - } - - function stub(object, property, func) { - if (!!func && typeof func != "function") { - throw new TypeError("Custom stub should be function"); - } - - var wrapper; - - if (func) { - wrapper = sinon.spy && sinon.spy.create ? sinon.spy.create(func) : func; - } else { - wrapper = stub.create(); - } - - if (!object && !property) { - return sinon.stub.create(); - } - - if (!property && !!object && typeof object == "object") { - for (var prop in object) { - if (object.hasOwnProperty(prop) && typeof object[prop] == "function") { - stub(object, prop); - } - } - - return object; - } - - return sinon.wrapMethod(object, property, wrapper); - } - - function getCallback(stub, args) { - if (stub.callArgAt < 0) { - for (var i = 0, l = args.length; i < l; ++i) { - if (!stub.callArgProp && typeof args[i] == "function") { - return args[i]; - } - - if (stub.callArgProp && args[i] && - typeof args[i][stub.callArgProp] == "function") { - return args[i][stub.callArgProp]; - } - } - - return null; - } - - return args[stub.callArgAt]; - } - - var join = Array.prototype.join; - - function getCallbackError(stub, func, args) { - if (stub.callArgAt < 0) { - var msg; - - if (stub.callArgProp) { - msg = sinon.functionName(stub) + - " expected to yield to '" + stub.callArgProp + - "', but no object with such a property was passed." - } else { - msg = sinon.functionName(stub) + - " expected to yield, but no callback was passed." - } - - if (args.length > 0) { - msg += " Received [" + join.call(args, ", ") + "]"; - } - - return msg; - } - - return "argument at index " + stub.callArgAt + " is not a function: " + func; - } - - function callCallback(stub, args) { - if (typeof stub.callArgAt == "number") { - var func = getCallback(stub, args); - - if (typeof func != "function") { - throw new TypeError(getCallbackError(stub, func, args)); - } - - func.apply(null, stub.callbackArguments); - } - } - - var uuid = 0; - - sinon.extend(stub, (function () { - var slice = Array.prototype.slice; - - function throwsException(error, message) { - if (typeof error == "string") { - this.exception = new Error(message || ""); - this.exception.name = error; - } else if (!error) { - this.exception = new Error("Error"); - } else { - this.exception = error; - } - - return this; - } - - return { - create: function create() { - var functionStub = function () { - if (functionStub.exception) { - throw functionStub.exception; - } - - callCallback(functionStub, arguments); - - return functionStub.returnValue; - }; - - functionStub.id = "stub#" + uuid++; - var orig = functionStub; - functionStub = sinon.spy.create(functionStub); - functionStub.func = orig; - - sinon.extend(functionStub, stub); - functionStub._create = sinon.stub.create; - functionStub.displayName = "stub"; - functionStub.toString = sinon.functionToString; - - return functionStub; - }, - - returns: function returns(value) { - this.returnValue = value; - - return this; - }, - - "throws": throwsException, - throwsException: throwsException, - - callsArg: function callsArg(pos) { - if (typeof pos != "number") { - throw new TypeError("argument index is not number"); - } - - this.callArgAt = pos; - this.callbackArguments = []; - - return this; - }, - - callsArgWith: function callsArgWith(pos) { - if (typeof pos != "number") { - throw new TypeError("argument index is not number"); - } - - this.callArgAt = pos; - this.callbackArguments = slice.call(arguments, 1); - - return this; - }, - - yields: function () { - this.callArgAt = -1; - this.callbackArguments = slice.call(arguments, 0); - - return this; - }, - - yieldsTo: function (prop) { - this.callArgAt = -1; - this.callArgProp = prop; - this.callbackArguments = slice.call(arguments, 1); - - return this; - } - }; - }())); - - if (commonJSModule) { - module.exports = stub; - } else { - sinon.stub = stub; - } -}(typeof sinon == "object" && sinon || null)); - -/** - * @depend ../sinon.js - * @depend stub.js - */ -/*jslint eqeqeq: false, onevar: false, nomen: false*/ -/*global module, require, sinon*/ -/** - * Mock functions. - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -(function (sinon) { - var commonJSModule = typeof module == "object" && typeof require == "function"; - var push = [].push; - - if (!sinon && commonJSModule) { - sinon = require("../sinon"); - } - - if (!sinon) { - return; - } - - function mock(object) { - if (!object) { - return sinon.expectation.create("Anonymous mock"); - } - - return mock.create(object); - } - - sinon.mock = mock; - - sinon.extend(mock, (function () { - function each(collection, callback) { - if (!collection) { - return; - } - - for (var i = 0, l = collection.length; i < l; i += 1) { - callback(collection[i]); - } - } - - return { - create: function create(object) { - if (!object) { - throw new TypeError("object is null"); - } - - var mockObject = sinon.extend({}, mock); - mockObject.object = object; - delete mockObject.create; - - return mockObject; - }, - - expects: function expects(method) { - if (!method) { - throw new TypeError("method is falsy"); - } - - if (!this.expectations) { - this.expectations = {}; - this.proxies = []; - } - - if (!this.expectations[method]) { - this.expectations[method] = []; - var mockObject = this; - - sinon.wrapMethod(this.object, method, function () { - return mockObject.invokeMethod(method, this, arguments); - }); - - push.call(this.proxies, method); - } - - var expectation = sinon.expectation.create(method); - push.call(this.expectations[method], expectation); - - return expectation; - }, - - restore: function restore() { - var object = this.object; - - each(this.proxies, function (proxy) { - if (typeof object[proxy].restore == "function") { - object[proxy].restore(); - } - }); - }, - - verify: function verify() { - var expectations = this.expectations || {}; - var messages = [], met = []; - - each(this.proxies, function (proxy) { - each(expectations[proxy], function (expectation) { - if (!expectation.met()) { - push.call(messages, expectation.toString()); - } else { - push.call(met, expectation.toString()); - } - }); - }); - - this.restore(); - - if (messages.length > 0) { - sinon.expectation.fail(messages.concat(met).join("\n")); - } - - return true; - }, - - invokeMethod: function invokeMethod(method, thisValue, args) { - var expectations = this.expectations && this.expectations[method]; - var length = expectations && expectations.length || 0; - - for (var i = 0; i < length; i += 1) { - if (!expectations[i].met() && - expectations[i].allowsCall(thisValue, args)) { - return expectations[i].apply(thisValue, args); - } - } - - var messages = []; - - for (i = 0; i < length; i += 1) { - push.call(messages, " " + expectations[i].toString()); - } - - messages.unshift("Unexpected call: " + sinon.spyCall.toString.call({ - proxy: method, - args: args - })); - - sinon.expectation.fail(messages.join("\n")); - } - }; - }())); - - var times = sinon.timesInWords; - - sinon.expectation = (function () { - var slice = Array.prototype.slice; - var _invoke = sinon.spy.invoke; - - function callCountInWords(callCount) { - if (callCount == 0) { - return "never called"; - } else { - return "called " + times(callCount); - } - } - - function expectedCallCountInWords(expectation) { - var min = expectation.minCalls; - var max = expectation.maxCalls; - - if (typeof min == "number" && typeof max == "number") { - var str = times(min); - - if (min != max) { - str = "at least " + str + " and at most " + times(max); - } - - return str; - } - - if (typeof min == "number") { - return "at least " + times(min); - } - - return "at most " + times(max); - } - - function receivedMinCalls(expectation) { - var hasMinLimit = typeof expectation.minCalls == "number"; - return !hasMinLimit || expectation.callCount >= expectation.minCalls; - } - - function receivedMaxCalls(expectation) { - if (typeof expectation.maxCalls != "number") { - return false; - } - - return expectation.callCount == expectation.maxCalls; - } - - return { - minCalls: 1, - maxCalls: 1, - - create: function create(methodName) { - var expectation = sinon.extend(sinon.stub.create(), sinon.expectation); - delete expectation.create; - expectation.method = methodName; - - return expectation; - }, - - invoke: function invoke(func, thisValue, args) { - this.verifyCallAllowed(thisValue, args); - - return _invoke.apply(this, arguments); - }, - - atLeast: function atLeast(num) { - if (typeof num != "number") { - throw new TypeError("'" + num + "' is not number"); - } - - if (!this.limitsSet) { - this.maxCalls = null; - this.limitsSet = true; - } - - this.minCalls = num; - - return this; - }, - - atMost: function atMost(num) { - if (typeof num != "number") { - throw new TypeError("'" + num + "' is not number"); - } - - if (!this.limitsSet) { - this.minCalls = null; - this.limitsSet = true; - } - - this.maxCalls = num; - - return this; - }, - - never: function never() { - return this.exactly(0); - }, - - once: function once() { - return this.exactly(1); - }, - - twice: function twice() { - return this.exactly(2); - }, - - thrice: function thrice() { - return this.exactly(3); - }, - - exactly: function exactly(num) { - if (typeof num != "number") { - throw new TypeError("'" + num + "' is not a number"); - } - - this.atLeast(num); - return this.atMost(num); - }, - - met: function met() { - return !this.failed && receivedMinCalls(this); - }, - - verifyCallAllowed: function verifyCallAllowed(thisValue, args) { - if (receivedMaxCalls(this)) { - this.failed = true; - sinon.expectation.fail(this.method + " already called " + times(this.maxCalls)); - } - - if ("expectedThis" in this && this.expectedThis !== thisValue) { - sinon.expectation.fail(this.method + " called with " + thisValue + " as thisValue, expected " + - this.expectedThis); - } - - if (!("expectedArguments" in this)) { - return; - } - - if (!args || args.length === 0) { - sinon.expectation.fail(this.method + " received no arguments, expected " + - this.expectedArguments.join()); - } - - if (args.length < this.expectedArguments.length) { - sinon.expectation.fail(this.method + " received too few arguments (" + args.join() + - "), expected " + this.expectedArguments.join()); - } - - if (this.expectsExactArgCount && - args.length != this.expectedArguments.length) { - sinon.expectation.fail(this.method + " received too many arguments (" + args.join() + - "), expected " + this.expectedArguments.join()); - } - - for (var i = 0, l = this.expectedArguments.length; i < l; i += 1) { - if (!sinon.deepEqual(this.expectedArguments[i], args[i])) { - sinon.expectation.fail(this.method + " received wrong arguments (" + args.join() + - "), expected " + this.expectedArguments.join()); - } - } - }, - - allowsCall: function allowsCall(thisValue, args) { - if (this.met()) { - return false; - } - - if ("expectedThis" in this && this.expectedThis !== thisValue) { - return false; - } - - if (!("expectedArguments" in this)) { - return true; - } - - args = args || []; - - if (args.length < this.expectedArguments.length) { - return false; - } - - if (this.expectsExactArgCount && - args.length != this.expectedArguments.length) { - return false; - } - - for (var i = 0, l = this.expectedArguments.length; i < l; i += 1) { - if (!sinon.deepEqual(this.expectedArguments[i], args[i])) { - return false; - } - } - - return true; - }, - - withArgs: function withArgs() { - this.expectedArguments = slice.call(arguments); - return this; - }, - - withExactArgs: function withExactArgs() { - this.withArgs.apply(this, arguments); - this.expectsExactArgCount = true; - return this; - }, - - on: function on(thisValue) { - this.expectedThis = thisValue; - return this; - }, - - toString: function () { - var args = (this.expectedArguments || []).slice(); - - if (!this.expectsExactArgCount) { - push.call(args, "[...]"); - } - - var callStr = sinon.spyCall.toString.call({ - proxy: this.method, args: args - }); - - var message = callStr.replace(", [...", "[, ...") + " " + - expectedCallCountInWords(this); - - if (this.met()) { - return "Expectation met: " + message; - } - - return "Expected " + message + " (" + - callCountInWords(this.callCount) + ")"; - }, - - verify: function verify() { - if (!this.met()) { - sinon.expectation.fail(this.toString()); - } - - return true; - }, - - fail: function (message) { - var exception = new Error(message); - exception.name = "ExpectationError"; - - throw exception; - } - }; - }()); - - if (commonJSModule) { - module.exports = mock; - } else { - sinon.mock = mock; - } -}(typeof sinon == "object" && sinon || null)); - -/** - * @depend ../sinon.js - * @depend stub.js - * @depend mock.js - */ -/*jslint eqeqeq: false, onevar: false, forin: true*/ -/*global module, require, sinon*/ -/** - * Collections of stubs, spies and mocks. - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -(function (sinon) { - var commonJSModule = typeof module == "object" && typeof require == "function"; - var push = [].push; - - if (!sinon && commonJSModule) { - sinon = require("../sinon"); - } - - if (!sinon) { - return; - } - - function getFakes(fakeCollection) { - if (!fakeCollection.fakes) { - fakeCollection.fakes = []; - } - - return fakeCollection.fakes; - } - - function each(fakeCollection, method) { - var fakes = getFakes(fakeCollection); - - for (var i = 0, l = fakes.length; i < l; i += 1) { - if (typeof fakes[i][method] == "function") { - fakes[i][method](); - } - } - } - - function compact(fakeCollection) { - var fakes = getFakes(fakeCollection); - var i = 0; - while (i < fakes.length) { - fakes.splice(i, 1); - } - } - - var collection = { - verify: function resolve() { - each(this, "verify"); - }, - - restore: function restore() { - each(this, "restore"); - compact(this); - }, - - verifyAndRestore: function verifyAndRestore() { - var exception; - - try { - this.verify(); - } catch (e) { - exception = e; - } - - this.restore(); - - if (exception) { - throw exception; - } - }, - - add: function add(fake) { - push.call(getFakes(this), fake); - return fake; - }, - - spy: function spy() { - return this.add(sinon.spy.apply(sinon, arguments)); - }, - - stub: function stub(object, property, value) { - if (property) { - var original = object[property]; - - if (typeof original != "function") { - if (!object.hasOwnProperty(property)) { - throw new TypeError("Cannot stub non-existent own property " + property); - } - - object[property] = value; - - return this.add({ - restore: function () { - object[property] = original; - } - }); - } - } - - return this.add(sinon.stub.apply(sinon, arguments)); - }, - - mock: function mock() { - return this.add(sinon.mock.apply(sinon, arguments)); - }, - - inject: function inject(obj) { - var col = this; - - obj.spy = function () { - return col.spy.apply(col, arguments); - }; - - obj.stub = function () { - return col.stub.apply(col, arguments); - }; - - obj.mock = function () { - return col.mock.apply(col, arguments); - }; - - return obj; - } - }; - - if (commonJSModule) { - module.exports = collection; - } else { - sinon.collection = collection; - } -}(typeof sinon == "object" && sinon || null)); - -/*jslint eqeqeq: false, plusplus: false, evil: true, onevar: false, browser: true, forin: false*/ -/*global module, require, window*/ -/** - * Fake timer API - * setTimeout - * setInterval - * clearTimeout - * clearInterval - * tick - * reset - * Date - * - * Inspired by jsUnitMockTimeOut from JsUnit - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -if (typeof sinon == "undefined") { - var sinon = {}; -} - -sinon.clock = (function () { - var id = 0; - - function addTimer(args, recurring) { - if (args.length === 0) { - throw new Error("Function requires at least 1 parameter"); - } - - var toId = id++; - var delay = args[1] || 0; - - if (!this.timeouts) { - this.timeouts = {}; - } - - this.timeouts[toId] = { - id: toId, - func: args[0], - callAt: this.now + delay - }; - - if (recurring === true) { - this.timeouts[toId].interval = delay; - } - - return toId; - } - - function parseTime(str) { - if (!str) { - return 0; - } - - var strings = str.split(":"); - var l = strings.length, i = l; - var ms = 0, parsed; - - if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) { - throw new Error("tick only understands numbers and 'h:m:s'"); - } - - while (i--) { - parsed = parseInt(strings[i], 10); - - if (parsed >= 60) { - throw new Error("Invalid time " + str); - } - - ms += parsed * Math.pow(60, (l - i - 1)); - } - - return ms * 1000; - } - - function createObject(object) { - var newObject; - - if (Object.create) { - newObject = Object.create(object); - } else { - var F = function () {}; - F.prototype = object; - newObject = new F(); - } - - newObject.Date.clock = newObject; - return newObject; - } - - return { - now: 0, - - create: function create(now) { - var clock = createObject(this); - - if (typeof now == "number") { - this.now = now; - } - - return clock; - }, - - setTimeout: function setTimeout(callback, timeout) { - return addTimer.call(this, arguments, false); - }, - - clearTimeout: function clearTimeout(timerId) { - if (!this.timeouts) { - this.timeouts = []; - } - - delete this.timeouts[timerId]; - }, - - setInterval: function setInterval(callback, timeout) { - return addTimer.call(this, arguments, true); - }, - - clearInterval: function clearInterval(timerId) { - this.clearTimeout(timerId); - }, - - tick: function tick(ms) { - ms = typeof ms == "number" ? ms : parseTime(ms); - var tickFrom = this.now, tickTo = this.now + ms, previous = this.now; - var timer = this.firstTimerInRange(tickFrom, tickTo); - - while (timer && tickFrom <= tickTo) { - if (this.timeouts[timer.id]) { - tickFrom = this.now = timer.callAt; - this.callTimer(timer); - } - - timer = this.firstTimerInRange(previous, tickTo); - previous = tickFrom; - } - - this.now = tickTo; - }, - - firstTimerInRange: function (from, to) { - var timer, smallest, originalTimer; - - for (var id in this.timeouts) { - if (this.timeouts.hasOwnProperty(id)) { - if (this.timeouts[id].callAt < from || this.timeouts[id].callAt > to) { - continue; - } - - if (!smallest || this.timeouts[id].callAt < smallest) { - originalTimer = this.timeouts[id]; - smallest = this.timeouts[id].callAt; - - timer = { - func: this.timeouts[id].func, - callAt: this.timeouts[id].callAt, - interval: this.timeouts[id].interval, - id: this.timeouts[id].id - }; - } - } - } - - return timer || null; - }, - - callTimer: function (timer) { - try { - if (typeof timer.func == "function") { - timer.func.call(null); - } else { - eval(timer.func); - } - } catch (e) {} - - if (!this.timeouts[timer.id]) { - return; - } - - if (typeof timer.interval == "number") { - this.timeouts[timer.id].callAt += timer.interval; - } else { - delete this.timeouts[timer.id]; - } - }, - - reset: function reset() { - this.timeouts = {}; - }, - - Date: (function () { - var NativeDate = Date; - - function ClockDate(year, month, date, hour, minute, second, ms) { - // Defensive and verbose to avoid potential harm in passing - // explicit undefined when user does not pass argument - switch (arguments.length) { - case 0: - return new NativeDate(ClockDate.clock.now); - case 1: - return new NativeDate(year); - case 2: - return new NativeDate(year, month); - case 3: - return new NativeDate(year, month, date); - case 4: - return new NativeDate(year, month, date, hour); - case 5: - return new NativeDate(year, month, date, hour, minute); - case 6: - return new NativeDate(year, month, date, hour, minute, second); - default: - return new NativeDate(year, month, date, hour, minute, second, ms); - } - } - - if (NativeDate.now) { - ClockDate.now = function now() { - return ClockDate.clock.now; - }; - } - - if (NativeDate.toSource) { - ClockDate.toSource = function toSource() { - return NativeDate.toSource(); - }; - } - - ClockDate.toString = function toString() { - return NativeDate.toString(); - }; - - ClockDate.prototype = NativeDate.prototype; - ClockDate.parse = NativeDate.parse; - ClockDate.UTC = NativeDate.UTC; - - return ClockDate; - }()) - }; -}()); - -sinon.timers = { - setTimeout: setTimeout, - clearTimeout: clearTimeout, - setInterval: setInterval, - clearInterval: clearInterval, - Date: Date -}; - -sinon.useFakeTimers = (function (global) { - var methods = ["Date", "setTimeout", "setInterval", "clearTimeout", "clearInterval"]; - - function restore() { - var method; - - for (var i = 0, l = this.methods.length; i < l; i++) { - method = this.methods[i]; - global[method] = this["_" + method]; - } - } - - function stubGlobal(method, clock) { - clock["_" + method] = global[method]; - - global[method] = function () { - return clock[method].apply(clock, arguments); - }; - - for (var prop in clock[method]) { - if (clock[method].hasOwnProperty(prop)) { - global[method][prop] = clock[method][prop]; - } - } - - global[method].clock = clock; - } - - return function useFakeTimers(now) { - var clock = sinon.clock.create(now); - clock.restore = restore; - clock.methods = Array.prototype.slice.call(arguments, - typeof now == "number" ? 1 : 0); - - if (clock.methods.length === 0) { - clock.methods = methods; - } - - for (var i = 0, l = clock.methods.length; i < l; i++) { - stubGlobal(clock.methods[i], clock); - } - - return clock; - }; -}(typeof global != "undefined" ? global : this)); - -if (typeof module == "object" && typeof require == "function") { - module.exports = sinon; -} - -/*jslint eqeqeq: false, onevar: false*/ -/*global sinon, module, require, ActiveXObject, XMLHttpRequest, DOMParser*/ -/** - * Minimal Event interface implementation - * - * Original implementation by Sven Fuchs: https://gist.github.com/995028 - * Modifications and tests by Christian Johansen. - * - * @author Sven Fuchs (svenfuchs@artweb-design.de) - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2011 Sven Fuchs, Christian Johansen - */ - -if (typeof sinon == "undefined") { - this.sinon = {}; -} - -(function () { - var push = [].push; - - sinon.Event = function Event(type, bubbles, cancelable) { - this.initEvent(type, bubbles, cancelable); - }; - - sinon.Event.prototype = { - initEvent: function(type, bubbles, cancelable) { - this.type = type; - this.bubbles = bubbles; - this.cancelable = cancelable; - }, - - stopPropagation: function () {}, - - preventDefault: function () { - this.defaultPrevented = true; - } - }; - - sinon.EventTarget = { - addEventListener: function addEventListener(event, listener, useCapture) { - this.eventListeners = this.eventListeners || {}; - this.eventListeners[event] = this.eventListeners[event] || []; - push.call(this.eventListeners[event], listener); - }, - - removeEventListener: function removeEventListener(event, listener, useCapture) { - var listeners = this.eventListeners && this.eventListeners[event] || []; - - for (var i = 0, l = listeners.length; i < l; ++i) { - if (listeners[i] == listener) { - return listeners.splice(i, 1); - } - } - }, - - dispatchEvent: function dispatchEvent(event) { - var type = event.type; - var listeners = this.eventListeners && this.eventListeners[type] || []; - - for (var i = 0; i < listeners.length; i++) { - if (typeof listeners[i] == "function") { - listeners[i].call(this, event); - } else { - listeners[i].handleEvent(event); - } - } - - return !!event.defaultPrevented; - } - }; -}()); - -/** - * @depend event.js - */ -/*jslint eqeqeq: false, onevar: false*/ -/*global sinon, module, require, ActiveXObject, XMLHttpRequest, DOMParser*/ -/** - * Fake XMLHttpRequest object - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -if (typeof sinon == "undefined") { - this.sinon = {}; -} - -sinon.xhr = { XMLHttpRequest: this.XMLHttpRequest }; - -sinon.FakeXMLHttpRequest = (function () { - /*jsl:ignore*/ - var unsafeHeaders = { - "Accept-Charset": true, - "Accept-Encoding": true, - "Connection": true, - "Content-Length": true, - "Cookie": true, - "Cookie2": true, - "Content-Transfer-Encoding": true, - "Date": true, - "Expect": true, - "Host": true, - "Keep-Alive": true, - "Referer": true, - "TE": true, - "Trailer": true, - "Transfer-Encoding": true, - "Upgrade": true, - "User-Agent": true, - "Via": true - }; - /*jsl:end*/ - - function FakeXMLHttpRequest() { - this.readyState = FakeXMLHttpRequest.UNSENT; - this.requestHeaders = {}; - this.requestBody = null; - this.status = 0; - this.statusText = ""; - - if (typeof FakeXMLHttpRequest.onCreate == "function") { - FakeXMLHttpRequest.onCreate(this); - } - } - - function verifyState(xhr) { - if (xhr.readyState !== FakeXMLHttpRequest.OPENED) { - throw new Error("INVALID_STATE_ERR"); - } - - if (xhr.sendFlag) { - throw new Error("INVALID_STATE_ERR"); - } - } - - sinon.extend(FakeXMLHttpRequest.prototype, sinon.EventTarget, { - async: true, - - open: function open(method, url, async, username, password) { - this.method = method; - this.url = url; - this.async = typeof async == "boolean" ? async : true; - this.username = username; - this.password = password; - this.responseText = null; - this.responseXML = null; - this.requestHeaders = {}; - this.sendFlag = false; - this.readyStateChange(FakeXMLHttpRequest.OPENED); - }, - - readyStateChange: function readyStateChange(state) { - this.readyState = state; - - if (typeof this.onreadystatechange == "function") { - this.onreadystatechange(); - } - - this.dispatchEvent(new sinon.Event("readystatechange")); - }, - - setRequestHeader: function setRequestHeader(header, value) { - verifyState(this); - - if (unsafeHeaders[header] || /^(Sec-|Proxy-)/.test(header)) { - throw new Error("Refused to set unsafe header \"" + header + "\""); - } - - if (this.requestHeaders[header]) { - this.requestHeaders[header] += "," + value; - } else { - this.requestHeaders[header] = value; - } - }, - - // Helps testing - setResponseHeaders: function setResponseHeaders(headers) { - this.responseHeaders = {}; - - for (var header in headers) { - if (headers.hasOwnProperty(header)) { - this.responseHeaders[header] = headers[header]; - } - } - - if (this.async) { - this.readyStateChange(FakeXMLHttpRequest.HEADERS_RECEIVED); - } - }, - - // Currently treats ALL data as a DOMString (i.e. no Document) - send: function send(data) { - verifyState(this); - - if (!/^(get|head)$/i.test(this.method)) { - if (this.requestHeaders["Content-Type"]) { - var value = this.requestHeaders["Content-Type"].split(";"); - this.requestHeaders["Content-Type"] = value[0] + ";charset=utf-8"; - } else { - this.requestHeaders["Content-Type"] = "text/plain;charset=utf-8"; - } - - this.requestBody = data; - } - - this.errorFlag = false; - this.sendFlag = this.async; - this.readyStateChange(FakeXMLHttpRequest.OPENED); - - if (typeof this.onSend == "function") { - this.onSend(this); - } - }, - - abort: function abort() { - this.aborted = true; - this.responseText = null; - this.errorFlag = true; - this.requestHeaders = {}; - - if (this.readyState > sinon.FakeXMLHttpRequest.UNSENT && this.sendFlag) { - this.readyStateChange(sinon.FakeXMLHttpRequest.DONE); - this.sendFlag = false; - } - - this.readyState = sinon.FakeXMLHttpRequest.UNSENT; - }, - - getResponseHeader: function getResponseHeader(header) { - if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) { - return null; - } - - if (/^Set-Cookie2?$/i.test(header)) { - return null; - } - - header = header.toLowerCase(); - - for (var h in this.responseHeaders) { - if (h.toLowerCase() == header) { - return this.responseHeaders[h]; - } - } - - return null; - }, - - getAllResponseHeaders: function getAllResponseHeaders() { - if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) { - return ""; - } - - var headers = ""; - - for (var header in this.responseHeaders) { - if (this.responseHeaders.hasOwnProperty(header) && - !/^Set-Cookie2?$/i.test(header)) { - headers += header + ": " + this.responseHeaders[header] + "\r\n"; - } - } - - return headers; - }, - - setResponseBody: function setResponseBody(body) { - if (this.readyState == FakeXMLHttpRequest.DONE) { - throw new Error("Request done"); - } - - if (this.async && this.readyState != FakeXMLHttpRequest.HEADERS_RECEIVED) { - throw new Error("No headers received"); - } - - var chunkSize = this.chunkSize || 10; - var index = 0; - this.responseText = ""; - - do { - if (this.async) { - this.readyStateChange(FakeXMLHttpRequest.LOADING); - } - - this.responseText += body.substring(index, index + chunkSize); - index += chunkSize; - } while (index < body.length); - - var type = this.getResponseHeader("Content-Type"); - - if (this.responseText && - (!type || /(text\/xml)|(application\/xml)|(\+xml)/.test(type))) { - try { - this.responseXML = FakeXMLHttpRequest.parseXML(this.responseText); - } catch (e) {} - } - - if (this.async) { - this.readyStateChange(FakeXMLHttpRequest.DONE); - } else { - this.readyState = FakeXMLHttpRequest.DONE; - } - }, - - respond: function respond(status, headers, body) { - this.setResponseHeaders(headers || {}); - this.status = typeof status == "number" ? status : 200; - this.statusText = FakeXMLHttpRequest.statusCodes[this.status]; - this.setResponseBody(body || ""); - } - }); - - sinon.extend(FakeXMLHttpRequest, { - UNSENT: 0, - OPENED: 1, - HEADERS_RECEIVED: 2, - LOADING: 3, - DONE: 4 - }); - - // Borrowed from JSpec - FakeXMLHttpRequest.parseXML = function parseXML(text) { - var xmlDoc; - - if (typeof DOMParser != "undefined") { - var parser = new DOMParser(); - xmlDoc = parser.parseFromString(text, "text/xml"); - } else { - xmlDoc = new ActiveXObject("Microsoft.XMLDOM"); - xmlDoc.async = "false"; - xmlDoc.loadXML(text); - } - - return xmlDoc; - }; - - FakeXMLHttpRequest.statusCodes = { - 100: "Continue", - 101: "Switching Protocols", - 200: "OK", - 201: "Created", - 202: "Accepted", - 203: "Non-Authoritative Information", - 204: "No Content", - 205: "Reset Content", - 206: "Partial Content", - 300: "Multiple Choice", - 301: "Moved Permanently", - 302: "Found", - 303: "See Other", - 304: "Not Modified", - 305: "Use Proxy", - 307: "Temporary Redirect", - 400: "Bad Request", - 401: "Unauthorized", - 402: "Payment Required", - 403: "Forbidden", - 404: "Not Found", - 405: "Method Not Allowed", - 406: "Not Acceptable", - 407: "Proxy Authentication Required", - 408: "Request Timeout", - 409: "Conflict", - 410: "Gone", - 411: "Length Required", - 412: "Precondition Failed", - 413: "Request Entity Too Large", - 414: "Request-URI Too Long", - 415: "Unsupported Media Type", - 416: "Requested Range Not Satisfiable", - 417: "Expectation Failed", - 422: "Unprocessable Entity", - 500: "Internal Server Error", - 501: "Not Implemented", - 502: "Bad Gateway", - 503: "Service Unavailable", - 504: "Gateway Timeout", - 505: "HTTP Version Not Supported" - }; - - return FakeXMLHttpRequest; -}()); - -(function (global) { - var GlobalXMLHttpRequest = global.XMLHttpRequest; - var GlobalActiveXObject = global.ActiveXObject; - var supportsActiveX = typeof ActiveXObject != "undefined"; - var supportsXHR = typeof XMLHttpRequest != "undefined"; - - sinon.useFakeXMLHttpRequest = function () { - sinon.FakeXMLHttpRequest.restore = function restore(keepOnCreate) { - if (supportsXHR) { - global.XMLHttpRequest = GlobalXMLHttpRequest; - } - - if (supportsActiveX) { - global.ActiveXObject = GlobalActiveXObject; - } - - delete sinon.FakeXMLHttpRequest.restore; - - if (keepOnCreate !== true) { - delete sinon.FakeXMLHttpRequest.onCreate; - } - }; - - if (supportsXHR) { - global.XMLHttpRequest = sinon.FakeXMLHttpRequest; - } - - if (supportsActiveX) { - global.ActiveXObject = function ActiveXObject(objId) { - if (objId == "Microsoft.XMLHTTP" || /^Msxml2\.XMLHTTP/i.test(objId)) { - return new sinon.FakeXMLHttpRequest(); - } - - return new GlobalActiveXObject(objId); - }; - } - - return sinon.FakeXMLHttpRequest; - }; -}(this)); - -if (typeof module == "object" && typeof require == "function") { - module.exports = sinon; -} - -/** - * @depend fake_xml_http_request.js - */ -/*jslint eqeqeq: false, onevar: false, regexp: false, plusplus: false*/ -/*global module, require, window*/ -/** - * The Sinon "server" mimics a web server that receives requests from - * sinon.FakeXMLHttpRequest and provides an API to respond to those requests, - * both synchronously and asynchronously. To respond synchronuously, canned - * answers have to be provided upfront. - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -if (typeof sinon == "undefined") { - var sinon = {}; -} - -sinon.fakeServer = (function () { - var push = [].push; - function F() {} - - function create(proto) { - F.prototype = proto; - return new F(); - } - - function responseArray(handler) { - var response = handler; - - if (Object.prototype.toString.call(handler) != "[object Array]") { - response = [200, {}, handler]; - } - - if (typeof response[2] != "string") { - throw new TypeError("Fake server response body should be string, but was " + - typeof response[2]); - } - - return response; - } - - var wloc = window.location; - var rCurrLoc = new RegExp("^" + wloc.protocol + "//" + wloc.host); - - function matchOne(response, reqMethod, reqUrl) { - var rmeth = response.method; - var matchMethod = !rmeth || rmeth.toLowerCase() == reqMethod.toLowerCase(); - var url = response.url; - var matchUrl = !url || url == reqUrl || (typeof url.test == "function" && url.test(reqUrl)); - - return matchMethod && matchUrl; - } - - function match(response, request) { - var requestMethod = this.getHTTPMethod(request); - var requestUrl = request.url; - - if (!/^https?:\/\//.test(requestUrl) || rCurrLoc.test(requestUrl)) { - requestUrl = requestUrl.replace(rCurrLoc, ""); - } - - if (matchOne(response, this.getHTTPMethod(request), requestUrl)) { - if (typeof response.response == "function") { - var args = [request].concat(requestUrl.match(response.url).slice(1)); - return response.response.apply(response, args); - } - - return true; - } - - return false; - } - - return { - create: function () { - var server = create(this); - this.xhr = sinon.useFakeXMLHttpRequest(); - server.requests = []; - - this.xhr.onCreate = function (xhrObj) { - server.addRequest(xhrObj); - }; - - return server; - }, - - addRequest: function addRequest(xhrObj) { - var server = this; - push.call(this.requests, xhrObj); - - xhrObj.onSend = function () { - server.handleRequest(this); - }; - - if (this.autoRespond && !this.responding) { - setTimeout(function () { - server.responding = false; - server.respond(); - }, this.autoRespondAfter || 10); - - this.responding = true; - } - }, - - getHTTPMethod: function getHTTPMethod(request) { - if (this.fakeHTTPMethods && /post/i.test(request.method)) { - var matches = (request.requestBody || "").match(/_method=([^\b;]+)/); - return !!matches ? matches[1] : request.method; - } - - return request.method; - }, - - handleRequest: function handleRequest(xhr) { - if (xhr.async) { - if (!this.queue) { - this.queue = []; - } - - push.call(this.queue, xhr); - } else { - this.processRequest(xhr); - } - }, - - respondWith: function respondWith(method, url, body) { - if (arguments.length == 1) { - this.response = responseArray(method); - } else { - if (!this.responses) { - this.responses = []; - } - - if (arguments.length == 2) { - body = url; - url = method; - method = null; - } - - push.call(this.responses, { - method: method, - url: url, - response: typeof body == "function" ? body : responseArray(body) - }); - } - }, - - respond: function respond() { - var queue = this.queue || []; - var request; - - while(request = queue.shift()) { - this.processRequest(request); - } - }, - - processRequest: function processRequest(request) { - try { - if (request.aborted) { - return; - } - - var response = this.response || [404, {}, ""]; - - if (this.responses) { - for (var i = 0, l = this.responses.length; i < l; i++) { - if (match.call(this, this.responses[i], request)) { - response = this.responses[i].response; - break; - } - } - } - - if (request.readyState != 4) { - request.respond(response[0], response[1], response[2]); - } - } catch (e) {} - }, - - restore: function restore() { - return this.xhr.restore && this.xhr.restore.apply(this.xhr, arguments); - } - }; -}()); - -if (typeof module == "object" && typeof require == "function") { - module.exports = sinon; -} - -/** - * @depend fake_server.js - * @depend fake_timers.js - */ -/*jslint browser: true, eqeqeq: false, onevar: false*/ -/*global sinon*/ -/** - * Add-on for sinon.fakeServer that automatically handles a fake timer along with - * the FakeXMLHttpRequest. The direct inspiration for this add-on is jQuery - * 1.3.x, which does not use xhr object's onreadystatehandler at all - instead, - * it polls the object for completion with setInterval. Dispite the direct - * motivation, there is nothing jQuery-specific in this file, so it can be used - * in any environment where the ajax implementation depends on setInterval or - * setTimeout. - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -(function () { - function Server() {} - Server.prototype = sinon.fakeServer; - - sinon.fakeServerWithClock = new Server(); - - sinon.fakeServerWithClock.addRequest = function addRequest(xhr) { - if (xhr.async) { - if (typeof setTimeout.clock == "object") { - this.clock = setTimeout.clock; - } else { - this.clock = sinon.useFakeTimers(); - this.resetClock = true; - } - - if (!this.longestTimeout) { - var clockSetTimeout = this.clock.setTimeout; - var clockSetInterval = this.clock.setInterval; - var server = this; - - this.clock.setTimeout = function (fn, timeout) { - server.longestTimeout = Math.max(timeout, server.longestTimeout || 0); - - return clockSetTimeout.apply(this, arguments); - }; - - this.clock.setInterval = function (fn, timeout) { - server.longestTimeout = Math.max(timeout, server.longestTimeout || 0); - - return clockSetInterval.apply(this, arguments); - }; - } - } - - return sinon.fakeServer.addRequest.call(this, xhr); - }; - - sinon.fakeServerWithClock.respond = function respond() { - var returnVal = sinon.fakeServer.respond.apply(this, arguments); - - if (this.clock) { - this.clock.tick(this.longestTimeout || 0); - this.longestTimeout = 0; - - if (this.resetClock) { - this.clock.restore(); - this.resetClock = false; - } - } - - return returnVal; - }; - - sinon.fakeServerWithClock.restore = function restore() { - if (this.clock) { - this.clock.restore(); - } - - return sinon.fakeServer.restore.apply(this, arguments); - }; -}()); - -/** - * @depend ../sinon.js - * @depend collection.js - * @depend util/fake_timers.js - * @depend util/fake_server_with_clock.js - */ -/*jslint eqeqeq: false, onevar: false, plusplus: false*/ -/*global require, module*/ -/** - * Manages fake collections as well as fake utilities such as Sinon's - * timers and fake XHR implementation in one convenient object. - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -if (typeof module == "object" && typeof require == "function") { - var sinon = require("../sinon"); - sinon.extend(sinon, require("./util/fake_timers")); -} - -(function () { - var push = [].push; - - function exposeValue(sandbox, config, key, value) { - if (!value) { - return; - } - - if (config.injectInto) { - config.injectInto[key] = value; - } else { - push.call(sandbox.args, value); - } - } - - function prepareSandboxFromConfig(config) { - var sandbox = sinon.create(sinon.sandbox); - - if (config.useFakeServer) { - if (typeof config.useFakeServer == "object") { - sandbox.serverPrototype = config.useFakeServer; - } - - sandbox.useFakeServer(); - } - - if (config.useFakeTimers) { - if (typeof config.useFakeTimers == "object") { - sandbox.useFakeTimers.apply(sandbox, config.useFakeTimers); - } else { - sandbox.useFakeTimers(); - } - } - - return sandbox; - } - - sinon.sandbox = sinon.extend(sinon.create(sinon.collection), { - useFakeTimers: function useFakeTimers() { - this.clock = sinon.useFakeTimers.apply(sinon, arguments); - - return this.add(this.clock); - }, - - serverPrototype: sinon.fakeServer, - - useFakeServer: function useFakeServer() { - var proto = this.serverPrototype || sinon.fakeServer; - - if (!proto || !proto.create) { - return null; - } - - this.server = proto.create(); - return this.add(this.server); - }, - - inject: function (obj) { - sinon.collection.inject.call(this, obj); - - if (this.clock) { - obj.clock = this.clock; - } - - if (this.server) { - obj.server = this.server; - obj.requests = this.server.requests; - } - - return obj; - }, - - create: function (config) { - if (!config) { - return sinon.create(sinon.sandbox); - } - - var sandbox = prepareSandboxFromConfig(config); - sandbox.args = sandbox.args || []; - var prop, value, exposed = sandbox.inject({}); - - if (config.properties) { - for (var i = 0, l = config.properties.length; i < l; i++) { - prop = config.properties[i]; - value = exposed[prop] || prop == "sandbox" && sandbox; - exposeValue(sandbox, config, prop, value); - } - } else { - exposeValue(sandbox, config, "sandbox", value); - } - - return sandbox; - } - }); - - sinon.sandbox.useFakeXMLHttpRequest = sinon.sandbox.useFakeServer; - - if (typeof module != "undefined") { - module.exports = sinon.sandbox; - } -}()); - -/** - * @depend ../sinon.js - * @depend stub.js - * @depend mock.js - * @depend sandbox.js - */ -/*jslint eqeqeq: false, onevar: false, forin: true, plusplus: false*/ -/*global module, require, sinon*/ -/** - * Test function, sandboxes fakes - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -(function (sinon) { - var commonJSModule = typeof module == "object" && typeof require == "function"; - - if (!sinon && commonJSModule) { - sinon = require("../sinon"); - } - - if (!sinon) { - return; - } - - function test(callback) { - var type = typeof callback; - - if (type != "function") { - throw new TypeError("sinon.test needs to wrap a test function, got " + type); - } - - return function () { - var config = sinon.getConfig(sinon.config); - config.injectInto = config.injectIntoThis && this || config.injectInto; - var sandbox = sinon.sandbox.create(config); - var exception, result; - var args = Array.prototype.slice.call(arguments).concat(sandbox.args); - - try { - result = callback.apply(this, args); - } catch (e) { - exception = e; - } - - sandbox.verifyAndRestore(); - - if (exception) { - throw exception; - } - - return result; - }; - } - - test.config = { - injectIntoThis: true, - injectInto: null, - properties: ["spy", "stub", "mock", "clock", "server", "requests"], - useFakeTimers: true, - useFakeServer: true - }; - - if (commonJSModule) { - module.exports = test; - } else { - sinon.test = test; - } -}(typeof sinon == "object" && sinon || null)); - -/** - * @depend ../sinon.js - * @depend test.js - */ -/*jslint eqeqeq: false, onevar: false, eqeqeq: false*/ -/*global module, require, sinon*/ -/** - * Test case, sandboxes all test functions - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -(function (sinon) { - var commonJSModule = typeof module == "object" && typeof require == "function"; - - if (!sinon && commonJSModule) { - sinon = require("../sinon"); - } - - if (!sinon || !Object.prototype.hasOwnProperty) { - return; - } - - function createTest(property, setUp, tearDown) { - return function () { - if (setUp) { - setUp.apply(this, arguments); - } - - var exception, result; - - try { - result = property.apply(this, arguments); - } catch (e) { - exception = e; - } - - if (tearDown) { - tearDown.apply(this, arguments); - } - - if (exception) { - throw exception; - } - - return result; - }; - } - - function testCase(tests, prefix) { - /*jsl:ignore*/ - if (!tests || typeof tests != "object") { - throw new TypeError("sinon.testCase needs an object with test functions"); - } - /*jsl:end*/ - - prefix = prefix || "test"; - var rPrefix = new RegExp("^" + prefix); - var methods = {}, testName, property, method; - var setUp = tests.setUp; - var tearDown = tests.tearDown; - - for (testName in tests) { - if (tests.hasOwnProperty(testName)) { - property = tests[testName]; - - if (/^(setUp|tearDown)$/.test(testName)) { - continue; - } - - if (typeof property == "function" && rPrefix.test(testName)) { - method = property; - - if (setUp || tearDown) { - method = createTest(property, setUp, tearDown); - } - - methods[testName] = sinon.test(method); - } else { - methods[testName] = tests[testName]; - } - } - } - - return methods; - } - - if (commonJSModule) { - module.exports = testCase; - } else { - sinon.testCase = testCase; - } -}(typeof sinon == "object" && sinon || null)); - -/** - * @depend ../sinon.js - * @depend stub.js - */ -/*jslint eqeqeq: false, onevar: false, nomen: false, plusplus: false*/ -/*global module, require, sinon*/ -/** - * Assertions matching the test spy retrieval interface. - * - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2010-2011 Christian Johansen - */ - -(function (sinon) { - var commonJSModule = typeof module == "object" && typeof require == "function"; - var slice = Array.prototype.slice; - var assert; - - if (!sinon && commonJSModule) { - sinon = require("../sinon"); - } - - if (!sinon) { - return; - } - - function verifyIsStub() { - var method; - - for (var i = 0, l = arguments.length; i < l; ++i) { - method = arguments[i]; - - if (!method) { - assert.fail("fake is not a spy"); - } - - if (typeof method != "function") { - assert.fail(method + " is not a function"); - } - - if (typeof method.getCall != "function") { - assert.fail(method + " is not stubbed"); - } - } - } - - function failAssertion(object, msg) { - var failMethod = object.fail || assert.fail; - failMethod.call(object, msg); - } - - function mirrorPropAsAssertion(name, method, message) { - if (arguments.length == 2) { - message = method; - method = name; - } - - assert[name] = function (fake) { - verifyIsStub(fake); - - var args = slice.call(arguments, 1); - var failed = false; - - if (typeof method == "function") { - failed = !method(fake); - } else { - failed = typeof fake[method] == "function" ? - !fake[method].apply(fake, args) : !fake[method]; - } - - if (failed) { - failAssertion(this, fake.printf.apply(fake, [message].concat(args))); - } else { - assert.pass(name); - } - }; - } - - function exposedName(prefix, prop) { - return !prefix || /^fail/.test(prop) ? prop : - prefix + prop.slice(0, 1).toUpperCase() + prop.slice(1); - }; - - assert = { - failException: "AssertError", - - fail: function fail(message) { - var error = new Error(message); - error.name = this.failException || assert.failException; - - throw error; - }, - - pass: function pass(assertion) {}, - - callOrder: function assertCallOrder() { - verifyIsStub.apply(null, arguments); - var expected = "", actual = ""; - - if (!sinon.calledInOrder(arguments)) { - try { - expected = [].join.call(arguments, ", "); - actual = sinon.orderByFirstCall(slice.call(arguments)).join(", "); - } catch (e) {} - - failAssertion(this, "expected " + expected + " to be " + - "called in order but were called as " + actual); - } else { - assert.pass("callOrder"); - } - }, - - callCount: function assertCallCount(method, count) { - verifyIsStub(method); - - if (method.callCount != count) { - var msg = "expected %n to be called " + sinon.timesInWords(count) + - " but was called %c%C"; - failAssertion(this, method.printf(msg)); - } else { - assert.pass("callCount"); - } - }, - - expose: function expose(target, options) { - if (!target) { - throw new TypeError("target is null or undefined"); - } - - var o = options || {}; - var prefix = typeof o.prefix == "undefined" && "assert" || o.prefix; - var includeFail = typeof o.includeFail == "undefined" || !!o.includeFail; - - for (var method in this) { - if (method != "export" && (includeFail || !/^(fail)/.test(method))) { - target[exposedName(prefix, method)] = this[method]; - } - } - - return target; - } - }; - - mirrorPropAsAssertion("called", "expected %n to have been called at least once but was never called"); - mirrorPropAsAssertion("notCalled", function (spy) { return !spy.called; }, - "expected %n to not have been called but was called %c%C"); - mirrorPropAsAssertion("calledOnce", "expected %n to be called once but was called %c%C"); - mirrorPropAsAssertion("calledTwice", "expected %n to be called twice but was called %c%C"); - mirrorPropAsAssertion("calledThrice", "expected %n to be called thrice but was called %c%C"); - mirrorPropAsAssertion("calledOn", "expected %n to be called with %1 as this but was called with %t"); - mirrorPropAsAssertion("alwaysCalledOn", "expected %n to always be called with %1 as this but was called with %t"); - mirrorPropAsAssertion("calledWith", "expected %n to be called with arguments %*%C"); - mirrorPropAsAssertion("alwaysCalledWith", "expected %n to always be called with arguments %*%C"); - mirrorPropAsAssertion("calledWithExactly", "expected %n to be called with exact arguments %*%C"); - mirrorPropAsAssertion("alwaysCalledWithExactly", "expected %n to always be called with exact arguments %*%C"); - mirrorPropAsAssertion("neverCalledWith", "expected %n to never be called with arguments %*%C"); - mirrorPropAsAssertion("threw", "%n did not throw exception%C"); - mirrorPropAsAssertion("alwaysThrew", "%n did not always throw exception%C"); - - if (commonJSModule) { - module.exports = assert; - } else { - sinon.assert = assert; - } -}(typeof sinon == "object" && sinon || null)); diff --git a/spec/javascripts/support/jasmine.yml b/spec/javascripts/support/jasmine.yml deleted file mode 100644 index ab759a75862..00000000000 --- a/spec/javascripts/support/jasmine.yml +++ /dev/null @@ -1,74 +0,0 @@ -# src_files -# -# Return an array of filepaths relative to src_dir to include before jasmine specs. -# Default: [] -# -# EXAMPLE: -# -# src_files: -# - lib/source1.js -# - lib/source2.js -# - dist/**/*.js -# -src_files: - - spec/javascripts/support/jquery.min.js - - spec/javascripts/support/jquery-ui-1.8.16.custom.min.js - - app/assets/javascripts/active_admin/compiled/lib/**/*.js - - app/assets/javascripts/active_admin/compiled/components/**/*.js - -# stylesheets -# -# Return an array of stylesheet filepaths relative to src_dir to include before jasmine specs. -# Default: [] -# -# EXAMPLE: -# -# stylesheets: -# - css/style.css -# - stylesheets/*.css -# -stylesheets: - -# helpers -# -# Return an array of filepaths relative to spec_dir to include before jasmine specs. -# Default: ["helpers/**/*.js"] -# -# EXAMPLE: -# -helpers: - - helpers/**/*.js - - -# spec_files -# -# Return an array of filepaths relative to spec_dir to include. -# Default: ["**/*[sS]pec.js"] -# -# EXAMPLE: -# -spec_files: - - **/*[sS]pec.js - - -# src_dir -# -# Source directory path. Your src_files must be returned relative to this path. Will use root if left blank. -# Default: project root -# -# EXAMPLE: -# -# src_dir: public -# -src_dir: - -# spec_dir -# -# Spec directory path. Your spec_files must be returned relative to this path. -# Default: spec/javascripts -# -# EXAMPLE: -# -# spec_dir: spec/javascripts -# -spec_dir: spec/javascripts/ diff --git a/spec/javascripts/support/jasmine_config.rb b/spec/javascripts/support/jasmine_config.rb deleted file mode 100644 index 47286f23055..00000000000 --- a/spec/javascripts/support/jasmine_config.rb +++ /dev/null @@ -1,23 +0,0 @@ -module Jasmine - class Config - - # Add your overrides or custom config code here - - end -end - - -# Note - this is necessary for rspec2, which has removed the backtrace -module Jasmine - class SpecBuilder - def declare_spec(parent, spec) - me = self - example_name = spec["name"] - @spec_ids << spec["id"] - backtrace = @example_locations[parent.description + " " + example_name] - parent.it example_name, {} do - me.report_spec(spec["id"]) - end - end - end -end diff --git a/spec/javascripts/support/jasmine_runner.rb b/spec/javascripts/support/jasmine_runner.rb deleted file mode 100644 index 13ebce0cdd0..00000000000 --- a/spec/javascripts/support/jasmine_runner.rb +++ /dev/null @@ -1,32 +0,0 @@ -$:.unshift(ENV['JASMINE_GEM_PATH']) if ENV['JASMINE_GEM_PATH'] # for gem testing purposes - -require 'rubygems' -require 'jasmine' -jasmine_config_overrides = File.expand_path(File.join(File.dirname(__FILE__), 'jasmine_config.rb')) -require jasmine_config_overrides if File.exist?(jasmine_config_overrides) -if Jasmine::rspec2? - require 'rspec' -else - require 'spec' -end - -jasmine_config = Jasmine::Config.new -spec_builder = Jasmine::SpecBuilder.new(jasmine_config) - -should_stop = false - -if Jasmine::rspec2? - RSpec.configuration.after(:suite) do - spec_builder.stop if should_stop - end -else - Spec::Runner.configure do |config| - config.after(:suite) do - spec_builder.stop if should_stop - end - end -end - -spec_builder.start -should_stop = true -spec_builder.declare_suites \ No newline at end of file diff --git a/spec/javascripts/support/jquery-ui-1.8.16.custom.min.js b/spec/javascripts/support/jquery-ui-1.8.16.custom.min.js deleted file mode 100755 index 350b024139a..00000000000 --- a/spec/javascripts/support/jquery-ui-1.8.16.custom.min.js +++ /dev/null @@ -1,34 +0,0 @@ -/*! - * jQuery UI 1.8.16 - * - * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI - */ -(function(c,j){function k(a,b){var d=a.nodeName.toLowerCase();if("area"===d){b=a.parentNode;d=b.name;if(!a.href||!d||b.nodeName.toLowerCase()!=="map")return false;a=c("img[usemap=#"+d+"]")[0];return!!a&&l(a)}return(/input|select|textarea|button|object/.test(d)?!a.disabled:"a"==d?a.href||b:b)&&l(a)}function l(a){return!c(a).parents().andSelf().filter(function(){return c.curCSS(this,"visibility")==="hidden"||c.expr.filters.hidden(this)}).length}c.ui=c.ui||{};if(!c.ui.version){c.extend(c.ui,{version:"1.8.16", -keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91}});c.fn.extend({propAttr:c.fn.prop||c.fn.attr,_focus:c.fn.focus,focus:function(a,b){return typeof a==="number"?this.each(function(){var d= -this;setTimeout(function(){c(d).focus();b&&b.call(d)},a)}):this._focus.apply(this,arguments)},scrollParent:function(){var a;a=c.browser.msie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(c.curCSS(this,"position",1))&&/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(c.curCSS(this, -"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0);return/fixed/.test(this.css("position"))||!a.length?c(document):a},zIndex:function(a){if(a!==j)return this.css("zIndex",a);if(this.length){a=c(this[0]);for(var b;a.length&&a[0]!==document;){b=a.css("position");if(b==="absolute"||b==="relative"||b==="fixed"){b=parseInt(a.css("zIndex"),10);if(!isNaN(b)&&b!==0)return b}a=a.parent()}}return 0},disableSelection:function(){return this.bind((c.support.selectstart?"selectstart": -"mousedown")+".ui-disableSelection",function(a){a.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}});c.each(["Width","Height"],function(a,b){function d(f,g,m,n){c.each(e,function(){g-=parseFloat(c.curCSS(f,"padding"+this,true))||0;if(m)g-=parseFloat(c.curCSS(f,"border"+this+"Width",true))||0;if(n)g-=parseFloat(c.curCSS(f,"margin"+this,true))||0});return g}var e=b==="Width"?["Left","Right"]:["Top","Bottom"],h=b.toLowerCase(),i={innerWidth:c.fn.innerWidth,innerHeight:c.fn.innerHeight, -outerWidth:c.fn.outerWidth,outerHeight:c.fn.outerHeight};c.fn["inner"+b]=function(f){if(f===j)return i["inner"+b].call(this);return this.each(function(){c(this).css(h,d(this,f)+"px")})};c.fn["outer"+b]=function(f,g){if(typeof f!=="number")return i["outer"+b].call(this,f);return this.each(function(){c(this).css(h,d(this,f,true,g)+"px")})}});c.extend(c.expr[":"],{data:function(a,b,d){return!!c.data(a,d[3])},focusable:function(a){return k(a,!isNaN(c.attr(a,"tabindex")))},tabbable:function(a){var b=c.attr(a, -"tabindex"),d=isNaN(b);return(d||b>=0)&&k(a,!d)}});c(function(){var a=document.body,b=a.appendChild(b=document.createElement("div"));c.extend(b.style,{minHeight:"100px",height:"auto",padding:0,borderWidth:0});c.support.minHeight=b.offsetHeight===100;c.support.selectstart="onselectstart"in b;a.removeChild(b).style.display="none"});c.extend(c.ui,{plugin:{add:function(a,b,d){a=c.ui[a].prototype;for(var e in d){a.plugins[e]=a.plugins[e]||[];a.plugins[e].push([b,d[e]])}},call:function(a,b,d){if((b=a.plugins[b])&& -a.element[0].parentNode)for(var e=0;e0)return true;a[b]=1;d=a[b]>0;a[b]=0;return d},isOverAxis:function(a,b,d){return a>b&&a").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){cl||(cl=c.createElement("iframe"),cl.frameBorder=cl.width=cl.height=0),b.appendChild(cl);if(!cm||!cl.createElement)cm=(cl.contentWindow||cl.contentDocument).document,cm.write((c.compatMode==="CSS1Compat"?"":"")+""),cm.close();d=cm.createElement(a),cm.body.appendChild(d),e=f.css(d,"display"),b.removeChild(cl)}ck[a]=e}return ck[a]}function cu(a,b){var c={};f.each(cq.concat.apply([],cq.slice(0,b)),function(){c[this]=a});return c}function ct(){cr=b}function cs(){setTimeout(ct,0);return cr=f.now()}function cj(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ci(){try{return new a.XMLHttpRequest}catch(b){}}function cc(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){if(c!=="border")for(;g=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?parseFloat(d):j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.1",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c
a",d=q.getElementsByTagName("*"),e=q.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=q.getElementsByTagName("input")[0],b={leadingWhitespace:q.firstChild.nodeType===3,tbody:!q.getElementsByTagName("tbody").length,htmlSerialize:!!q.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:q.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete q.test}catch(s){b.deleteExpando=!1}!q.addEventListener&&q.attachEvent&&q.fireEvent&&(q.attachEvent("onclick",function(){b.noCloneEvent=!1}),q.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),q.appendChild(i),k=c.createDocumentFragment(),k.appendChild(q.lastChild),b.checkClone=k.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,k.removeChild(i),k.appendChild(q),q.innerHTML="",a.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",q.style.width="2px",q.appendChild(j),b.reliableMarginRight=(parseInt((a.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0);if(q.attachEvent)for(o in{submit:1,change:1,focusin:1})n="on"+o,p=n in q,p||(q.setAttribute(n,"return;"),p=typeof q[n]=="function"),b[o+"Bubbles"]=p;k.removeChild(q),k=g=h=j=q=i=null,f(function(){var a,d,e,g,h,i,j,k,m,n,o,r=c.getElementsByTagName("body")[0];!r||(j=1,k="position:absolute;top:0;left:0;width:1px;height:1px;margin:0;",m="visibility:hidden;border:0;",n="style='"+k+"border:5px solid #000;padding:0;'",o="
"+""+"
",a=c.createElement("div"),a.style.cssText=m+"width:0;height:0;position:static;top:0;margin-top:"+j+"px",r.insertBefore(a,r.firstChild),q=c.createElement("div"),a.appendChild(q),q.innerHTML="
t
",l=q.getElementsByTagName("td"),p=l[0].offsetHeight===0,l[0].style.display="",l[1].style.display="none",b.reliableHiddenOffsets=p&&l[0].offsetHeight===0,q.innerHTML="",q.style.width=q.style.paddingLeft="1px",f.boxModel=b.boxModel=q.offsetWidth===2,typeof q.style.zoom!="undefined"&&(q.style.display="inline",q.style.zoom=1,b.inlineBlockNeedsLayout=q.offsetWidth===2,q.style.display="",q.innerHTML="
",b.shrinkWrapBlocks=q.offsetWidth!==2),q.style.cssText=k+m,q.innerHTML=o,d=q.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,i={doesNotAddBorder:e.offsetTop!==5,doesAddBorderForTableAndCells:h.offsetTop===5},e.style.position="fixed",e.style.top="20px",i.fixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",i.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,i.doesNotIncludeMarginInBodyOffset=r.offsetTop!==j,r.removeChild(a),q=a=null,f.extend(b,i))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.nodeName.toLowerCase()]||f.valHooks[g.type];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;h=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/\bhover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function(a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")}; -f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;le&&i.push({elem:this,matches:d.slice(e)});for(j=0;j0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h0)for(h=g;h=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/",""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div
","
"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function() -{for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1>");try{for(var c=0,d=this.length;c1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||!bc.test("<"+a.nodeName)?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!_.test(k))k=b.createTextNode(k);else{k=k.replace(Y,"<$1>");var l=(Z.exec(k)||["",""])[1].toLowerCase(),m=bg[l]||bg._default,n=m[0],o=b.createElement("div");b===c?bh.appendChild(o):U(b).appendChild(o),o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=$.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]===""&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&X.test(k)&&o.insertBefore(b.createTextNode(X.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return br.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bq,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bq.test(g)?g.replace(bq,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bz(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bA=function(a,b){var c,d,e;b=b.replace(bs,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b)));return c}),c.documentElement.currentStyle&&(bB=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f===null&&g&&(e=g[b])&&(f=e),!bt.test(f)&&bu.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f||0,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),bz=bA||bB,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bD=/%20/g,bE=/\[\]$/,bF=/\r?\n/g,bG=/#.*$/,bH=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bI=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bJ=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bK=/^(?:GET|HEAD)$/,bL=/^\/\//,bM=/\?/,bN=/)<[^<]*)*<\/script>/gi,bO=/^(?:select|textarea)/i,bP=/\s+/,bQ=/([?&])_=[^&]*/,bR=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bS=f.fn.load,bT={},bU={},bV,bW,bX=["*/"]+["*"];try{bV=e.href}catch(bY){bV=c.createElement("a"),bV.href="",bV=bV.href}bW=bR.exec(bV.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bS)return bS.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
").append(c.replace(bN,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bO.test(this.nodeName)||bI.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bF,"\r\n")}}):{name:b.name,value:c.replace(bF,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b_(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b_(a,b);return a},ajaxSettings:{url:bV,isLocal:bJ.test(bW[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bX},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bZ(bT),ajaxTransport:bZ(bU),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?cb(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cc(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bH.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bG,"").replace(bL,bW[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bP),d.crossDomain==null&&(r=bR.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bW[1]&&r[2]==bW[2]&&(r[3]||(r[1]==="http:"?80:443))==(bW[3]||(bW[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),b$(bT,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bK.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bM.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bQ,"$1_="+x);d.url=y+(y===d.url?(bM.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bX+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=b$(bU,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)ca(g,a[g],c,e);return d.join("&").replace(bD,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cd=f.now(),ce=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cd++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(ce.test(b.url)||e&&ce.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(ce,l),b.url===j&&(e&&(k=k.replace(ce,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var cf=a.ActiveXObject?function(){for(var a in ch)ch[a](0,1)}:!1,cg=0,ch;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ci()||cj()}:ci,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cf&&delete ch[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cg,cf&&(ch||(ch={},f(a).unload(cf)),ch[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var ck={},cl,cm,cn=/^(?:toggle|show|hide)$/,co=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cp,cq=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cr;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cu("show",3),a,b,c);for(var g=0,h=this.length;g=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cy(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cy(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,d,"padding")):this[d]():null},f.fn["outer"+c]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,d,a?"margin":"border")):this[d]():null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c],h=e.document.body;return e.document.compatMode==="CSS1Compat"&&g||h&&h["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var i=f.css(e,d),j=parseFloat(i);return f.isNumeric(j)?j:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window); \ No newline at end of file diff --git a/spec/locales/i18n_spec.rb b/spec/locales/i18n_spec.rb new file mode 100644 index 00000000000..14a7c210af8 --- /dev/null +++ b/spec/locales/i18n_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true +require "i18n/tasks" +require "i18n-spec" + +Dir.glob("config/locales/*.yml") do |locale_file| + RSpec.describe locale_file do + it { is_expected.to be_parseable } + it { is_expected.to have_one_top_level_namespace } + it { is_expected.to be_named_like_top_level_namespace } + it { is_expected.to_not have_legacy_interpolations } + it { is_expected.to have_a_valid_locale } + it { is_expected.to be_a_subset_of "config/locales/en.yml" } + end +end + +RSpec.describe "I18n" do + let(:i18n) { I18n::Tasks::BaseTask.new } + + let(:unused_keys) { i18n.unused_keys } + let(:unused_key_count) { unused_keys.leaves.count } + let(:unused_key_failure_msg) do + "#{unused_key_count} unused i18n keys, run `bin/i18n-tasks unused` to show them" + end + + let(:inconsistent_interpolations) { i18n.inconsistent_interpolations } + let(:inconsistent_interpolation_key_count) { inconsistent_interpolations.leaves.count } + let(:inconsistent_interpolation_failure_msg) do + "#{inconsistent_interpolation_key_count} inconsistent interpolations, run `bin/i18n-tasks check-consistent-interpolations` to show them" + end + + let(:non_normalized_paths) { i18n.non_normalized_paths } + let(:non_normalized_paths_count) { non_normalized_paths.size } + let(:non_normalized_paths_failure_msg) do + "#{non_normalized_paths_count} non-normalized paths, run `bin/i18n-tasks check-normalized` to show them" + end + + it "does not have unused keys" do + expect(unused_keys).to be_empty, unused_key_failure_msg + end + + it "does not have inconsistent interpolations" do + expect(inconsistent_interpolations).to be_empty, inconsistent_interpolation_failure_msg + end + + it "does not have non-normalized paths" do + expect(non_normalized_paths).to be_empty, non_normalized_paths_failure_msg + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 00000000000..01eb64555e5 --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true +require "spec_helper" + +ENV["RAILS_ENV"] = "test" + +require_relative "../tasks/test_application" + +require "#{ActiveAdmin::TestApplication.new.full_app_dir}/config/environment.rb" + +require "rspec/rails" + +# 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 +ActiveAdmin.application.load! + +RSpec.configure do |config| + config.use_transactional_fixtures = true + config.use_instantiated_fixtures = false + config.render_views = false + + config.include Devise::Test::ControllerHelpers, type: :controller + + require "support/active_admin_integration_spec_helper" + config.include ActiveAdminIntegrationSpecHelper +end + +# Force deprecations to raise an exception. +ActiveAdmin::DeprecationHelper.behavior = :raise diff --git a/spec/requests/default_namespace_spec.rb b/spec/requests/default_namespace_spec.rb new file mode 100644 index 00000000000..527f2c12a99 --- /dev/null +++ b/spec/requests/default_namespace_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe ActiveAdmin::Application, type: :request do + let(:resource) { ActiveAdmin.register Category } + + [false, nil].each do |value| + describe "with a #{value} default namespace" do + around do |example| + with_custom_default_namespace(value) { example.call } + end + + it "should generate resource paths" do + expect(resource.route_collection_path).to eq "/categories" + end + + it "should generate a log out path" do + expect(destroy_admin_user_session_path).to eq "/logout" + end + + it "should generate a log in path" do + expect(new_admin_user_session_path).to eq "/login" + end + end + end + + describe "with a test default namespace" do + around do |example| + with_custom_default_namespace(:test) { example.call } + end + + it "should generate resource paths" do + expect(resource.route_collection_path).to eq "/test/categories" + end + + it "should generate a log out path" do + expect(destroy_admin_user_session_path).to eq "/test/logout" + end + + it "should generate a log in path" do + expect(new_admin_user_session_path).to eq "/test/login" + end + end + + describe "with a namespace with underscores in the name" do + around do |example| + with_custom_default_namespace(:abc_123) { example.call } + end + + it "should generate resource paths" do + expect(resource.route_collection_path).to eq "/abc_123/categories" + end + + it "should generate a log out path" do + expect(destroy_admin_user_session_path).to eq "/abc_123/logout" + end + + it "should generate a log in path" do + expect(new_admin_user_session_path).to eq "/abc_123/login" + end + end + + private + + def with_custom_default_namespace(namespace) + application = ActiveAdmin::Application.new + application.default_namespace = namespace + + with_temp_application(application) { yield } + end + + def with_temp_application(application) + original_application = ActiveAdmin.application + ActiveAdmin.application = application + + load_resources { ActiveAdmin.register(Category) } + + yield + + ensure + ActiveAdmin.application = original_application + end +end diff --git a/spec/requests/memory_spec.rb b/spec/requests/memory_spec.rb new file mode 100644 index 00000000000..08c1fb5b907 --- /dev/null +++ b/spec/requests/memory_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe "Memory Leak", type: :request, if: RUBY_ENGINE == "ruby" do + around do |example| + with_resources_during(example) { ActiveAdmin.register(Category) } + end + + def count_instances_of(klass) + ObjectSpace.each_object(klass) {} + end + + [ActiveAdmin::Namespace, ActiveAdmin::Resource].each do |klass| + it "should not leak #{klass}" do + previously_disabled = GC.enable + GC.start + count = count_instances_of(klass) + + load_resources { ActiveAdmin.register(Category) } + + GC.start + GC.disable if previously_disabled + expect(count_instances_of klass).to be <= count + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a021f0dea3a..2c87b626a8b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,175 +1,18 @@ -require 'spec_helper_without_rails' +# frozen_string_literal: true +require_relative "support/simplecov_helper" -module ActiveAdminIntegrationSpecHelper - extend self +ActiveAdmin::TestSupport::SimpleCovHelper.start("RSpec") - def load_defaults! - ActiveAdmin.unload! - ActiveAdmin.load! - ActiveAdmin.register(Category) - ActiveAdmin.register(User) - ActiveAdmin.register(Post){ belongs_to :user, :optional => true } - reload_menus! - end - - def reload_menus! - ActiveAdmin.application.namespaces.values.each{|n| n.reset_menu! } - end - - # Sometimes we need to reload the routes within - # the application to test them out - def reload_routes! - Rails.application.reload_routes! - end - - # Helper method to load resources and ensure that Active Admin is - # setup with the new configurations. - # - # Eg: - # load_resources do - # ActiveAdmin.regiser(Post) - # end - # - def load_resources - ActiveAdmin.unload! - yield - reload_menus! - reload_routes! - end - - # Sets up a describe block where you can render controller - # actions. Uses the Admin::PostsController as the subject - # for the describe block - def describe_with_render(*args, &block) - describe *args do - include RSpec::Rails::ControllerExampleGroup - render_views - # metadata[:behaviour][:describes] = ActiveAdmin.namespaces[:admin].resources['Post'].controller - module_eval &block - end - end - - def arbre(assigns = {}, helpers = mock_action_view, &block) - Arbre::Context.new(assigns, helpers, &block) - end - - def render_arbre_component(assigns = {}, helpers = mock_action_view, &block) - arbre(assigns, helpers, &block).children.first - end - - # Setup a describe block which uses capybara and rails integration - # test methods. - def describe_with_capybara(*args, &block) - describe *args do - include RSpec::Rails::IntegrationExampleGroup - module_eval &block - end - end - - # Returns a fake action view instance to use with our renderers - def mock_action_view(assigns = {}) - controller = ActionView::TestCase::TestController.new - ActionView::Base.send :include, ActionView::Helpers - ActionView::Base.send :include, ActiveAdmin::ViewHelpers - ActionView::Base.send :include, Rails.application.routes.url_helpers - ActionView::Base.new(ActionController::Base.view_paths, assigns, controller) - end - alias_method :action_view, :mock_action_view - - # A mock resource to register - class MockResource - end - -end - -ENV['RAILS_ENV'] = 'test' -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']) - system 'rake setup' -end - -# Ensure the Active Admin load path is happy -require 'rails' -require 'active_admin' -ActiveAdmin.application.load_paths = [ENV['RAILS_ROOT'] + "/app/admin"] - -require ENV['RAILS_ROOT'] + '/config/environment' - -require 'rspec/rails' - -# Setup Some Admin stuff for us to play with -include ActiveAdminIntegrationSpecHelper -load_defaults! -reload_routes! - -# 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 - -# Don't add asset cache timestamps. Makes it easy to integration -# test for the presence of an asset file -ENV["RAILS_ASSET_ID"] = '' +require_relative "support/matchers/perform_database_query_matcher" +require_relative "support/shared_contexts/capture_stderr" +require_relative "support/active_support_deprecation" RSpec.configure do |config| - config.use_transactional_fixtures = true - config.use_instantiated_fixtures = false - config.include Devise::TestHelpers, :type => :controller - config.render_views = false -end - -# All RSpec configuration needs to happen before any examples -# or else it whines. -require 'integration_example_group' -RSpec.configure do |c| - c.include RSpec::Rails::IntegrationExampleGroup, :example_group => { :file_path => /\bspec\/integration\// } - c.include Devise::TestHelpers, :type => :controller -end - -# Ensure this is defined for Ruby 1.8 -module MiniTest; class Assertion < Exception; end; end - -RSpec::Matchers.define :have_tag do |*args| - - match_unless_raises Test::Unit::AssertionFailedError do |response| - tag = args.shift - content = args.first.is_a?(Hash) ? nil : args.shift - - options = { - :tag => tag.to_s - }.merge(args[0] || {}) - - options[:content] = content if content - - begin - begin - assert_tag(options) - rescue NoMethodError - # We are not in a controller, so let's do the checking ourselves - doc = HTML::Document.new(response, false, false) - tag = doc.find(options) - assert tag, "expected tag, but no tag found matching #{options.inspect} in:\n#{response.inspect}" - end - # In Ruby 1.9, MiniTest::Assertion get's raised, so we'll - # handle raising a Test::Unit::AssertionFailedError - rescue MiniTest::Assertion => e - raise Test::Unit::AssertionFailedError, e.message - end - end -end - -# improve the performance of the specs suite by not logging anything -# see http://blog.plataformatec.com.br/2011/12/three-tips-to-improve-the-performance-of-your-test-suite/ -Rails.logger.level = 4 - - -# Improves performance by forcing the garbage collector to run less often. -unless ENV['DEFER_GC'] == '0' || ENV['DEFER_GC'] == 'false' - require 'support/deferred_garbage_collection' - RSpec.configure do |config| - config.before(:all) { DeferredGarbageCollection.start } - config.after(:all) { DeferredGarbageCollection.reconsider } - end + config.disable_monkey_patching! + config.filter_run focus: true + config.filter_run_excluding changes_filesystem: true + config.run_all_when_everything_filtered = true + config.color = true + config.order = :random + config.example_status_persistence_file_path = ".rspec_failures" end diff --git a/spec/spec_helper_without_rails.rb b/spec/spec_helper_without_rails.rb deleted file mode 100644 index c3623db3746..00000000000 --- a/spec/spec_helper_without_rails.rb +++ /dev/null @@ -1,11 +0,0 @@ -$LOAD_PATH.unshift(File.dirname(__FILE__)) -$LOAD_PATH << File.expand_path('../support', __FILE__) - -ENV['BUNDLE_GEMFILE'] = File.expand_path('../../Gemfile', __FILE__) -require "bundler" -Bundler.setup - -require 'detect_rails_version' -ENV['RAILS'] = detect_rails_version - -require 'coveralls' and Coveralls.wear_merged! if ENV['TRAVIS'] diff --git a/spec/support/active_admin_integration_spec_helper.rb b/spec/support/active_admin_integration_spec_helper.rb new file mode 100644 index 00000000000..b10c763c549 --- /dev/null +++ b/spec/support/active_admin_integration_spec_helper.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true +module ActiveAdminIntegrationSpecHelper + def with_resources_during(example) + load_resources { yield } + + example.run + + load_resources {} + end + + def reload_menus! + ActiveAdmin.application.namespaces.each { |n| n.reset_menu! } + end + + # Sometimes we need to reload the routes within + # the application to test them out + def reload_routes! + Rails.application.reload_routes! + end + + # Helper method to load resources and ensure that Active Admin is + # setup with the new configurations. + # + # Eg: + # load_resources do + # ActiveAdmin.register(Post) + # end + # + def load_resources + ActiveAdmin.unload! + yield + reload_menus! + reload_routes! + end + + def arbre(assigns = {}, helpers = mock_action_view, &block) + Arbre::Context.new(assigns, helpers, &block) + end + + def render_arbre_component(assigns = {}, helpers = mock_action_view, &block) + arbre(assigns, helpers, &block).children.first + end + + # A mock action view to test view helpers + class MockActionView < ::ActionView::Base + include ActiveAdmin::LayoutHelper + include ActiveAdmin::AutoLinkHelper + include ActiveAdmin::DisplayHelper + include ActiveAdmin::IndexHelper + include MethodOrProcHelper + include Rails.application.routes.url_helpers + + def compiled_method_container + self.class + end + end + + # Returns a fake action view instance to use with our renderers + def mock_action_view(base = MockActionView) + controller = ActionView::TestCase::TestController.new + + base.new(view_paths, {}, controller) + end + + # Instantiates a fake decorated controller ready to unit test for a specific action + def controller_with_decorator(action, decorator_class) + method = action == "index" ? :apply_collection_decorator : :apply_decorator + + controller_class = Class.new do + include ActiveAdmin::ResourceController::Decorators + + public method + end + + active_admin_config = double(decorator_class: decorator_class) + + if action != "index" + form_presenter = double(options: { decorate: !decorator_class.nil? }) + + allow(active_admin_config).to receive(:get_page_presenter).with(:form).and_return(form_presenter) + end + + controller = controller_class.new + + allow(controller).to receive(:active_admin_config).and_return(active_admin_config) + allow(controller).to receive(:action_name).and_return(action) + + controller + end + + def view_paths + paths = ActionController::Base.view_paths + ActionView::LookupContext.new(paths) + end + + def with_translation(scope, value) + previous_value = nil + + previous_scope = scope.each_with_object([]) do |part, subscope| + subscope << part + previous_value = I18n.t(subscope.join("."), default: nil) + break subscope if previous_value.nil? + end + + I18n.backend.store_translations I18n.locale, to_translation_hash(scope, value) + yield + ensure + I18n.backend.store_translations I18n.locale, to_translation_hash(previous_scope, previous_value) + end + + def to_translation_hash(scope, value) + scope.reverse.inject(value) { |assigned_value, key| { key => assigned_value } } + end +end diff --git a/spec/support/active_support_deprecation.rb b/spec/support/active_support_deprecation.rb new file mode 100644 index 00000000000..5baa7e0503a --- /dev/null +++ b/spec/support/active_support_deprecation.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# Module to help with deprecation warnings in specs. +module ActiveAdmin + # A good name for this module would be ActiveAdmin::ActiveSupport::Deprecation, but + # that would require a lot of changes in the codebase because, for example, there are references to + # ActiveSupport::Notification in ActiveAdmin module without the :: prefix. + # So, we are using ActiveAdmin::DeprecationHelper instead. + module DeprecationHelper + def self.behavior=(value) + Rails.application.deprecators.behavior = :raise + end + end +end diff --git a/spec/support/deferred_garbage_collection.rb b/spec/support/deferred_garbage_collection.rb deleted file mode 100644 index 26eddb89a00..00000000000 --- a/spec/support/deferred_garbage_collection.rb +++ /dev/null @@ -1,19 +0,0 @@ -class DeferredGarbageCollection - - DEFERRED_GC_THRESHOLD = (ENV['DEFER_GC'] || 15.0).to_f - - @@last_gc_run = Time.now - - def self.start - GC.disable - end - - def self.reconsider - if Time.now - @@last_gc_run >= DEFERRED_GC_THRESHOLD - GC.enable - GC.start - GC.disable - @@last_gc_run = Time.now - end - end -end diff --git a/spec/support/detect_rails_version.rb b/spec/support/detect_rails_version.rb deleted file mode 100644 index a4ba1f130b7..00000000000 --- a/spec/support/detect_rails_version.rb +++ /dev/null @@ -1,42 +0,0 @@ -# Detects the current version of Rails that is being used -# -# -unless defined? RAILS_VERSION_FILE - RAILS_VERSION_FILE = File.expand_path("../../../.rails-version", __FILE__) -end - -unless defined? TRAVIS_CONFIG - require 'yaml' - filename = File.expand_path("../../../.travis.yml", __FILE__) - TRAVIS_CONFIG = YAML.load_file filename - TRAVIS_RAILS_VERSIONS = TRAVIS_CONFIG['env'].grep(/RAILS=(.*)/){ $1 }.sort -end - -unless defined? DEFAULT_RAILS_VERSION - DEFAULT_RAILS_VERSION = TRAVIS_RAILS_VERSIONS.last -end - -def detect_rails_version - version = version_from_file || version_from_env || DEFAULT_RAILS_VERSION - - puts "Detected Rails: #{version}" if ENV['DEBUG'] - - version -end - -def version_from_file - if File.exists?(RAILS_VERSION_FILE) - version = File.read(RAILS_VERSION_FILE).chomp.strip - version = nil if version == "" - - version - end -end - -def version_from_env - ENV['RAILS'] -end - -def write_rails_version(version) - File.open(RAILS_VERSION_FILE, "w+"){|f| f << version } -end diff --git a/spec/support/integration_example_group.rb b/spec/support/integration_example_group.rb deleted file mode 100644 index 3096ebde2b8..00000000000 --- a/spec/support/integration_example_group.rb +++ /dev/null @@ -1,31 +0,0 @@ -require 'action_dispatch' -require 'capybara/rails' -require 'capybara/dsl' - -module RSpec - module Rails - module IntegrationExampleGroup - extend ActiveSupport::Concern - - include ActionDispatch::Integration::Runner - include RSpec::Rails::TestUnitAssertionAdapter - include ActionDispatch::Assertions - include Capybara::DSL - include RSpec::Matchers - - def app - ::Rails.application - end - - def last_response - page - end - - included do - before do - @router = ::Rails.application.routes - end - end - end - end -end diff --git a/spec/support/jslint.yml b/spec/support/jslint.yml deleted file mode 100644 index ffe3a51269e..00000000000 --- a/spec/support/jslint.yml +++ /dev/null @@ -1,80 +0,0 @@ -# ------------ rake task options ------------ - -# JS files to check by default, if no parameters are passed to rake jslint -# (you may want to limit this only to your own scripts and exclude any external scripts and frameworks) - -# this can be overridden by adding 'paths' and 'exclude_paths' parameter to rake command: -# rake jslint paths=path1,path2,... exclude_paths=library1,library2,... - -paths: - - app/assets/javascripts/active_admin/**/*.js - -exclude_paths: - - app/assets/javascripts/active_admin/vendor.js - -# ------------ jslint options ------------ -# see http://www.jslint.com/lint.html#options for more detailed explanations - -# "enforce" type options (true means potentially more warnings) - -adsafe: false # true if ADsafe rules should be enforced. See http://www.ADsafe.org/ -bitwise: true # true if bitwise operators should not be allowed -newcap: true # true if Initial Caps must be used with constructor functions -eqeqeq: false # true if === should be required (for ALL equality comparisons) -immed: false # true if immediate function invocations must be wrapped in parens -nomen: false # true if initial or trailing underscore in identifiers should be forbidden -onevar: false # true if only one var statement per function should be allowed -plusplus: false # true if ++ and -- should not be allowed -regexp: false # true if . and [^...] should not be allowed in RegExp literals -safe: false # true if the safe subset rules are enforced (used by ADsafe) -strict: false # true if the ES5 "use strict"; pragma is required -undef: false # true if variables must be declared before used -white: false # true if strict whitespace rules apply (see also 'indent' option) - -# "allow" type options (false means potentially more warnings) - -cap: false # true if upper case HTML should be allowed -css: true # true if CSS workarounds should be tolerated -debug: false # true if debugger statements should be allowed (set to false before going into production) -es5: false # true if ECMAScript 5 syntax should be allowed -evil: false # true if eval should be allowed -forin: true # true if unfiltered 'for in' statements should be allowed -fragment: true # true if HTML fragments should be allowed -laxbreak: false # true if statement breaks should not be checked -on: false # true if HTML event handlers (e.g. onclick="...") should be allowed -sub: false # true if subscript notation may be used for expressions better expressed in dot notation - -# other options - -maxlen: 300 # Maximum line length -indent: 2 # Number of spaces that should be used for indentation - used only if 'white' option is set -maxerr: 50 # The maximum number of warnings reported (per file) -passfail: false # true if the scan should stop on first error (per file) -# following are relevant only if undef = true -predef: '' # Names of predefined global variables - comma-separated string or a YAML array -browser: true # true if the standard browser globals should be predefined -rhino: false # true if the Rhino environment globals should be predefined -windows: false # true if Windows-specific globals should be predefined -widget: false # true if the Yahoo Widgets globals should be predefined -devel: true # true if functions like alert, confirm, console, prompt etc. are predefined - - -# ------------ jslint_on_rails custom lint options (switch to true to disable some annoying warnings) ------------ - -# ignores "missing semicolon" warning at the end of a function; this lets you write one-liners -# like: x.map(function(i) { return i + 1 }); without having to put a second semicolon inside the function -lastsemic: false - -# allows you to use the 'new' expression as a statement (without assignment) -# so you can call e.g. new Ajax.Request(...), new Effect.Highlight(...) without assigning to a dummy variable -newstat: false - -# ignores the "Expected an assignment or function call and instead saw an expression" warning, -# if the expression contains a proper statement and makes sense; this lets you write things like: -# element && element.show(); -# valid || other || lastChance || alert('OMG!'); -# selected ? show() : hide(); -# although these will still cause a warning: -# element && link; -# selected ? 5 : 10; -statinexp: false diff --git a/spec/support/matchers/perform_database_query_matcher.rb b/spec/support/matchers/perform_database_query_matcher.rb new file mode 100644 index 00000000000..1746cf32d35 --- /dev/null +++ b/spec/support/matchers/perform_database_query_matcher.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +RSpec::Matchers.define :perform_database_query do |query| + match do |block| + query_regexp = query.is_a?(Regexp) ? query : Regexp.new(Regexp.escape(query)) + + @match = nil + + callback = lambda do |_name, _started, _finished, _unique_id, payload| + if query_regexp.match?(payload[:sql]) + @match = true + end + end + + ActiveSupport::Notifications.subscribed(callback, "sql.active_record", &block) + + @match + end + + failure_message do |_| + "Expected queries like \"#{query}\" but none were made" + end + + failure_message_when_negated do |_| + "Expected no queries like \"#{query}\" but at least one were made" + end + + supports_block_expectations +end diff --git a/spec/support/rails_template.rb b/spec/support/rails_template.rb index 871e468a7c4..d2a7e406666 100644 --- a/spec/support/rails_template.rb +++ b/spec/support/rails_template.rb @@ -1,108 +1,133 @@ +# frozen_string_literal: true # Rails template to build the sample app for specs -# Create a cucumber database and environment -copy_file File.expand_path('../templates/cucumber.rb', __FILE__), "config/environments/cucumber.rb" -copy_file File.expand_path('../templates/cucumber_with_reloading.rb', __FILE__), "config/environments/cucumber_with_reloading.rb" - -gsub_file 'config/database.yml', /^test:.*\n/, "test: &test\n" -gsub_file 'config/database.yml', /\z/, "\ncucumber:\n <<: *test\n database: db/cucumber.sqlite3" -gsub_file 'config/database.yml', /\z/, "\ncucumber_with_reloading:\n <<: *test\n database: db/cucumber.sqlite3" - -generate :model, "post title:string body:text published_at:datetime author_id:integer category_id:integer starred:boolean" -inject_into_file 'app/models/post.rb', %q{ - belongs_to :category - belongs_to :author, :class_name => 'User' - accepts_nested_attributes_for :author - attr_accessible :author if Rails::VERSION::STRING >= '3.2' -}, :after => 'class Post < ActiveRecord::Base' -copy_file File.expand_path('../templates/post_decorator.rb', __FILE__), "app/models/post_decorator.rb" - -generate :model, "user type:string first_name:string last_name:string username:string age:integer" -inject_into_file 'app/models/user.rb', %q{ - has_many :posts, :foreign_key => 'author_id' - def display_name - "#{first_name} #{last_name}" - end -}, :after => 'class User < ActiveRecord::Base' - -generate :model, 'publisher --migration=false --parent=User' -generate :model, 'category name:string description:text' -inject_into_file 'app/models/category.rb', %q{ - has_many :posts - accepts_nested_attributes_for :posts -}, :after => 'class Category < ActiveRecord::Base' -generate :model, 'store name:string' - -# Generate a model with string ids -generate :model, "tag name:string" -gsub_file(Dir['db/migrate/*_create_tags.rb'][0], /\:tags\sdo\s.*/, ":tags, :id => false, :primary_key => :id do |t|\n\t\t\tt.string :id\n") -inject_into_file 'app/models/tag.rb', %q{ - self.primary_key = :id - before_create :set_id - - private - def set_id - self.id = 8.times.inject("") { |s,e| s << (i = Kernel.rand(62); i += ((i < 10) ? 48 : ((i < 36) ? 55 : 61 ))).chr } - end -}, :after => 'class Tag < ActiveRecord::Base' +gem "cssbundling-rails" -if Rails::VERSION::MAJOR == 3 && Rails::VERSION::MINOR == 1 #Rails 3.1 Gotcha - gsub_file 'app/models/tag.rb', /self\.primary_key.*$/, "define_attr_method :primary_key, :id" -end +# Manifest already exists in Rails 7.2 +create_file "app/assets/config/manifest.js", skip: true -# 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" +rails_command "css:install:tailwind" +# Remove default configuration generated: https://github.com/rails/cssbundling-rails/blob/v1.4.2/lib/install/tailwind/install.rb#L7 +remove_file "app/assets/stylesheets/application.tailwind.css" -# 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__)" +rails_command "importmap:install" -# Add some translations -append_file "config/locales/en.yml", File.read(File.expand_path('../templates/en.yml', __FILE__)) +initial_timestamp = Time.now.strftime("%Y%m%d%H%M%S").to_i -# Add predefined admin resources -directory File.expand_path('../templates/admin', __FILE__), "app/admin" +template File.expand_path("templates/migrations/create_posts.tt", __dir__), "db/migrate/#{initial_timestamp}_create_posts.rb" -run "rm Gemfile" -run "rm -r test" -run "rm -r spec" +copy_file File.expand_path("templates/models/post.rb", __dir__), "app/models/post.rb" +copy_file File.expand_path("templates/post_decorator.rb", __dir__), "app/models/post_decorator.rb" +copy_file File.expand_path("templates/post_poro_decorator.rb", __dir__), "app/models/post_poro_decorator.rb" -$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) +template File.expand_path("templates/migrations/create_blog_posts.tt", __dir__), "db/migrate/#{initial_timestamp + 1}_create_blog_posts.rb" -generate :'active_admin:install' +copy_file File.expand_path("templates/models/blog/post.rb", __dir__), "app/models/blog/post.rb" -rake "db:migrate" -rake "db:test:prepare" -run "/usr/bin/env RAILS_ENV=cucumber rake db:migrate" +template File.expand_path("templates/migrations/create_profiles.tt", __dir__), "db/migrate/#{initial_timestamp + 2}_create_profiles.rb" -# Setup parallel_tests -def setup_parallel_tests_database(after, force_insert_same_content = false) - inject_into_file 'config/database.yml', "<%= ENV['TEST_ENV_NUMBER'] %>", :after => after, :force => force_insert_same_content -end +copy_file File.expand_path("templates/models/user.rb", __dir__), "app/models/user.rb" + +template File.expand_path("templates/migrations/create_users.tt", __dir__), "db/migrate/#{initial_timestamp + 3}_create_users.rb" + +copy_file File.expand_path("templates/models/profile.rb", __dir__), "app/models/profile.rb" + +copy_file File.expand_path("templates/models/publisher.rb", __dir__), "app/models/publisher.rb" + +template File.expand_path("templates/migrations/create_categories.tt", __dir__), "db/migrate/#{initial_timestamp + 4}_create_categories.rb" + +copy_file File.expand_path("templates/models/category.rb", __dir__), "app/models/category.rb" + +copy_file File.expand_path("templates/models/store.rb", __dir__), "app/models/store.rb" +template File.expand_path("templates/migrations/create_stores.tt", __dir__), "db/migrate/#{initial_timestamp + 5}_create_stores.rb" + +template File.expand_path("templates/migrations/create_tags.tt", __dir__), "db/migrate/#{initial_timestamp + 6}_create_tags.rb" + +copy_file File.expand_path("templates/models/tag.rb", __dir__), "app/models/tag.rb" + +template File.expand_path("templates/migrations/create_taggings.tt", __dir__), "db/migrate/#{initial_timestamp + 7}_create_taggings.rb" + +copy_file File.expand_path("templates/models/tagging.rb", __dir__), "app/models/tagging.rb" + +copy_file File.expand_path("templates/models/api_post.rb", __dir__), "app/models/api_post.rb" -setup_parallel_tests_database "test.sqlite3" -setup_parallel_tests_database "cucumber.sqlite3", true - -# Note: this is hack! -# Somehow, calling parallel_tests tasks from Rails generator using Thor does not work ... -# RAILS_ENV variable never makes it to parallel_tests tasks. -# We need to call these tasks in the after set up hook in order to creates cucumber DBs + run migrations on test & cucumber DBs -create_file 'lib/tasks/parallel.rake', %q{ -namespace :parallel do - def run_in_parallel(cmd, options) - count = "-n #{options[:count]}" if options[:count] - executable = 'parallel_test' - command = "#{executable} --exec '#{cmd}' #{count} #{'--non-parallel' if options[:non_parallel]}" - abort unless system(command) +copy_file File.expand_path("templates/helpers/time_helper.rb", __dir__), "app/helpers/time_helper.rb" + +copy_file File.expand_path("templates/models/company.rb", __dir__), "app/models/company.rb" +template File.expand_path("templates/migrations/create_companies.tt", __dir__), "db/migrate/#{initial_timestamp + 8}_create_companies.rb" +template File.expand_path("templates/migrations/create_join_table_companies_stores.tt", __dir__), "db/migrate/#{initial_timestamp + 9}_create_join_table_companies_stores.rb" + +inject_into_file "app/models/application_record.rb", before: "end" do + <<-RUBY + + def self.ransackable_attributes(auth_object=nil) + authorizable_ransackable_attributes end - desc "create cucumber databases via db:create --> parallel:create_cucumber_db[num_cpus]" - task :create_cucumber_db, :count do |t, args| - run_in_parallel("rake db:create RAILS_ENV=cucumber", args) + def self.ransackable_associations(auth_object=nil) + authorizable_ransackable_associations end + RUBY +end + +environment 'config.hosts << ".ngrok-free.app"', env: :development + +# Make sure we can turn on class reloading in feature specs. +gsub_file "config/environments/test.rb", /config.enable_reloading = false/, "config.enable_reloading = !!ENV['CLASS_RELOADING']" + +environment "config.active_record.maintain_test_schema = false", env: :test + +gsub_file "config/boot.rb", /^.*BUNDLE_GEMFILE.*$/, <<-RUBY + ENV['BUNDLE_GEMFILE'] = "#{File.expand_path(ENV['BUNDLE_GEMFILE'])}" +RUBY - desc "load dumped schema for cucumber databases" - task :load_schema_cucumber_db, :count do |t,args| - run_in_parallel("rake db:schema:load RAILS_ENV=cucumber", args) +# Setup Active Admin +generate "active_admin:install" + +gsub_file "tailwind-active_admin.config.js", /^.*const activeAdminPath.*$/, <<~JS + const activeAdminPath = '../../../'; +JS +gsub_file "tailwind-active_admin.config.js", Regexp.new("@activeadmin/activeadmin/plugin"), "../../../plugin" + +# Force strong parameters to raise exceptions +environment "config.action_controller.action_on_unpermitted_parameters = :raise" + +environment "config.active_record.permanent_connection_checkout = :disallowed" + +inject_into_file "package.json", after: '"private": "true",' do + "\n \"type\": \"module\",\n" +end + +# Add some translations +append_file "config/locales/en.yml", File.read(File.expand_path("templates/en.yml", __dir__)) + +# Add predefined admin resources, override any file that was generated by rails new generator +directory File.expand_path("templates/admin", __dir__), "app/admin" +directory File.expand_path("templates/views", __dir__), "app/views" +directory File.expand_path("templates/policies", __dir__), "app/policies" +directory File.expand_path("templates/public", __dir__), "public", force: true + +route "root to: redirect('admin')" if ENV["RAILS_ENV"] != "test" + +# Rails doesn't write test.sqlite3 files if we run `db:migrate:reset` or `db:drop db:create db:migrate` in a single command. +# That's why we run it in two steps. +rails_command "db:drop db:create", env: ENV["RAILS_ENV"] +rails_command "db:migrate", env: ENV["RAILS_ENV"] + +if ENV["RAILS_ENV"] == "test" + inject_into_file "config/database.yml", "<%= ENV['TEST_ENV_NUMBER'] %>", after: "test.sqlite3" + + require "parallel_tests" + ParallelTests.determine_number_of_processes(nil).times do |n| + copy_file File.expand_path("storage/test.sqlite3", destination_root), "storage/test.sqlite3#{n + 1}" + + # Copy Write-Ahead-Log (-wal) and Wal-Index (-shm) files. + # Files were introduced by rails 7.1 sqlite3 optimizations (https://github.com/rails/rails/pull/49349/files). + %w(shm wal).each do |suffix| + file = File.expand_path("storage/test.sqlite3-#{suffix}", destination_root) + if File.exist?(file) + copy_file File.expand_path("storage/test.sqlite3-#{suffix}", destination_root), "storage/test.sqlite3#{n + 1}-#{suffix}", mode: :preserve + end + end end end -} diff --git a/spec/support/rails_template_with_data.rb b/spec/support/rails_template_with_data.rb index abf8ae58cb4..1923503af6f 100644 --- a/spec/support/rails_template_with_data.rb +++ b/spec/support/rails_template_with_data.rb @@ -1,59 +1,94 @@ -# Use the default -apply File.expand_path("../rails_template.rb", __FILE__) +# frozen_string_literal: true +apply File.expand_path("rails_template.rb", __dir__) -# Register Active Admin controllers -%w{ Post User Category }.each do |type| - generate :'active_admin:resource', type -end +inject_into_file "config/initializers/active_admin.rb", <<-RUBY, after: "ActiveAdmin.setup do |config|" -scopes = <<-EOF - scope :all, :default => true + config.comments_menu = { parent: 'Administrative' } +RUBY - scope :drafts do |posts| - posts.where(["published_at IS NULL"]) - end +inject_into_file "app/admin/admin_users.rb", <<-RUBY, after: "ActiveAdmin.register AdminUser do" - scope :scheduled do |posts| - posts.where(["posts.published_at IS NOT NULL AND posts.published_at > ?", Time.now.utc]) - end + menu parent: "Administrative", priority: 1 +RUBY - scope :published do |posts| - posts.where(["posts.published_at IS NOT NULL AND posts.published_at < ?", Time.now.utc]) - end +directory File.expand_path("templates_with_data/admin", __dir__), "app/admin" - scope :my_posts do |posts| - posts.where(:author_id => current_admin_user.id) - end -EOF -inject_into_file 'app/admin/posts.rb', scopes , :after => "ActiveAdmin.register Post do\n" +append_file "db/seeds.rb", "\n\n" + <<~RUBY + texts = [ + "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + "Sed metus lacus, sagittis et feugiat a, vestibulum non risus.", + "Vestibulum eu eleifend orci, eget ornare velit.", + "Proin rhoncus velit imperdiet sapien iaculis tempor.", + "Morbi a semper justo.", + "Donec at sagittis nunc.", + "Proin vitae accumsan elit, ut tincidunt tellus.", + "Interdum et malesuada fames ac ante ipsum primis in faucibus.", + "Morbi suscipit ex quis est tincidunt ultrices. Integer blandit scelerisque nisi.", + "Aenean lacinia molestie maximus.", + "Mauris blandit sem nec nisl sollicitudin scelerisque.", + "Praesent ac nisi eu dui consectetur aliquet vitae ac ante.", + "Vivamus vel arcu eget lacus luctus tempus." + ] -# Setup some default data -append_file "db/seeds.rb", <<-EOF - users = ["Jimi Hendrix", "Jimmy Page", "Yngwie Malmsteen", "Eric Clapton", "Kirk Hammett"].collect do |name| + user_data = ["Jimi Hendrix", "Jimmy Page", "Yngwie Malmsteen", "Eric Clapton", "Kirk Hammett"].map do |name| first, last = name.split(" ") - User.create! :first_name => first, - :last_name => last, - :username => [first,last].join('-').downcase, - :age => rand(80) + { + first_name: first, + last_name: last, + username: name.downcase.gsub(" ", ""), + age: rand(80), + encrypted_password: SecureRandom.hex + } + end + User.insert_all(user_data) + user_ids = User.pluck(:id) + + category_data = ["Rock", "Pop Rock", "Alt-Country", "Blues", "Dub-Step"].map { |i| { name: i } } + Category.insert_all(category_data) + category_ids = Category.pluck(:id) + + tag_data = ["Amy Winehouse", "Guitar", "Genius Oddities", "Music Culture"].map { |i| { name: i } } + Tag.insert_all(tag_data) + tag_ids = Tag.pluck(:id) + + published_at_values = [5.days.ago, 1.day.ago, nil, 3.days.from_now] + + post_data = Array.new(800) do |i| + user_id = user_ids[i % user_ids.size] + category_id = category_ids[i % category_ids.size] + published = published_at_values[i % published_at_values.size] + { + title: "Blog Post \#{i}", + body: texts.shuffle.slice(0, rand(1..texts.size)).join(" "), + custom_category_id: category_id, + published_date: published, + author_id: user_id, + starred: true + } end + Post.insert_all(post_data) + post_ids = Post.pluck(:id) - categories = ["Rock", "Pop Rock", "Alt-Country", "Blues", "Dub-Step"].collect do |name| - Category.create! :name => name + tagging_data = post_ids.select { rand > 0.4 }.map do |id| + { + tag_id: tag_ids.sample, + post_id: id + } end + Tagging.insert_all(tagging_data) - published_at_values = [Time.now.utc - 5.days, Time.now.utc - 1.day, nil, Time.now.utc + 3.days] - - 1_000.times do |i| - user = users[i % users.size] - cat = categories[i % categories.size] - published_at = published_at_values[i % published_at_values.size] - Post.create :title => "Blog Post \#{i}", - :body => "Blog post \#{i} is written by \#{user.username} about \#{cat.name}", - :category => cat, - :published_at => published_at, - :author => user, - :starred => true + admin_user_id = AdminUser.first.id + comment_data = Array.new(800) do |i| + { + namespace: :admin, + author_type: "AdminUser", + author_id: admin_user_id, + body: texts.shuffle.slice(0, rand(1..texts.size)).join(" "), + resource_type: "Category", + resource_id: category_ids.sample + } end -EOF + ActiveAdmin::Comment.insert_all(comment_data) +RUBY -rake 'db:seed' +rails_command "db:seed" diff --git a/spec/support/shared_contexts/capture_stderr.rb b/spec/support/shared_contexts/capture_stderr.rb new file mode 100644 index 00000000000..f8f899bf433 --- /dev/null +++ b/spec/support/shared_contexts/capture_stderr.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +RSpec.shared_context "capture stderr" do + around do |example| + original_stderr = $stderr + $stderr = StringIO.new + example.run + $stderr = original_stderr + end +end diff --git a/spec/support/simplecov_changes_env.rb b/spec/support/simplecov_changes_env.rb new file mode 100644 index 00000000000..813fe55fe6d --- /dev/null +++ b/spec/support/simplecov_changes_env.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true +require_relative "simplecov_helper" + +ActiveAdmin::TestSupport::SimpleCovHelper.start("filesystem changes specs") diff --git a/spec/support/simplecov_helper.rb b/spec/support/simplecov_helper.rb new file mode 100644 index 00000000000..61848b69d8c --- /dev/null +++ b/spec/support/simplecov_helper.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module ActiveAdmin + module TestSupport + module SimpleCovHelper + module_function + + def start(command_name = nil) + return unless ENV["COVERAGE"] == "true" + return if @started + + require "simplecov" + + ::SimpleCov.command_name(command_name) if command_name + ::SimpleCov.start "activeadmin" + + @started = true + end + end + end +end diff --git a/spec/support/simplecov_regular_env.rb b/spec/support/simplecov_regular_env.rb new file mode 100644 index 00000000000..8056a1827f2 --- /dev/null +++ b/spec/support/simplecov_regular_env.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true +require_relative "simplecov_helper" + +ActiveAdmin::TestSupport::SimpleCovHelper.start(["regular specs", ENV["TEST_ENV_NUMBER"]].join(" ").rstrip) diff --git a/spec/support/templates/admin/companies.rb b/spec/support/templates/admin/companies.rb new file mode 100644 index 00000000000..5094c380ef2 --- /dev/null +++ b/spec/support/templates/admin/companies.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +ActiveAdmin.register Company do + permit_params :name, store_ids: [] + + form do |f| + f.inputs 'Company' do + f.input :name + f.input :stores + end + f.actions + end + + show do + attributes_table :name, :stores, :created_at, :update_at + end +end diff --git a/spec/support/templates/admin/profiles.rb b/spec/support/templates/admin/profiles.rb new file mode 100644 index 00000000000..5fead810c55 --- /dev/null +++ b/spec/support/templates/admin/profiles.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true +ActiveAdmin.register Profile do + menu parent: 'Users' + permit_params :user_id, :bio +end diff --git a/spec/support/templates/admin/stores.rb b/spec/support/templates/admin/stores.rb index 2078d0015c1..50820770d1c 100644 --- a/spec/support/templates/admin/stores.rb +++ b/spec/support/templates/admin/stores.rb @@ -1 +1,6 @@ -ActiveAdmin.register Store +# frozen_string_literal: true +ActiveAdmin.register Store do + permit_params :name + + index pagination_total: false +end diff --git a/spec/support/templates/cucumber.rb b/spec/support/templates/cucumber.rb deleted file mode 100644 index 183fa0f8cc9..00000000000 --- a/spec/support/templates/cucumber.rb +++ /dev/null @@ -1,24 +0,0 @@ -require File.expand_path('config/environments/test', Rails.root) - -# rails/railties/lib/rails/test_help.rb aborts if the environment is not 'test'. (Rails 3.0.0.beta3) -# We can't run Cucumber/RSpec/Test_Unit tests in different environments then. -# -# For now, I patch StringInquirer so that Rails.env.test? returns true when Rails.env is 'test' or 'cucumber' -# -# https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/4458-rails-should-allow-test-to-run-in-cucumber-environment -module ActiveSupport - class StringInquirer < String - def method_missing(method_name, *arguments) - if method_name.to_s[-1,1] == "?" - test_string = method_name.to_s[0..-2] - if test_string == 'test' - self == 'test' or self == 'cucumber' - else - self == test_string - end - else - super - end - end - end -end diff --git a/spec/support/templates/cucumber_with_reloading.rb b/spec/support/templates/cucumber_with_reloading.rb deleted file mode 100644 index e9b2592901a..00000000000 --- a/spec/support/templates/cucumber_with_reloading.rb +++ /dev/null @@ -1,5 +0,0 @@ -require File.expand_path('config/environments/cucumber', Rails.root) - -Rails.application.class.configure do - config.cache_classes = false -end diff --git a/spec/support/templates/en.yml b/spec/support/templates/en.yml index f3edd4e5403..a917fd27f4f 100644 --- a/spec/support/templates/en.yml +++ b/spec/support/templates/en.yml @@ -1,8 +1,9 @@ # Sample translations used to test ActiveAdmin's I18n integration. +en: activerecord: models: store: one: Bookstore other: Bookstores active_admin: - download: "Download this:" + download: "Export:" diff --git a/spec/support/templates/helpers/time_helper.rb b/spec/support/templates/helpers/time_helper.rb new file mode 100644 index 00000000000..1f8a68b2c00 --- /dev/null +++ b/spec/support/templates/helpers/time_helper.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true +module TimeHelper + + def format_time(time, format: :long) + time + end + +end diff --git a/spec/support/templates/migrations/create_blog_posts.tt b/spec/support/templates/migrations/create_blog_posts.tt new file mode 100644 index 00000000000..d20bad046d1 --- /dev/null +++ b/spec/support/templates/migrations/create_blog_posts.tt @@ -0,0 +1,16 @@ +class CreateBlogPosts < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>] + def change + create_table :blog_posts do |t| + t.string :title + t.text :body + t.date :published_date + t.integer :author_id + t.integer :position + t.integer :custom_category_id + t.boolean :starred + t.integer :foo_id + t.datetime :created_at + t.datetime :updated_at + end + end +end diff --git a/spec/support/templates/migrations/create_categories.tt b/spec/support/templates/migrations/create_categories.tt new file mode 100644 index 00000000000..a3737e3345b --- /dev/null +++ b/spec/support/templates/migrations/create_categories.tt @@ -0,0 +1,11 @@ +class CreateCategories < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>] + def change + create_table :categories do |t| + t.string :name + t.text :description + t.integer :posts_count, default: 0 + t.datetime :created_at + t.datetime :updated_at + end + end +end diff --git a/spec/support/templates/migrations/create_companies.tt b/spec/support/templates/migrations/create_companies.tt new file mode 100644 index 00000000000..5c19d956c86 --- /dev/null +++ b/spec/support/templates/migrations/create_companies.tt @@ -0,0 +1,9 @@ +class CreateCompanies < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>] + def change + create_table :companies do |t| + t.string :name + t.datetime :created_at + t.datetime :updated_at + end + end +end diff --git a/spec/support/templates/migrations/create_join_table_companies_stores.tt b/spec/support/templates/migrations/create_join_table_companies_stores.tt new file mode 100644 index 00000000000..57b391f68ce --- /dev/null +++ b/spec/support/templates/migrations/create_join_table_companies_stores.tt @@ -0,0 +1,5 @@ +class CreateJoinTableCompaniesStores < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>] + def change + create_join_table :companies, :stores + end +end diff --git a/spec/support/templates/migrations/create_posts.tt b/spec/support/templates/migrations/create_posts.tt new file mode 100644 index 00000000000..b1eef3c6be2 --- /dev/null +++ b/spec/support/templates/migrations/create_posts.tt @@ -0,0 +1,16 @@ +class CreatePosts < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>] + def change + create_table :posts do |t| + t.string :title + t.text :body + t.date :published_date + t.integer :author_id + t.integer :position + t.integer :custom_category_id + t.boolean :starred + t.integer :foo_id + t.datetime :created_at + t.datetime :updated_at + end + end +end diff --git a/spec/support/templates/migrations/create_profiles.tt b/spec/support/templates/migrations/create_profiles.tt new file mode 100644 index 00000000000..a3c62b78506 --- /dev/null +++ b/spec/support/templates/migrations/create_profiles.tt @@ -0,0 +1,10 @@ +class CreateProfiles < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>] + def change + create_table :profiles do |t| + t.integer :user_id + t.text :bio + t.datetime :created_at + t.datetime :updated_at + end + end +end diff --git a/spec/support/templates/migrations/create_stores.tt b/spec/support/templates/migrations/create_stores.tt new file mode 100644 index 00000000000..16b2b512432 --- /dev/null +++ b/spec/support/templates/migrations/create_stores.tt @@ -0,0 +1,10 @@ +class CreateStores < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>] + def change + create_table :stores do |t| + t.string :name + t.integer :user_id + t.datetime :created_at + t.datetime :updated_at + end + end +end diff --git a/spec/support/templates/migrations/create_taggings.tt b/spec/support/templates/migrations/create_taggings.tt new file mode 100644 index 00000000000..fde00250d0f --- /dev/null +++ b/spec/support/templates/migrations/create_taggings.tt @@ -0,0 +1,11 @@ +class CreateTaggings < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>] + def change + create_table :taggings do |t| + t.integer :post_id + t.integer :tag_id + t.integer :position + t.datetime :created_at + t.datetime :updated_at + end + end +end diff --git a/spec/support/templates/migrations/create_tags.tt b/spec/support/templates/migrations/create_tags.tt new file mode 100644 index 00000000000..6516c3a99fa --- /dev/null +++ b/spec/support/templates/migrations/create_tags.tt @@ -0,0 +1,9 @@ +class CreateTags < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>] + def change + create_table :tags do |t| + t.string :name + t.datetime :created_at + t.datetime :updated_at + end + end +end diff --git a/spec/support/templates/migrations/create_users.tt b/spec/support/templates/migrations/create_users.tt new file mode 100644 index 00000000000..b1472214cc3 --- /dev/null +++ b/spec/support/templates/migrations/create_users.tt @@ -0,0 +1,16 @@ +class CreateUsers < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>] + def change + create_table :users do |t| + t.string :type + t.string :first_name + t.string :last_name + t.string :username + t.integer :age + t.string :encrypted_password + t.string :reason_of_sign_in + t.integer :sign_in_count, default: 0 + t.datetime :created_at + t.datetime :updated_at + end + end +end diff --git a/spec/support/templates/models/api_post.rb b/spec/support/templates/models/api_post.rb new file mode 100644 index 00000000000..2b577be145e --- /dev/null +++ b/spec/support/templates/models/api_post.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +# A non-ActiveRecord resource used in features to prove ActiveAdmin's +# filter DSL works for resources that bypass the ActiveRecord DataAccess path. +# `.fetch` returns a page of records plus total_count, like an HTTP API client. +class ApiPost + extend ActiveModel::Naming + + Record = Data.define(:id, :title, :status) + + # Filter widgets probe predicate-suffixed accessors ActiveAdmin computes on the fly + # (e.g. `_eq`/`_in` for bare, non-predicate filter names), so unknown readers return nil + # instead of raising, matching Ransack::Search's own permissive method_missing. + Search = Data.define(:title_cont, :status_eq) do + def method_missing(name, *) = nil + def respond_to_missing?(name, include_private = false) = true + end + + ALL_RECORDS = [ + Record.new(id: 1, title: "Alpha", status: "active"), + Record.new(id: 2, title: "Beta", status: "inactive"), + Record.new(id: 3, title: "Gamma", status: "active"), + Record.new(id: 4, title: "Delta", status: "active"), + Record.new(id: 5, title: "Epsilon", status: "inactive"), + Record.new(id: 6, title: "Zeta", status: "active"), + Record.new(id: 7, title: "Eta", status: "active") + ].freeze + + Result = Data.define(:records, :total_count, :limit, :offset) + + def self.fetch(page: 1, per_page: 5, title_cont: nil, status_eq: nil) + page = [page.to_i, 1].max + per_page = per_page.to_i + + records = ALL_RECORDS + records = records.select { |r| r.title.include?(title_cont) } if title_cont.present? + records = records.select { |r| r.status == status_eq } if status_eq.present? + + offset = (page - 1) * per_page + + Result.new( + records: records[offset, per_page] || [], + total_count: records.size, + limit: per_page, + offset: offset + ) + end +end diff --git a/spec/support/templates/models/blog/post.rb b/spec/support/templates/models/blog/post.rb new file mode 100644 index 00000000000..ec1c0d573d5 --- /dev/null +++ b/spec/support/templates/models/blog/post.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true +class Blog::Post < ApplicationRecord + belongs_to :category, foreign_key: :custom_category_id + belongs_to :author, class_name: "User" + has_many :taggings + accepts_nested_attributes_for :author + accepts_nested_attributes_for :taggings, allow_destroy: true +end diff --git a/spec/support/templates/models/category.rb b/spec/support/templates/models/category.rb new file mode 100644 index 00000000000..ef02bc0ab4a --- /dev/null +++ b/spec/support/templates/models/category.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +class Category < ApplicationRecord + has_many :posts, foreign_key: :custom_category_id + has_many :authors, through: :posts + accepts_nested_attributes_for :posts + validates :name, presence: true +end diff --git a/spec/support/templates/models/company.rb b/spec/support/templates/models/company.rb new file mode 100644 index 00000000000..7b7ec03c715 --- /dev/null +++ b/spec/support/templates/models/company.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +class Company < ApplicationRecord + has_and_belongs_to_many :stores + + validates :name, presence: true +end diff --git a/spec/support/templates/models/post.rb b/spec/support/templates/models/post.rb new file mode 100644 index 00000000000..64f33fc2376 --- /dev/null +++ b/spec/support/templates/models/post.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true +class Post < ApplicationRecord + belongs_to :category, foreign_key: :custom_category_id, optional: true, counter_cache: true + belongs_to :author, class_name: "User", optional: true + has_many :taggings + has_many :tags, through: :taggings + accepts_nested_attributes_for :author + accepts_nested_attributes_for :taggings, allow_destroy: true + + # validates :title, :body, :author, :category, presence: true + + ransacker :custom_title_searcher do |parent| + parent.table[:title] + end + + ransacker :custom_created_at_searcher do |parent| + parent.table[:created_at] + end + + ransacker :custom_searcher_numeric, type: :numeric do + # nothing to see here + end + + class << self + def ransackable_scopes(_auth_object = nil) + super + [:fancy_filter] + end + + def fancy_filter(value) + where(starred: value == "Starred") + end + end +end diff --git a/spec/support/templates/models/profile.rb b/spec/support/templates/models/profile.rb new file mode 100644 index 00000000000..1484fababeb --- /dev/null +++ b/spec/support/templates/models/profile.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true +class Profile < ApplicationRecord + belongs_to :user +end diff --git a/spec/support/templates/models/publisher.rb b/spec/support/templates/models/publisher.rb new file mode 100644 index 00000000000..f0efb0ee867 --- /dev/null +++ b/spec/support/templates/models/publisher.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true +class Publisher < User +end diff --git a/spec/support/templates/models/store.rb b/spec/support/templates/models/store.rb new file mode 100644 index 00000000000..22105e8b99d --- /dev/null +++ b/spec/support/templates/models/store.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true +class Store < ApplicationRecord +end diff --git a/spec/support/templates/models/tag.rb b/spec/support/templates/models/tag.rb new file mode 100644 index 00000000000..f0cf3474685 --- /dev/null +++ b/spec/support/templates/models/tag.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true +class Tag < ApplicationRecord + has_many :taggings + has_many :posts, through: :taggings +end diff --git a/spec/support/templates/models/tagging.rb b/spec/support/templates/models/tagging.rb new file mode 100644 index 00000000000..93a82335c3f --- /dev/null +++ b/spec/support/templates/models/tagging.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true +class Tagging < ApplicationRecord + belongs_to :post, optional: true + belongs_to :tag, optional: true + + delegate :name, to: :tag, prefix: true +end diff --git a/spec/support/templates/models/user.rb b/spec/support/templates/models/user.rb new file mode 100644 index 00000000000..1e0f809c4fc --- /dev/null +++ b/spec/support/templates/models/user.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true +class User < ApplicationRecord + class VIP < self + end + has_many :posts, foreign_key: "author_id" + has_many :articles, class_name: "Post", foreign_key: "author_id" + has_many :highlights, -> { where(starred: true) }, class_name: "Post", foreign_key: "author_id" + has_many :unstarred_posts, -> { where(starred: false) }, class_name: "Post", foreign_key: "author_id" + has_many :qualified_posts, -> { where(starred: false) }, class_name: "::Post", foreign_key: "author_id" + has_one :profile + accepts_nested_attributes_for :profile, allow_destroy: true + accepts_nested_attributes_for :posts, allow_destroy: true + + ransacker :age_in_five_years, type: :numeric, formatter: proc { |v| v.to_i - 5 } do |parent| + parent.table[:age] + end + + def display_name + "#{first_name} #{last_name}" + end +end diff --git a/spec/support/templates/policies/active_admin/comment_policy.rb b/spec/support/templates/policies/active_admin/comment_policy.rb new file mode 100644 index 00000000000..8586202b6b5 --- /dev/null +++ b/spec/support/templates/policies/active_admin/comment_policy.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +module ActiveAdmin + class CommentPolicy < ApplicationPolicy + def destroy? + record.author == user + end + + class Scope < ApplicationPolicy::Scope + def resolve + scope.where(author: user) + end + end + end +end diff --git a/spec/support/templates/policies/active_admin/page_policy.rb b/spec/support/templates/policies/active_admin/page_policy.rb new file mode 100644 index 00000000000..f691f6fac62 --- /dev/null +++ b/spec/support/templates/policies/active_admin/page_policy.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true +module ActiveAdmin + class PagePolicy < ApplicationPolicy + def show? + case record.name + when "Dashboard" + true + else + false + end + end + end +end diff --git a/spec/support/templates/policies/admin_user_policy.rb b/spec/support/templates/policies/admin_user_policy.rb new file mode 100644 index 00000000000..c534681af69 --- /dev/null +++ b/spec/support/templates/policies/admin_user_policy.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true +class AdminUserPolicy < ApplicationPolicy +end diff --git a/spec/support/templates/policies/application_policy.rb b/spec/support/templates/policies/application_policy.rb new file mode 100644 index 00000000000..a95f6fad5fb --- /dev/null +++ b/spec/support/templates/policies/application_policy.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true +class ApplicationPolicy + attr_reader :user, :record + + def initialize(user, record) + @user = user + @record = record + end + + def index? + true + end + + def show? + scope.where(id: record.id).exists? + end + + def new? + create? + end + + def create? + true + end + + def edit? + update? + end + + def update? + true + end + + def destroy? + true + end + + def destroy_all? + true + end + + def scope + Pundit.policy_scope!(user, record.class) + end + + class Scope + attr_reader :user, :scope + + def initialize(user, scope) + @user = user + @scope = scope + end + + def resolve + scope + end + end +end diff --git a/spec/support/templates/policies/category_policy.rb b/spec/support/templates/policies/category_policy.rb new file mode 100644 index 00000000000..3aef6aa9358 --- /dev/null +++ b/spec/support/templates/policies/category_policy.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true +class CategoryPolicy < ApplicationPolicy +end diff --git a/spec/support/templates/policies/company_policy.rb b/spec/support/templates/policies/company_policy.rb new file mode 100644 index 00000000000..385626ffb2f --- /dev/null +++ b/spec/support/templates/policies/company_policy.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true +class CompanyPolicy < ApplicationPolicy +end diff --git a/spec/support/templates/policies/post_policy.rb b/spec/support/templates/policies/post_policy.rb new file mode 100644 index 00000000000..aeb5424de63 --- /dev/null +++ b/spec/support/templates/policies/post_policy.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +class PostPolicy < ApplicationPolicy + def new? + true + end + + def create? + record.category.nil? || record.category.name != "Announcements" || user.is_a?(User::VIP) + end + + def update? + record.author == user + end +end diff --git a/spec/support/templates/policies/profile_policy.rb b/spec/support/templates/policies/profile_policy.rb new file mode 100644 index 00000000000..a2ecd2a46cb --- /dev/null +++ b/spec/support/templates/policies/profile_policy.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true +class ProfilePolicy < ApplicationPolicy +end diff --git a/spec/support/templates/policies/store_policy.rb b/spec/support/templates/policies/store_policy.rb new file mode 100644 index 00000000000..2038c15c661 --- /dev/null +++ b/spec/support/templates/policies/store_policy.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true +class StorePolicy < ApplicationPolicy +end diff --git a/spec/support/templates/policies/tag_policy.rb b/spec/support/templates/policies/tag_policy.rb new file mode 100644 index 00000000000..1193466ff9c --- /dev/null +++ b/spec/support/templates/policies/tag_policy.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true +class TagPolicy < ApplicationPolicy +end diff --git a/spec/support/templates/policies/user_policy.rb b/spec/support/templates/policies/user_policy.rb new file mode 100644 index 00000000000..cb5a7e4b5c9 --- /dev/null +++ b/spec/support/templates/policies/user_policy.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true +class UserPolicy < ApplicationPolicy +end diff --git a/spec/support/templates/post_decorator.rb b/spec/support/templates/post_decorator.rb index 2405696d53b..2548d0a2b9b 100644 --- a/spec/support/templates/post_decorator.rb +++ b/spec/support/templates/post_decorator.rb @@ -1,53 +1,25 @@ -# Minimal example of a decorator -require_dependency 'post' +# frozen_string_literal: true +require "draper" -# DelegateClass(Post) does not delegate attribute methods -# in some environments: -# http://travis-ci.org/#!/gregbell/active_admin/jobs/2021466 -# -class PostDecorator < SimpleDelegator - delegate :id, :to => :__getobj__ +class PostDecorator < Draper::Decorator + decorates :post + delegate_all - def self.decorate(object) - new(object) + # @param attributes [Hash] + def assign_attributes(attributes) + object.assign_attributes attributes.except(:virtual_title) + self.virtual_title = attributes.fetch(:virtual_title) if attributes.key?(:virtual_title) end - def self.decorate_collection(collection) - DecoratedEnumerableProxy.new(collection) + def virtual_title + object.title end - def self.model_name - ActiveModel::Name.new Post + def virtual_title=(virtual_title) + object.title = virtual_title end def decorator_method - 'A method only available on the decorator' - end - - # Minimal example of decorating a collection. - # A full example can be found in the draper project: - # https://github.com/jcasimir/draper/blob/master/lib/draper/decorated_enumerable_proxy.rb - class DecoratedEnumerableProxy < DelegateClass(ActiveRecord::Relation) - include Enumerable - - delegate :as_json, :collect, :map, :each, :[], :all?, :include?, :first, :last, :shift, :to => :decorated_collection - - def klass - PostDecorator - end - - def wrapped_collection - __getobj__ - end - - def decorated_collection - @decorated_collection ||= wrapped_collection.collect { |member| klass.decorate(member) } - end - alias_method :to_ary, :decorated_collection - - def each(&blk) - to_ary.each(&blk) - end + "A method only available on the decorator" end end - diff --git a/spec/support/templates/post_poro_decorator.rb b/spec/support/templates/post_poro_decorator.rb new file mode 100644 index 00000000000..9822943d948 --- /dev/null +++ b/spec/support/templates/post_poro_decorator.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +class PostPoroDecorator + delegate_missing_to :post + + def initialize(post) + @post = post + end + + def decorator_method + "A method only available on the PORO decorator" + end + + private + + attr_reader :post +end diff --git a/spec/support/templates/public/favicon.ico b/spec/support/templates/public/favicon.ico new file mode 100644 index 00000000000..db016de0d55 Binary files /dev/null and b/spec/support/templates/public/favicon.ico differ diff --git a/spec/support/templates/views/admin/posts/_starred_batch_action_form.html.erb b/spec/support/templates/views/admin/posts/_starred_batch_action_form.html.erb new file mode 100644 index 00000000000..2adb88c38b1 --- /dev/null +++ b/spec/support/templates/views/admin/posts/_starred_batch_action_form.html.erb @@ -0,0 +1,33 @@ + + diff --git a/spec/support/templates_with_data/admin/api_posts.rb b/spec/support/templates_with_data/admin/api_posts.rb new file mode 100644 index 00000000000..8b6936240ad --- /dev/null +++ b/spec/support/templates_with_data/admin/api_posts.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true +ActiveAdmin.register ApiPost do + menu label: "PORO Posts" + actions :index + + # Active-filter chips require a Ransack search, unlike the filter form. + config.current_filters = false + + filter :title_cont, as: :string, label: "Title contains" + filter :status_eq, as: :select, label: "Status", collection: %w[active inactive] + + index title: "PORO Posts", download_links: false do + column :id, sortable: false + column :title, sortable: false + column :status, sortable: false + end + + controller do + protected + + # Non-ActiveRecord resources override `find_collection`; Kaminari.paginate_array + # adapts the API response for ActiveAdmin's pagination UI. + def find_collection(*) + q = params[:q] || {} + @search = ApiPost::Search.new( + title_cont: q[:title_cont], + status_eq: q[:status_eq] + ) + page = (params[:page] || 1).to_i + per_page = 5 + + result = ApiPost.fetch( + page: page, + per_page: per_page, + title_cont: @search.title_cont, + status_eq: @search.status_eq + ) + + Kaminari.paginate_array( + result.records, + total_count: result.total_count, + limit: result.limit, + offset: result.offset + ).page(page).per(per_page) + end + end +end diff --git a/spec/support/templates_with_data/admin/categories.rb b/spec/support/templates_with_data/admin/categories.rb new file mode 100644 index 00000000000..f501cca1b5a --- /dev/null +++ b/spec/support/templates_with_data/admin/categories.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +ActiveAdmin.register Category do + config.create_another = true + + permit_params :name, :description +end diff --git a/spec/support/templates_with_data/admin/components/custom_index.rb b/spec/support/templates_with_data/admin/components/custom_index.rb new file mode 100644 index 00000000000..acee208fd10 --- /dev/null +++ b/spec/support/templates_with_data/admin/components/custom_index.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true +module ActiveAdmin + module Views + class CustomIndex < ActiveAdmin::Component + + def build(page_presenter, collection) + add_class("custom-index") + set_attribute("data-index-as", "custom") + if active_admin_config.batch_actions.any? + div class: "p-3" do + resource_selection_toggle_panel + end + end + + div class: "p-3 grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6" do + collection.each do |obj| + instance_exec(obj, &page_presenter.block) + end + end + end + + def self.index_name + "custom" + end + + end + end +end diff --git a/spec/support/templates_with_data/admin/kitchen_sink.rb b/spec/support/templates_with_data/admin/kitchen_sink.rb new file mode 100644 index 00000000000..63f216ab307 --- /dev/null +++ b/spec/support/templates_with_data/admin/kitchen_sink.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true +ActiveAdmin.register_page "KitchenSink" do + content do + panel "About ActiveAdmin" do + para class: "mb-4" do + a "Active Admin", href: "https://github.com/activeadmin/activeadmin" + text_node "is a" + span "Ruby on Rails", class: "text-red-500" + text_node "framework for" + em "creating elegant backends" + text_node "for" + strong "website administration." + end + para do + abbr "HTML", title: "HyperText Markup Language" + text_node "is the basic building block of the Web." + end + end + div class: "grid grid-cols-1 md:grid-cols-2 gap-5" do + div do + h3 "TableFor Example", class: "mb-4 text-base/7 font-semibold text-gray-900 dark:text-white" + div class: "border border-gray-200 dark:border-gray-800 rounded-md shadow-xs overflow-hidden" do + div class: "overflow-x-auto" do + table_for User.first(5) do + column :id + column :display_name, class: "min-w-40" do |user| + auto_link user + end + column :username + column :age + column :created_at, class: "min-w-40" + column :updated_at, class: "min-w-40" + end + end + end + end + div do + h3 "Attributes Table Example", class: "mb-4 text-base/7 font-semibold text-gray-900 dark:text-white" + attributes_table_for(Post.first) do + row :title + row :published_date + row :author + row :category + row :starred + row :position + end + end + end + end + + sidebar "Sample Sidebar" do + div class: "pb-6" do + h3 "Applicant Information", class: "text-base/7 font-semibold text-gray-900 dark:text-white" + para "A sidebar can also be used on a custom page.", class: "mt-1 text-sm/6 text-gray-500 dark:text-gray-400" + end + dl do + div class: "border-t border-gray-100 dark:border-white/10 py-4" do + dt "Full name", class: "text-sm/6 font-medium text-gray-900 dark:text-gray-100" + dd "Margot Foster", class: "mt-1 text-sm/6 text-gray-700 dark:text-gray-400" + end + div class: "border-t border-gray-100 dark:border-white/10 py-4" do + dt "Application for", class: "text-sm/6 font-medium text-gray-900 dark:text-gray-100" + dd "Backend Developer", class: "mt-1 text-sm/6 text-gray-700 dark:text-gray-400" + end + div class: "border-t border-gray-100 dark:border-white/10 py-4" do + dt "Email address", class: "text-sm/6 font-medium text-gray-900 dark:text-gray-100" + dd "margotfoster@example.com", class: "mt-1 text-sm/6 text-gray-700 dark:text-gray-400" + end + div class: "border-t border-gray-100 dark:border-white/10 py-4" do + dt "Attachments", class: "text-sm/6 font-medium text-gray-900 dark:text-gray-100" + dd class: "mt-1 text-sm/6 text-gray-700 dark:text-gray-400" do + ul class: "divide-y divide-gray-100 rounded-md border border-gray-200 dark:divide-white/5 dark:border-white/10", role: "list" do + li class: "flex items-center justify-between py-3 pl-3 pr-4 text-sm/6" do + div class: "flex w-0 flex-1 items-center" do + text_node ''.html_safe + div class: "ms-2 flex min-w-0 flex-1 gap-2" do + span class: "truncate font-medium text-gray-900 dark:text-white" do + "resume_back_end_developer.pdf" + end + span class: "shrink-0 text-gray-400 dark:text-gray-500" do + "2.4mb" + end + end + end + end + li class: "flex items-center justify-between py-3 pl-3 pr-4 text-sm/6" do + div class: "flex w-0 flex-1 items-center" do + text_node ''.html_safe + div class: "ms-2 flex min-w-0 flex-1 gap-2" do + span class: "truncate font-medium text-gray-900 dark:text-white" do + "coverletter_back_end_developer.pdf" + end + span class: "shrink-0 text-gray-400 dark:text-gray-500" do + "4.5mb" + end + end + end + end + end + end + end + end + end +end diff --git a/spec/support/templates_with_data/admin/posts.rb b/spec/support/templates_with_data/admin/posts.rb new file mode 100644 index 00000000000..e63d8bb0930 --- /dev/null +++ b/spec/support/templates_with_data/admin/posts.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true +ActiveAdmin.register Post do + permit_params :custom_category_id, :author_id, :title, :body, :published_date, :position, :starred, taggings_attributes: [ :id, :tag_id, :name, :position, :_destroy ] + + filter :author + filter :category, as: :check_boxes + filter :taggings + filter :tags, as: :check_boxes + filter :title + filter :body + filter :published_date + filter :position + filter :starred + filter :foo_id + filter :created_at + filter :updated_at + filter :custom_title_searcher + filter :custom_created_at_searcher + filter :custom_searcher_numeric + + belongs_to :author, class_name: "User", param: "user_id", route_name: "user" + + config.per_page = [ 5, 10, 20 ] + + includes :author, :category, :taggings + + scope :all, default: true + + scope :drafts, group: :status do |posts| + posts.where(["published_date IS NULL"]) + end + + scope :scheduled, group: :status do |posts| + posts.where(["posts.published_date IS NOT NULL AND posts.published_date > ?", Time.current]) + end + + scope :published, group: :status do |posts| + posts.where(["posts.published_date IS NOT NULL AND posts.published_date < ?", Time.current]) + end + + scope :my_posts, group: :author do |posts| + posts.where(author_id: current_admin_user.id) + end + + batch_action :set_starred, partial: "starred_batch_action_form", link_html_options: { "data-modal-target": "starred-batch-action-modal", "data-modal-show": "starred-batch-action-modal" } do |ids, inputs| + Post.where(id: ids).update_all(starred: inputs["starred"].present?) + redirect_to collection_path(user_id: params["user_id"]), notice: "The posts have been updated." + end + + index do + selectable_column + id_column + column :title, class: "min-w-[150px]" + column :published_date, class: "min-w-[170px]" + column :author + column :category + column :starred + column :position + column :created_at, class: "min-w-[200px]" + column :updated_at, class: "min-w-[200px]" + end + + member_action :toggle_starred, method: :put do + resource.update(starred: !resource.starred) + redirect_to resource_path, notice: "Post updated." + end + + action_item :toggle_starred, only: :show do + link_to "Toggle Starred", toggle_starred_admin_user_post_path(resource.author, resource), method: :put, class: "action-item-button" + end + + show do + attributes_table_for(resource) do + row :id + row :title + row :published_date + row :author + row :body + row :category + row :starred + row :position + row :created_at + row :updated_at + end + + div class: "grid grid-cols-1 md:grid-cols-2 gap-4 my-4" do + div do + panel "Tags" do + table_for(post.taggings.order(:position)) do + column :id do |tagging| + link_to tagging.tag_id, admin_tag_path(tagging.tag) + end + column :tag, &:tag_name + column :position + column :updated_at + end + end + end + div do + panel "Category" do + attributes_table_for post.category do + row :id do |category| + link_to category.id, admin_category_path(category) + end + row :description + end + end + end + end + end + + form do |f| + f.semantic_errors(*f.object.errors.attribute_names) + f.inputs "Details", class: "mb-6" do + f.input :title + f.input :author + f.input :published_date, + hint: f.object.persisted? && "Created at #{f.object.created_at}" + f.input :custom_category_id + f.input :category, hint: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras tincidunt porttitor massa eu consequat. Suspendisse potenti. Curabitur gravida sem vel elit auctor ultrices." + f.input :position + f.input :starred + end + f.inputs "Content", class: "mb-6" do + f.input :body + end + f.inputs "Tags", class: "mb-6" do + f.has_many :taggings, heading: false, sortable: :position do |t| + t.input :tag + t.input :_destroy, as: :boolean + end + end + para "Press cancel to return to the list without saving.", class: "py-2" + f.actions + end +end diff --git a/spec/support/templates_with_data/admin/tags.rb b/spec/support/templates_with_data/admin/tags.rb new file mode 100644 index 00000000000..549ff702301 --- /dev/null +++ b/spec/support/templates_with_data/admin/tags.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +ActiveAdmin.register Tag do + config.create_another = true + + permit_params :name + + index do + selectable_column + id_column + column :name + column :created_at + actions do |tag| + item "Preview", admin_tag_path(tag) + end + end +end diff --git a/spec/support/templates_with_data/admin/users.rb b/spec/support/templates_with_data/admin/users.rb new file mode 100644 index 00000000000..44988ca2991 --- /dev/null +++ b/spec/support/templates_with_data/admin/users.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true +ActiveAdmin.register User do + config.create_another = true + + permit_params :first_name, :last_name, :username, :age + + preserve_default_filters! + filter :first_name_or_last_name_cont, as: :string, label: "First or Last Name" + + index do + selectable_column + id_column + column :first_name + column :last_name + column :username + column :age + column :created_at, class: "min-w-52" + column :updated_at, class: "min-w-52" + actions + end + + index as: ActiveAdmin::Views::CustomIndex do |user| + label do + div class: "flex items-center gap-2 text-xl mb-2" do + resource_selection_cell user + span link_to(user.display_name, admin_user_path(user)) + end + div "@#{user.username}", class: "mb-2" + div "#{user.age} years old", class: "mb-2 font-semibold" + end + end + + show do + attributes_table_for(resource) do + row :id + row :first_name + row :last_name + row :username + row :age + row :created_at + row :updated_at + end + + h3 "Posts", class: "font-bold py-5 text-2xl" + + paginated_collection(user.posts.includes(:category).order(:updated_at).page(params[:page]).per(10), download_links: false) do + table_for(collection) do + column :id do |post| + link_to post.id, admin_user_post_path(post.author, post) + end + column :title + column :published_date + column :category + column :created_at + column :updated_at + end + end + + div class: "mt-4" do + link_to "View all posts", admin_user_posts_path(user) + end + end +end diff --git a/spec/tasks/gemfile_spec.rb b/spec/tasks/gemfile_spec.rb new file mode 100644 index 00000000000..485013eef0d --- /dev/null +++ b/spec/tasks/gemfile_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +RSpec.describe "Gemfile sanity" do + # Strips the BUNDLED WITH section from a Gemfile.lock content string. + # This section only contains the Bundler version used and can differ between environments + # without affecting the actual dependency resolution. + # + # Handles various edge cases: + # - Different line endings (\n, \r\n) + # - Any amount of whitespace before the version number + # - Missing BUNDLED WITH section + # - Different formatting + def strip_bundled_with(lockfile_content) + # Normalize line endings to Unix style for consistent comparison + normalized = lockfile_content.gsub(/\r\n/, "\n") + + # Remove the BUNDLED WITH section and everything after it + # The regex matches: + # - Optional newline before BUNDLED WITH + # - The BUNDLED WITH line itself + # - Any content after it (version number with any whitespace/line endings) + normalized.gsub(/\n?BUNDLED WITH\n.*\z/m, '').strip + end + + it "is up to date" do + gemfile = ENV["BUNDLE_GEMFILE"] || "Gemfile" + current_lockfile = File.read("#{gemfile}.lock") + + new_lockfile = Bundler.with_original_env do + `bundle lock --print` + end + + msg = "Please update #{gemfile}'s lock file with `BUNDLE_GEMFILE=#{gemfile} bundle install --all` and commit the result" + + # Compare lockfiles without the BUNDLED WITH section to avoid false failures + # when only the Bundler version differs between environments + expect(strip_bundled_with(current_lockfile)).to eq(strip_bundled_with(new_lockfile)), msg + end +end diff --git a/spec/tasks/local_spec.rb b/spec/tasks/local_spec.rb new file mode 100644 index 00000000000..6e1e94fa24c --- /dev/null +++ b/spec/tasks/local_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +require "open3" + +RSpec.describe "local task" do + let(:local) do + Open3.capture2e("bin/rake local runner 'AdminUser.first'") + end + + it "succeeds" do + expect(local[1]).to be_success, local[0] + end +end diff --git a/spec/unit/abstract_view_factory_spec.rb b/spec/unit/abstract_view_factory_spec.rb deleted file mode 100644 index 0e608c82f20..00000000000 --- a/spec/unit/abstract_view_factory_spec.rb +++ /dev/null @@ -1,79 +0,0 @@ -require 'spec_helper' - -require 'active_admin/abstract_view_factory' - -describe ActiveAdmin::AbstractViewFactory do - - let(:view_factory){ ActiveAdmin::AbstractViewFactory.new } - let(:mock_view){ Class.new } - - describe "registering a new view key" do - before do - view_factory.register :my_new_view_class => mock_view - end - - it "should respond to :my_new_view_class" do - view_factory.respond_to?(:my_new_view_class) - end - - it "should respond to :my_new_view_class=" do - view_factory.respond_to?(:my_new_view_class=) - end - - it "should generate a getter method" do - view_factory.my_new_view_class.should == mock_view - end - - it "should be settable view a setter method" do - view_factory.my_new_view_class = "Some Obj" - view_factory.my_new_view_class.should == "Some Obj" - end - end - - describe "array syntax access" do - before do - view_factory.register :my_new_view_class => mock_view - end - - it "should be available through array syntax" do - view_factory[:my_new_view_class].should == mock_view - end - - it "should be settable through array syntax" do - view_factory[:my_new_view_class] = "My New View Class" - view_factory[:my_new_view_class].should == "My New View Class" - end - end - - describe "registering default views" do - before do - ActiveAdmin::AbstractViewFactory.register :my_default_view_class => mock_view - end - it "should generate a getter method" do - view_factory.my_default_view_class.should == mock_view - end - it "should be settable view a setter method and not change default" do - view_factory.my_default_view_class = "Some Obj" - view_factory.my_default_view_class.should == "Some Obj" - view_factory.default_for(:my_default_view_class).should == mock_view - end - end - - describe "subclassing the ViewFactory" do - let(:subclass) do - ActiveAdmin::AbstractViewFactory.register :my_subclassed_view => "From Parent" - Class.new(ActiveAdmin::AbstractViewFactory) do - def my_subclassed_view - "From Subclass" - end - end - end - - it "should use the subclass implementation" do - factory = subclass.new - factory.my_subclassed_view.should == "From Subclass" - end - end - - -end diff --git a/spec/unit/action_builder_spec.rb b/spec/unit/action_builder_spec.rb index 8f2a9d144b0..53d8e4ed3dd 100644 --- a/spec/unit/action_builder_spec.rb +++ b/spec/unit/action_builder_spec.rb @@ -1,17 +1,18 @@ -require 'spec_helper' +# frozen_string_literal: true +require "rails_helper" -describe 'defining new actions from registration blocks' do +RSpec.describe "defining actions from registration blocks", type: :controller do + let(:klass) { Admin::PostsController } - let(:controller){ Admin::PostsController } + before do + load_resources { action! } - describe "generating a new member action" do - before do - action! - reload_routes! - end + @controller = klass.new + end - after(:each) do - controller.clear_member_actions! + describe "creates a member action" do + after do + klass.clear_member_actions! end context "with a block" do @@ -24,13 +25,15 @@ end it "should create a new public instance method" do - controller.public_instance_methods.collect(&:to_s).should include("comment") + expect(klass.public_instance_methods.collect(&:to_s)).to include("comment") end + it "should add itself to the member actions config" do - controller.active_admin_config.member_actions.size.should == 1 + expect(klass.active_admin_config.member_actions.size).to eq 1 end + it "should create a new named route" do - Rails.application.routes.url_helpers.methods.collect(&:to_s).should include("comment_admin_post_path") + expect(Rails.application.routes.url_helpers.methods.collect(&:to_s)).to include("comment_admin_post_path") end end @@ -40,31 +43,32 @@ member_action :comment end end + it "should still generate a new empty action" do - controller.public_instance_methods.collect(&:to_s).should include("comment") + expect(klass.public_instance_methods.collect(&:to_s)).to include("comment") end end context "with :title" do let(:action!) do ActiveAdmin.register Post do - member_action :comment, :title => "My Awesome Comment" + member_action :comment, title: "My Awesome Comment" do + render json: { a: 2 } + end end end - subject { find_before_filter controller, :comment } + it "sets the page title" do + get :comment, params: { id: 1 } - it { should set_page_title_to "My Awesome Comment" } + expect(controller.instance_variable_get(:@page_title)).to eq "My Awesome Comment" + end end end - describe "generate a new collection action" do - before do - action! - reload_routes! - end - after(:each) do - controller.clear_collection_actions! + describe "creates a collection action" do + after do + klass.clear_collection_actions! end context "with a block" do @@ -75,52 +79,74 @@ end end end - it "should create a new public instance method" do - controller.public_instance_methods.collect(&:to_s).should include("comments") + + it "should create a public instance method" do + expect(klass.public_instance_methods.collect(&:to_s)).to include("comments") end + it "should add itself to the member actions config" do - controller.active_admin_config.collection_actions.size.should == 1 + expect(klass.active_admin_config.collection_actions.size).to eq 1 end - it "should create a new named route" do - Rails.application.routes.url_helpers.methods.collect(&:to_s).should include("comments_admin_posts_path") + + it "should create a named route" do + expect(Rails.application.routes.url_helpers.methods.collect(&:to_s)).to include("comments_admin_posts_path") end end + context "without a block" do let(:action!) do ActiveAdmin.register Post do collection_action :comments end end + it "should still generate a new empty action" do - controller.public_instance_methods.collect(&:to_s).should include("comments") + expect(klass.public_instance_methods.collect(&:to_s)).to include("comments") end end + context "with :title" do let(:action!) do ActiveAdmin.register Post do - collection_action :comments, :title => "My Awesome Comments" + collection_action :comments, title: "My Awesome Comments" do + render json: { a: 2 } + end end end - subject { find_before_filter controller, :comments } + it "sets the page title" do + get :comments - it { should set_page_title_to "My Awesome Comments" } + expect(controller.instance_variable_get(:@page_title)).to eq "My Awesome Comments" + end end end - def find_before_filter(controller, action) - controller._process_action_callbacks.detect { |f| f.kind == :before && f.options[:only] == [action] } - end + context "when method with given name is already defined" do + include_context "capture stderr" + + describe "defining member action" do + let :action! do + ActiveAdmin.register Post do + member_action :process + end + end - RSpec::Matchers.define :set_page_title_to do |expected| - match do |filter| - filter.raw_filter.call - @actual = filter.klass.instance_variable_get(:@page_title) - @actual == expected + it "writes warning to $stderr" do + expect($stderr.string).to include("Warning: method `process` already defined in Admin::PostsController") + end end - failure_message_for_should do |filter| - message = "expected before_filter to set the @page_title to '#{expected}', but was '#{@actual}'" + describe "defining collection action" do + let :action! do + ActiveAdmin.register Post do + collection_action :process + end + end + + it "writes warning to $stderr" do + expect($stderr.string).to include("Warning: method `process` already defined in Admin::PostsController") + end end end end diff --git a/spec/unit/active_admin_spec.rb b/spec/unit/active_admin_spec.rb index 87e1fc17597..a3871bf8dc3 100644 --- a/spec/unit/active_admin_spec.rb +++ b/spec/unit/active_admin_spec.rb @@ -1,9 +1,10 @@ -require 'spec_helper' +# frozen_string_literal: true +require "rails_helper" -describe ActiveAdmin do +RSpec.describe ActiveAdmin do %w(register register_page unload! load! routes).each do |method| it "delegates ##{method} to application" do - ActiveAdmin.application.should_receive(method) + expect(ActiveAdmin.application).to receive(method) ActiveAdmin.send(method) end diff --git a/spec/unit/application_spec.rb b/spec/unit/application_spec.rb index e29d649d8be..455325b4de2 100644 --- a/spec/unit/application_spec.rb +++ b/spec/unit/application_spec.rb @@ -1,132 +1,161 @@ -require 'spec_helper' -require 'fileutils' +# frozen_string_literal: true +require "rails_helper" +require "fileutils" -describe ActiveAdmin::Application do - - let(:application) do - ActiveAdmin::Application.new.tap do |app| - # Manually override the load paths becuase RSpec messes these up - app.load_paths = [File.expand_path('app/admin', Rails.root)] - end - end +RSpec.describe ActiveAdmin::Application do + let(:application) { ActiveAdmin::Application.new } it "should have a default load path of ['app/admin']" do - application.load_paths.should == [File.expand_path('app/admin', Rails.root)] + expect(application.load_paths).to eq [File.expand_path("app/admin", application.app_path)] end - it "should remove app/admin from the autoload path to remove the possibility of conflicts" do - ActiveSupport::Dependencies.autoload_paths.should_not include(File.join(Rails.root, "app/admin")) - end + describe "#prepare" do + before { application.prepare! } - it "should remove app/admin from the eager load paths (Active Admin deals with loading)" do - Rails.application.config.eager_load_paths.should_not include(File.join(Rails.root, "app/admin")) + it "should remove app/admin from the autoload paths" do + expect(ActiveSupport::Dependencies.autoload_paths).to_not include(Rails.root.join("app/admin")) + end end it "should store the site's title" do - application.site_title.should == "" + expect(application.site_title).to eq "" end it "should set the site title" do application.site_title = "New Title" - application.site_title.should == "New Title" + expect(application.site_title).to eq "New Title" end - it "should store the site's title link" do - application.site_title_link.should == "" + it "should set the site title using a block" do + application.site_title = proc { "Block Title" } + expect(application.site_title).to eq "Block Title" end - it "should set the site's title link" do - application.site_title_link = "http://www.mygreatsite.com" - application.site_title_link.should == "http://www.mygreatsite.com" + it "should return default localize format" do + expect(application.localize_format).to eq :long end - it "should store the site's title image" do - application.site_title_image.should == "" + it "should set localize format" do + application.localize_format = :default + expect(application.localize_format).to eq :default end - it "should set the site's title image" do - application.site_title_image = "http://railscasts.com/assets/episodes/stills/284-active-admin.png?1316476106" - application.site_title_image.should == "http://railscasts.com/assets/episodes/stills/284-active-admin.png?1316476106" + it "should allow comments by default" do + expect(application.comments).to eq true end - it "should have a view factory" do - application.view_factory.should be_an_instance_of(ActiveAdmin::ViewFactory) + it "should have default order clause class" do + expect(application.order_clause).to eq ActiveAdmin::OrderClause end - it "should allow comments by default" do - application.allow_comments.should == true + it "should have default show_count for scopes" do + expect(application.scopes_show_count).to eq true end - describe "authentication settings" do + it "fails if setting undefined" do + expect do + application.undefined_setting + end.to raise_error(NoMethodError) + end + describe "authentication settings" do it "should have no default current_user_method" do - application.current_user_method.should == false + expect(application.current_user_method).to eq false end it "should have no default authentication method" do - application.authentication_method.should == false + expect(application.authentication_method).to eq false end it "should have a logout link path (Devise's default)" do - application.logout_link_path.should == :destroy_admin_user_session_path - end - - it "should have a logout link method (Devise's default)" do - application.logout_link_method.should == :get + expect(application.logout_link_path).to eq :destroy_admin_user_session_path end end describe "files in load path" do + it "it should load sorted files" do + expect(application.files.map { |f| File.basename(f) }).to eq %w(admin_users.rb companies.rb dashboard.rb profiles.rb stores.rb) + end + it "should load files in the first level directory" do - application.files.should include(File.expand_path("app/admin/dashboard.rb", Rails.root)) + expect(application.files).to include(File.expand_path("app/admin/dashboard.rb", application.app_path)) end - it "should load files from subdirectories" do - FileUtils.mkdir_p(File.expand_path("app/admin/public", Rails.root)) - test_file = File.expand_path("app/admin/public/posts.rb", Rails.root) - FileUtils.touch(test_file) - application.files.should include(test_file) + it "should load files from subdirectories", :changes_filesystem do + test_dir = File.expand_path("app/admin/public", application.app_path) + test_file = File.expand_path("app/admin/public/posts.rb", application.app_path) + + begin + FileUtils.mkdir_p(test_dir) + FileUtils.touch(test_file) + expect(application.files).to include(test_file) + ensure + FileUtils.remove_entry_secure(test_dir, force: true) + end + end + + it "should honor load paths order", :changes_filesystem do + test_dir = File.expand_path("app/other-admin", application.app_path) + test_file = File.expand_path("app/other-admin/posts.rb", application.app_path) + + application.load_paths.unshift(test_dir) + + begin + FileUtils.mkdir_p(test_dir) + FileUtils.touch(test_file) + expect(application.files.map { |f| File.basename(f) }).to( + eq(%w(posts.rb admin_users.rb companies.rb dashboard.rb profiles.rb stores.rb)) + ) + ensure + FileUtils.remove_entry_secure(test_dir, force: true) + end end end describe "#namespace" do - it "should yield a new namespace" do application.namespace :new_namespace do |ns| - ns.name.should == :new_namespace + expect(ns.name).to eq :new_namespace end end it "should return an instantiated namespace" do admin = application.namespace :admin - admin.should == application.namespaces[:admin] + expect(admin).to eq application.namespaces[:admin] end it "should yield an existing namespace" do - expect { + expect do application.namespace :admin do |ns| - ns.should == application.namespaces[:admin] + expect(ns).to eq application.namespaces[:admin] raise "found" end - }.to raise_error("found") + end.to raise_error("found") + end + + it "should admit both strings and symbols" do + expect do + application.namespace "admin" do |ns| + expect(ns).to eq application.namespaces[:admin] + raise "found" + end + end.to raise_error("found") end it "should not pollute the global app" do - application.namespaces.keys.should be_empty + expect(application.namespaces).to be_empty application.namespace(:brand_new_ns) - application.namespaces.keys.should eq [:brand_new_ns] - ActiveAdmin.application.namespaces.keys.should eq [:admin] + expect(application.namespaces.names).to eq [:brand_new_ns] + expect(ActiveAdmin.application.namespaces.names).to eq [:admin] end end describe "#register_page" do it "finds or create the namespace and register the page to it" do - namespace = mock - application.should_receive(:namespace).with("public").and_return namespace - namespace.should_receive(:register_page).with("My Page", {:namespace => "public"}) - - application.register_page("My Page", :namespace => "public") + namespace = double + expect(application).to receive(:namespace).with("public").and_return namespace + expect(namespace).to receive(:register_page).with("My Page", { namespace: "public" }) + application.register_page("My Page", namespace: "public") end end - end diff --git a/spec/unit/asset_registration_spec.rb b/spec/unit/asset_registration_spec.rb deleted file mode 100644 index f1201baa13f..00000000000 --- a/spec/unit/asset_registration_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'spec_helper' - -module MockRegistration - extend ActiveAdmin::AssetRegistration -end - -describe ActiveAdmin::AssetRegistration do - - before do - MockRegistration.clear_stylesheets! - MockRegistration.clear_javascripts! - end - - it "should register a stylesheet file" do - MockRegistration.register_stylesheet "active_admin.css" - MockRegistration.stylesheets.length.should == 1 - MockRegistration.stylesheets.keys.first.should == "active_admin.css" - end - - it "should clear all existing stylesheets" do - MockRegistration.register_stylesheet "active_admin.css" - MockRegistration.stylesheets.length.should == 1 - MockRegistration.clear_stylesheets! - MockRegistration.stylesheets.should be_empty - end - - it "should allow media option when registering stylesheet" do - MockRegistration.register_stylesheet "active_admin.css", :media => :print - MockRegistration.stylesheets.values.first[:media].should == :print - end - - it "shouldn't register a stylesheet twice" do - MockRegistration.register_stylesheet "active_admin.css" - MockRegistration.register_stylesheet "active_admin.css" - MockRegistration.stylesheets.length.should == 1 - end - - - it "should register a javascript file" do - MockRegistration.register_javascript "active_admin.js" - MockRegistration.javascripts.should == ["active_admin.js"].to_set - end - - it "should clear all existing javascripts" do - MockRegistration.register_javascript "active_admin.js" - MockRegistration.javascripts.should == ["active_admin.js"].to_set - MockRegistration.clear_javascripts! - MockRegistration.javascripts.should be_empty - end - - it "shouldn't register a javascript twice" do - MockRegistration.register_javascript "active_admin.js" - MockRegistration.register_javascript "active_admin.js" - MockRegistration.javascripts.length.should == 1 - end -end diff --git a/spec/unit/async_count_spec.rb b/spec/unit/async_count_spec.rb new file mode 100644 index 00000000000..d875e7d445c --- /dev/null +++ b/spec/unit/async_count_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe ActiveAdmin::AsyncCount do + include ActiveAdmin::IndexHelper + + def seed_posts + [1, 2].map do |i| + Post.create!(title: "Test #{i}", author_id: i * 100) + end + end + + it "can be passed to the collection_size helper", if: Post.respond_to?(:async_count) do + seed_posts + + expect(collection_size(described_class.new(Post.all))).to eq(Post.count) + expect(collection_size(described_class.new(Post.group(:author_id)))).to eq(Post.distinct.pluck(:author_id).size) + end + + describe "#initialize" do + let(:collection) { Post.all } + + it "initiates an async_count query", if: Post.respond_to?(:async_count) do + expect(collection).to receive(:async_count) + described_class.new(collection) + end + + it "raises an error when ActiveRecord async_count is unavailable", unless: Post.respond_to?(:async_count) do + expect do + described_class.new(collection) + end.to raise_error(ActiveAdmin::AsyncCount::NotSupportedError, %r{does not support :async_count}) + end + end + + describe "#count", if: Post.respond_to?(:async_count) do + before { seed_posts } + + it "returns the result of a count query" do + async_count = described_class.new(Post.all) + expect(async_count.count).to eq(Post.count) + end + + it "returns the Hash of counts for a grouped query" do + async_count = described_class.new(Post.group(:author_id)) + expect(async_count.count).to eq(100 => 1, 200 => 1) + end + end + + describe "delegation", if: Post.respond_to?(:async_count) do + let(:collection) { Post.all } + + %i[ + except + group_values + length + limit_value + ].each do |method| + it "delegates #{method}" do + allow(collection).to receive(method).and_call_original + + async_count = described_class.new(collection) + async_count.public_send(method) + + expect(collection).to have_received(method).at_least(:once) + end + end + end +end diff --git a/spec/unit/authorization/authorization_adapter_spec.rb b/spec/unit/authorization/authorization_adapter_spec.rb index 77da3688628..a31a22f25ba 100644 --- a/spec/unit/authorization/authorization_adapter_spec.rb +++ b/spec/unit/authorization/authorization_adapter_spec.rb @@ -1,31 +1,25 @@ -require 'spec_helper' +# frozen_string_literal: true +require "rails_helper" -describe ActiveAdmin::AuthorizationAdapter do - - let(:adapter) { ActiveAdmin::AuthorizationAdapter.new(stub, stub) } +RSpec.describe ActiveAdmin::AuthorizationAdapter do + let(:adapter) { ActiveAdmin::AuthorizationAdapter.new(double, double) } describe "#authorized?" do - it "should always return true" do - adapter.authorized?(:read, "Resource").should == true + expect(adapter.authorized?(:read, "Resource")).to eq true end - end describe "#scope_collection" do - it "should return the collection unscoped" do - collection = stub - adapter.scope_collection(collection, ActiveAdmin::Auth::READ).should == collection + collection = double + expect(adapter.scope_collection(collection, ActiveAdmin::Auth::READ)).to eq collection end - end describe "using #normalized in a subclass" do - let(:auth_class) do Class.new(ActiveAdmin::AuthorizationAdapter) do - def authorized?(action, subject = nil) case subject when normalized(String) @@ -34,28 +28,25 @@ def authorized?(action, subject = nil) false end end - end end - let(:adapter) { auth_class.new(stub, stub) } + let(:adapter) { auth_class.new(double, double) } it "should match against a class" do - adapter.authorized?(:read, String).should == true + expect(adapter.authorized?(:read, String)).to eq true end - it 'should match against an instance' do - adapter.authorized?(:read, "String").should == true + it "should match against an instance" do + expect(adapter.authorized?(:read, "String")).to eq true end - it 'should not match a different class' do - adapter.authorized?(:read, Hash).should == false + it "should not match a different class" do + expect(adapter.authorized?(:read, Hash)).to eq false end - it 'should not match a different instance' do - adapter.authorized?(:read, {}).should == false + it "should not match a different instance" do + expect(adapter.authorized?(:read, {})).to eq false end - end - end diff --git a/spec/unit/authorization/controller_authorization_spec.rb b/spec/unit/authorization/controller_authorization_spec.rb index 26855cfeaba..deac16823dd 100644 --- a/spec/unit/authorization/controller_authorization_spec.rb +++ b/spec/unit/authorization/controller_authorization_spec.rb @@ -1,39 +1,43 @@ -require 'spec_helper' -Auth = ActiveAdmin::Authorization +# frozen_string_literal: true +require "rails_helper" -describe Admin::PostsController, "Controller Authorization", :type => :controller do - - let(:authorization){ controller.send(:active_admin_authorization) } +RSpec.describe "Controller Authorization", type: :controller do + let(:authorization) { controller.send(:active_admin_authorization) } before do - load_defaults! - # HACK: the AA config is missing, so we throw it in here - controller.class.active_admin_config = ActiveAdmin.application.namespace(:admin).resources['Post'].controller.active_admin_config + load_resources { ActiveAdmin.register Post } + @controller = Admin::PostsController.new + allow(authorization).to receive(:authorized?) end it "should authorize the index action" do - authorization.should_receive(:authorized?).with(Auth::READ, Post).and_return true + expect(authorization).to receive(:authorized?).with(auth::READ, Post).and_return true get :index - response.should be_success + expect(response).to be_successful end it "should authorize the new action" do - authorization.should_receive(:authorized?).with(Auth::CREATE, an_instance_of(Post)).and_return true + expect(authorization).to receive(:authorized?).with(auth::NEW, an_instance_of(Post)).and_return true get :new - response.should be_success + expect(response).to be_successful end it "should authorize the create action with the new resource" do - authorization.should_receive(:authorized?).with(Auth::CREATE, an_instance_of(Post)).and_return true + expect(authorization).to receive(:authorized?).with(auth::CREATE, an_instance_of(Post)).and_return true post :create - response.should redirect_to action: 'show', id: Post.last.id + expect(response).to redirect_to action: "show", id: Post.last.id end it "should redirect when the user isn't authorized" do - authorization.should_receive(:authorized?).with(Auth::READ, Post).and_return false + expect(authorization).to receive(:authorized?).with(auth::READ, Post).and_return false get :index - response.body.should eq 'You are being redirected.' - response.should redirect_to '/admin' + + expect(response).to redirect_to "/admin" end + private + + def auth + ActiveAdmin::Authorization + end end diff --git a/spec/unit/authorization/index_overriding_spec.rb b/spec/unit/authorization/index_overriding_spec.rb new file mode 100644 index 00000000000..5b28a59f58f --- /dev/null +++ b/spec/unit/authorization/index_overriding_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe "Index overriding", type: :controller do + before do + load_resources { ActiveAdmin.register Post } + @controller = Admin::PostsController.new + + @controller.instance_eval do + def index + super do + render body: "Rendered from passed block" + return + end + end + end + end + + it "should call block passed to overridden index" do + get :index + expect(response.body).to eq "Rendered from passed block" + end +end diff --git a/spec/unit/auto_link_spec.rb b/spec/unit/auto_link_spec.rb deleted file mode 100644 index 1d511d65372..00000000000 --- a/spec/unit/auto_link_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'spec_helper' - -class AutoLinkMockResource - attr_accessor :namespace - def initialize(namespace) - @namespace = namespace - end -end - -describe "auto linking resources" do - include ActiveAdmin::ViewHelpers::ActiveAdminApplicationHelper - include ActiveAdmin::ViewHelpers::AutoLinkHelper - include ActiveAdmin::ViewHelpers::DisplayHelper - - let(:active_admin_config) { AutoLinkMockResource.new(namespace) } - let(:active_admin_namespace){ ActiveAdmin::Namespace.new(ActiveAdmin::Application.new, :admin) } - let(:post){ Post.create! :title => "Hello World" } - - def admin_post_path(post) - "/admin/posts/#{post.id}" - end - - context "when the resource is not registered" do - it "should return the display name of the object" do - auto_link(post).should == "Hello World" - end - end - - context "when the resource is registered" do - before do - active_admin_namespace.register Post - end - it "should return a link with the display name of the object" do - self.should_receive(:link_to).with("Hello World", admin_post_path(post)) - auto_link(post) - end - end - -end diff --git a/spec/unit/batch_actions/resource_spec.rb b/spec/unit/batch_actions/resource_spec.rb index 4a4731dd04c..99b3c8488bb 100644 --- a/spec/unit/batch_actions/resource_spec.rb +++ b/spec/unit/batch_actions/resource_spec.rb @@ -1,7 +1,7 @@ -require 'spec_helper' - -describe ActiveAdmin::BatchActions::ResourceExtension do +# frozen_string_literal: true +require "rails_helper" +RSpec.describe ActiveAdmin::BatchActions::ResourceExtension do let(:resource) do namespace = ActiveAdmin::Namespace.new(ActiveAdmin::Application.new, :admin) namespace.batch_actions = true @@ -9,15 +9,13 @@ end describe "default action" do - it "should have the default action by default" do - resource.batch_actions.size.should == 1 and resource.batch_actions.first.sym == :destroy + expect(resource.batch_actions.size).to eq 1 + expect(resource.batch_actions.first.sym == :destroy).to eq true end - end describe "adding a new batch action" do - before do resource.clear_batch_actions! resource.add_batch_action :flag, "Flag" do @@ -26,66 +24,50 @@ end it "should add an batch action" do - resource.batch_actions.size.should == 1 + expect(resource.batch_actions.size).to eq 1 end it "should store an instance of BatchAction" do - resource.batch_actions.first.should be_an_instance_of(ActiveAdmin::BatchAction) + expect(resource.batch_actions.first).to be_an_instance_of(ActiveAdmin::BatchAction) end it "should store the block in the batch action" do - resource.batch_actions.first.block.should_not be_nil + expect(resource.batch_actions.first.block).to_not eq nil end - end describe "removing batch action" do - before do resource.remove_batch_action :destroy end it "should allow for batch action removal" do - resource.batch_actions.size.should == 0 + expect(resource.batch_actions.size).to eq 0 end - - end - - describe "#batch_action_path" do - - it "returns the path as a symbol" do - resource.batch_action_path.should == "/admin/posts/batch_action" - end - end describe "#display_if_block" do - it "should return true by default" do action = ActiveAdmin::BatchAction.new :default, "Default" - action.display_if_block.call.should == true + expect(action.display_if_block.call).to eq true end it "should return the :if block if set" do - action = ActiveAdmin::BatchAction.new :with_block, "With Block", :if => proc { false } - action.display_if_block.call.should == false + action = ActiveAdmin::BatchAction.new :with_block, "With Block", if: proc { false } + expect(action.display_if_block.call).to eq false end - end describe "batch action priority" do - it "should have a default priority" do action = ActiveAdmin::BatchAction.new :default, "Default" - action.priority.should == 10 + expect(action.priority).to eq 10 end it "should correctly order two actions" do - priority_one = ActiveAdmin::BatchAction.new :one, "One", :priority => 1 - priority_ten = ActiveAdmin::BatchAction.new :ten, "Ten", :priority => 10 - priority_one.should be < priority_ten + priority_one = ActiveAdmin::BatchAction.new :one, "One", priority: 1 + priority_ten = ActiveAdmin::BatchAction.new :ten, "Ten", priority: 10 + expect(priority_one).to be < priority_ten end - end - end diff --git a/spec/unit/batch_actions/settings_spec.rb b/spec/unit/batch_actions/settings_spec.rb index d013a52c0c9..6338b8fcd3a 100644 --- a/spec/unit/batch_actions/settings_spec.rb +++ b/spec/unit/batch_actions/settings_spec.rb @@ -1,6 +1,7 @@ -require 'spec_helper' +# frozen_string_literal: true +require "rails_helper" -describe "Batch Actions Settings" do +RSpec.describe "Batch Actions Settings" do let(:app) { ActiveAdmin::Application.new } let(:ns) { ActiveAdmin::Namespace.new(app, "Admin") } let(:post_resource) { ns.register Post } @@ -8,54 +9,54 @@ it "should be disabled globally by default" do # Note: the default initializer would set it to true - app.batch_actions.should be_false - ns.batch_actions.should be_false - post_resource.batch_actions_enabled?.should be_false + expect(app.batch_actions).to eq false + expect(ns.batch_actions).to eq false + expect(post_resource.batch_actions_enabled?).to eq false end it "should be settable to true" do app.batch_actions = true - app.batch_actions.should == true + expect(app.batch_actions).to eq true end it "should be an inheritable_setting" do app.batch_actions = true - ns.batch_actions.should == true + expect(ns.batch_actions).to eq true end it "should be settable at the namespace level" do app.batch_actions = true ns.batch_actions = false - app.batch_actions.should == true - ns.batch_actions.should == false + expect(app.batch_actions).to eq true + expect(ns.batch_actions).to eq false end it "should be settable at the resource level" do - post_resource.batch_actions_enabled?.should == false + expect(post_resource.batch_actions_enabled?).to eq false post_resource.batch_actions = true - post_resource.batch_actions_enabled?.should == true + expect(post_resource.batch_actions_enabled?).to eq true end it "should inherit the setting on the resource from the namespace" do ns.batch_actions = false - post_resource.batch_actions_enabled?.should == false - post_resource.batch_actions.should be_empty + expect(post_resource.batch_actions_enabled?).to eq false + expect(post_resource.batch_actions).to be_empty post_resource.batch_actions = true - post_resource.batch_actions_enabled?.should == true - post_resource.batch_actions.should_not be_empty + expect(post_resource.batch_actions_enabled?).to eq true + expect(post_resource.batch_actions).to_not be_empty end it "should inherit the setting from the namespace when set to nil" do ns.batch_actions = true post_resource.batch_actions = true - post_resource.batch_actions_enabled?.should == true - post_resource.batch_actions.should_not be_empty + expect(post_resource.batch_actions_enabled?).to eq true + expect(post_resource.batch_actions).to_not be_empty post_resource.batch_actions = nil - post_resource.batch_actions_enabled?.should == true # inherited from namespace - post_resource.batch_actions.should_not be_empty + expect(post_resource.batch_actions_enabled?).to eq true # inherited from namespace + expect(post_resource.batch_actions).to_not be_empty end end diff --git a/spec/unit/belongs_to_spec.rb b/spec/unit/belongs_to_spec.rb index 86d421c4883..e0675d1c22e 100644 --- a/spec/unit/belongs_to_spec.rb +++ b/spec/unit/belongs_to_spec.rb @@ -1,42 +1,239 @@ -require 'spec_helper' - -describe ActiveAdmin::Resource::BelongsTo do +# frozen_string_literal: true +require "rails_helper" +RSpec.describe ActiveAdmin::Resource::BelongsTo do + around do |example| + with_resources_during(example) do + ActiveAdmin.register User + ActiveAdmin.register(Post) { belongs_to :user } + end + end - let(:application){ ActiveAdmin::Application.new } - let(:namespace){ ActiveAdmin::Namespace.new(application, :admin) } - let(:post){ namespace.register(Post) } - let(:belongs_to){ ActiveAdmin::Resource::BelongsTo.new(post, :user) } + let(:user_config) { ActiveAdmin.register User } + let(:post_config) { ActiveAdmin.register(Post) { belongs_to :user } } + let(:belongs_to) { post_config.belongs_to_config } it "should have an owner" do - belongs_to.owner.should == post - end - - it "should have a namespace" do - belongs_to.namespace.should == namespace + expect(belongs_to.owner).to eq post_config end describe "finding the target" do context "when the resource has been registered" do - let(:user){ namespace.register(User) } - before { user } # Ensure user is registered - it "should return the target resource" do - belongs_to.target.should == user + expect(belongs_to.target).to eq user_config end end context "when the resource has not been registered" do + let(:belongs_to) { ActiveAdmin::Resource::BelongsTo.new post_config, :missing } + it "should raise a ActiveAdmin::BelongsTo::TargetNotFound" do - lambda { + expect do belongs_to.target - }.should raise_error(ActiveAdmin::Resource::BelongsTo::TargetNotFound) + end.to raise_error(ActiveAdmin::Resource::BelongsTo::TargetNotFound) + end + end + + context "when the resource is on a namespace" do + let(:blog_post_config) { ActiveAdmin.register Blog::Post } + let(:belongs_to) { ActiveAdmin::Resource::BelongsTo.new blog_post_config, :blog_author, class_name: "Blog::Author" } + before do + class Blog::Author + include ActiveModel::Naming + end + @blog_author_config = ActiveAdmin.register Blog::Author + end + it "should return the target resource" do + expect(belongs_to.target).to eq @blog_author_config end end end it "should be optional" do - belongs_to = ActiveAdmin::Resource::BelongsTo.new post, :user, :optional => true - belongs_to.should be_optional + belongs_to = ActiveAdmin::Resource::BelongsTo.new post_config, :user, optional: true + expect(belongs_to).to be_optional + end + + describe "controller" do + let(:controller) { post_config.controller.new } + let(:http_params) { { user_id: user.id } } + let(:user) { User.create! } + + before do + request = double "Request", format: "application/json" + allow(controller).to receive(:params) { ActionController::Parameters.new(http_params) } + allow(controller).to receive(:request) { request } + end + + it "should be able to access the collection" do + expect(controller.send :collection).to be_a ActiveRecord::Relation + end + + it "should be able to build a new resource" do + expect(controller.send :build_resource).to be_a Post + end + end + + describe "method_for_association_chain (with `defaults collection_name:`)" do + around do |example| + with_resources_during(example) do + ActiveAdmin.register User + ActiveAdmin.register(Post) do + belongs_to :user, optional: true + controller do + defaults collection_name: :unstarred_posts + end + end + end + end + + let(:post_config) { ActiveAdmin.application.namespaces[:admin].resources["Post"] } + let(:controller) { post_config.controller.new } + let(:user) { User.create! } + + it "scopes through the configured collection name, which is the parent's association" do + controller.params = ActionController::Parameters.new(user_id: user.id) + + expect(controller.send(:method_for_association_chain)).to eq(:unstarred_posts) + end + + it "falls back to the model's plural when the association's class name does not resolve" do + controller.params = ActionController::Parameters.new(user_id: user.id) + allow(User.reflect_on_association(:unstarred_posts)).to receive(:klass).and_raise(NameError) + + expect(controller.send(:method_for_association_chain)).to eq(:posts) + end + end + + describe "controller with a fully qualified association class name" do + around do |example| + with_resources_during(example) do + ActiveAdmin.register User + ActiveAdmin.register(Post) do + belongs_to :user + controller do + defaults collection_name: :qualified_posts + end + end + end + end + + let(:post_config) { ActiveAdmin.application.namespaces[:admin].resources["Post"] } + let(:controller) { post_config.controller.new } + let(:user) { User.create! } + + it "preserves the configured association scope" do + included_post = user.posts.create!(starred: false) + user.posts.create!(starred: true) + controller.params = ActionController::Parameters.new(user_id: user.id) + + expect(controller.send(:scoped_collection).to_a).to eq([included_post]) + end + end + + describe "method_for_association_chain (with `as:` alias and `defaults collection_name:`)" do + around do |example| + with_resources_during(example) do + ActiveAdmin.register User + ActiveAdmin.register(Post, as: "Story") do + belongs_to :user, optional: true + controller do + defaults collection_name: :unstarred_posts + end + end + end + end + + let(:post_config) { ActiveAdmin.application.namespaces[:admin].resources["Story"] } + let(:controller) { post_config.controller.new } + let(:user) { User.create! } + + it "prefers the collection name that was set over the alias's plural" do + controller.params = ActionController::Parameters.new(user_id: user.id) + + expect(controller.send(:method_for_association_chain)).to eq(:unstarred_posts) + end + end + + describe "method_for_association_chain (when the alias's plural is a real association)" do + around do |example| + with_resources_during(example) do + ActiveAdmin.register User + ActiveAdmin.register(Post, as: "Highlight") do + belongs_to :user, optional: true + end + end + end + + let(:post_config) { ActiveAdmin.application.namespaces[:admin].resources["Highlight"] } + let(:controller) { post_config.controller.new } + let(:user) { User.create! } + + it "scopes through it rather than deriving the model's plural" do + controller.params = ActionController::Parameters.new(user_id: user.id) + + expect(controller.send(:method_for_association_chain)).to eq(:highlights) + end + end + + describe "method_for_association_chain (with `as:` alias)" do + around do |example| + with_resources_during(example) do + ActiveAdmin.register User + ActiveAdmin.register(Post, as: "Story") { belongs_to :user, optional: true } + end + end + + let(:post_config) { ActiveAdmin.application.namespaces[:admin].resources["Story"] } + let(:controller) { post_config.controller.new } + let(:user) { User.create! } + + it "derives the parent's association name from the resource's model class" do + controller.params = ActionController::Parameters.new(user_id: user.id) + + expect(controller.send(:method_for_association_chain)).to eq(:posts) + end + end + + describe "controller with an alias matching an unrelated association" do + around do |example| + with_resources_during(example) do + ActiveAdmin.register Category + ActiveAdmin.register(Post, as: "Author") { belongs_to :category } + end + end + + let(:post_config) { ActiveAdmin.application.namespaces[:admin].resources["Author"] } + let(:controller) { post_config.controller.new } + let(:category) { Category.create!(name: "News") } + + it "scopes through posts rather than the category's authors" do + controller.params = ActionController::Parameters.new(category_id: category.id) + + expect(controller.send(:scoped_collection).klass).to eq(Post) + end + end + + describe "controller with optional nesting and scope_to" do + around do |example| + with_resources_during(example) do + ActiveAdmin.register User + ActiveAdmin.register(Post, as: "Article") do + belongs_to :user, optional: true + scope_to :current_category + end + end + end + + let(:post_config) { ActiveAdmin.application.namespaces[:admin].resources["Article"] } + let(:controller) { post_config.controller.new } + let(:category) { Category.create!(name: "News") } + + it "uses the scope_to parent's association on the non-nested route" do + controller.params = ActionController::Parameters.new + allow(controller).to receive(:current_category).and_return(category) + + expect(controller.send(:scoped_collection).klass).to eq(Post) + end end end diff --git a/spec/unit/cancan_adapter_spec.rb b/spec/unit/cancan_adapter_spec.rb index ebd68ccbd99..59a8b8213b3 100644 --- a/spec/unit/cancan_adapter_spec.rb +++ b/spec/unit/cancan_adapter_spec.rb @@ -1,43 +1,45 @@ -require 'spec_helper' - -describe ActiveAdmin::CanCanAdapter do +# frozen_string_literal: true +require "rails_helper" +RSpec.describe ActiveAdmin::CanCanAdapter do describe "full integration" do + let(:application) { ActiveAdmin::Application.new } + let(:namespace) { ActiveAdmin::Namespace.new(application, "Admin") } + let(:resource) { namespace.register(Post) } - let(:application){ ActiveAdmin::Application.new } - let(:namespace){ ActiveAdmin::Namespace.new(application, "Admin") } - let(:resource){ namespace.register(Post) } - - let :mock_ability_class do + let :ability_class do Class.new do include CanCan::Ability def initialize(user) can :read, Post + can :create, Post cannot :update, Post end - end end - let(:auth) { namespace.authorization_adapter.new(resource, mock) } + let(:auth) { namespace.authorization_adapter.new(resource, double) } before do namespace.authorization_adapter = ActiveAdmin::CanCanAdapter - namespace.cancan_ability_class = mock_ability_class + namespace.cancan_ability_class = ability_class end it "should initialize the ability stored in the namespace configuration" do - auth.authorized?(:read, Post).should == true - auth.authorized?(:update, Post).should == false + expect(auth.authorized?(:read, Post)).to eq true + expect(auth.authorized?(:update, Post)).to eq false + end + + it "should treat :new ability the same as :create" do + expect(auth.authorized?(:new, Post)).to eq true + expect(auth.authorized?(:create, Post)).to eq true end it "should scope the collection with accessible_by" do - collection = mock - collection.should_receive(:accessible_by).with(auth.cancan_ability, :edit) + collection = double + expect(collection).to receive(:accessible_by).with(auth.cancan_ability, :edit) auth.scope_collection(collection, :edit) end - end - end diff --git a/spec/unit/collection_decorator_spec.rb b/spec/unit/collection_decorator_spec.rb new file mode 100644 index 00000000000..9aaf0c62d45 --- /dev/null +++ b/spec/unit/collection_decorator_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true +require "rails_helper" + +class NumberDecorator + def initialize(number) + @number = number + end + + def selectable? + @number.even? + end +end + +RSpec.describe ActiveAdmin::CollectionDecorator do + describe "#decorated_collection" do + subject { collection.decorated_collection } + let(:collection) { ActiveAdmin::CollectionDecorator.decorate((1..10).to_a, with: NumberDecorator) } + + it "returns an array of decorated objects" do + expect(subject).to all(be_a(NumberDecorator)) + end + end + + describe "array methods" do + subject { ActiveAdmin::CollectionDecorator.decorate((1..10).to_a, with: NumberDecorator) } + + it "delegates them to the decorated collection" do + expect(subject.count(&:selectable?)).to eq(5) + end + end +end diff --git a/spec/unit/comments_spec.rb b/spec/unit/comments_spec.rb index 4940c088895..42fbc8228e9 100644 --- a/spec/unit/comments_spec.rb +++ b/spec/unit/comments_spec.rb @@ -1,90 +1,196 @@ -require 'spec_helper' +# frozen_string_literal: true +require "rails_helper" -describe "Comments" do - let(:application){ ActiveAdmin::Application.new } +RSpec.describe "Comments" do + let(:application) { ActiveAdmin::Application.new } describe ActiveAdmin::Comment do - subject { ActiveAdmin::Comment.new } - - describe "Associations and Validations" do - it { should belong_to :resource } - it { should belong_to :author } - it { should validate_presence_of :resource } - it { should validate_presence_of :body } - it { should validate_presence_of :namespace } + let(:comment) { ActiveAdmin::Comment.new } + + let(:user) { User.create!(first_name: "John", last_name: "Doe") } + + let(:post) { Post.create!(title: "Hello World") } + + it "belongs to a resource" do + comment.assign_attributes(resource_type: "Post", resource_id: post.id) + + expect(comment.resource).to eq(post) + end + + it "belongs to an author" do + comment.assign_attributes(author_type: "User", author_id: user.id) + + expect(comment.author).to eq(user) + end + + it "needs a body" do + expect(comment).to_not be_valid + expect(comment.errors[:body]).to eq(["can't be blank"]) + end + + it "needs a namespace" do + expect(comment).to_not be_valid + expect(comment.errors[:namespace]).to eq(["can't be blank"]) + end + + it "needs a resource" do + expect(comment).to_not be_valid + expect(comment.errors[:resource]).to eq(["can't be blank"]) + end + + it "authorizes default ransackable attributes" do + expect(described_class.ransackable_attributes).to eq described_class.authorizable_ransackable_attributes + end + + it "authorizes default ransackable associations" do + expect(described_class.ransackable_associations).to eq described_class.authorizable_ransackable_associations end describe ".find_for_resource_in_namespace" do - let(:post){ Post.create!(:title => "Hello World") } - let(:namespace_name){ "admin" } + let(:namespace_name) { "admin" } before do - @comment = ActiveAdmin::Comment.create! :resource => post, - :body => "A Comment", - :namespace => namespace_name + @comment = ActiveAdmin::Comment.create! author: user, + resource: post, + body: "A Comment", + namespace: namespace_name end it "should return a comment for the resource in the same namespace" do - ActiveAdmin::Comment.find_for_resource_in_namespace(post, namespace_name).should == [@comment] + expect(ActiveAdmin::Comment.find_for_resource_in_namespace(post, namespace_name)).to eq [@comment] end it "should not return a comment for the same resource in a different namespace" do - ActiveAdmin::Comment.find_for_resource_in_namespace(post, 'public').should == [] + ActiveAdmin.application.namespaces[:public] = ActiveAdmin.application.namespaces[:admin] + expect(ActiveAdmin::Comment.find_for_resource_in_namespace(post, "public")).to eq [] + ActiveAdmin.application.namespaces.instance_variable_get(:@namespaces).delete(:public) end it "should not return a comment for a different resource" do - another_post = Post.create! :title => "Another Hello World" - ActiveAdmin::Comment.find_for_resource_in_namespace(another_post, namespace_name).should == [] + another_post = Post.create! title: "Another Hello World" + expect(ActiveAdmin::Comment.find_for_resource_in_namespace(another_post, namespace_name)).to eq [] end - end - describe ".resource_id_cast" do - let(:post) { Post.create!(:title => "Testing.") } - let(:namespace_name) { "admin" } + it "should return the most recent comment first by default" do + another_comment = ActiveAdmin::Comment.create! author: user, + resource: post, + body: "Another Comment", + namespace: namespace_name, + created_at: @comment.created_at + 20.minutes + + yet_another_comment = ActiveAdmin::Comment.create! author: user, + resource: post, + body: "Yet Another Comment", + namespace: namespace_name, + created_at: @comment.created_at + 10.minutes + + comments = ActiveAdmin::Comment.find_for_resource_in_namespace(post, namespace_name) + expect(comments.size).to eq 3 + expect(comments.first).to eq(@comment) + expect(comments.second).to eq(yet_another_comment) + expect(comments.last).to eq(another_comment) + end - it "should cast resource_id as string" do - comment = ActiveAdmin::Comment.create! :resource => post, - :body => "Another Comment", - :namespace => namespace_name - ActiveAdmin::Comment.resource_id_cast(comment).class.should eql String + context "when custom ordering configured" do + around do |example| + previous_order = ActiveAdmin.application.comments_order + ActiveAdmin.application.comments_order = "created_at DESC" + + example.call + + ActiveAdmin.application.comments_order = previous_order + end + + it "should return the correctly ordered comments" do + another_comment = ActiveAdmin::Comment.create!( + author: user, + resource: post, + body: "Another Comment", + namespace: namespace_name, + created_at: @comment.created_at + 20.minutes + ) + + comments = ActiveAdmin::Comment.find_for_resource_in_namespace( + post, namespace_name + ) + expect(comments.size).to eq 2 + expect(comments.first).to eq(another_comment) + expect(comments.last).to eq(@comment) + end end end - describe ".resource_id_type" do - it "should be :string" do - ActiveAdmin::Comment.resource_id_type.should eql :string + describe ".resource_type" do + let(:post) { Post.create!(title: "Testing.") } + let(:post_decorator) { double "PostDecorator" } + + before do + allow(post_decorator).to receive(:model).and_return(post) + allow(post_decorator).to receive(:decorated?).and_return(true) + end + + context "when a decorated object is passed" do + let(:resource) { post_decorator } + + it "returns undeorated object class string" do + expect(ActiveAdmin::Comment.resource_type resource).to eql "Post" + end + end + + context "when an undecorated object is passed" do + let(:resource) { post } + + it "returns object class string" do + expect(ActiveAdmin::Comment.resource_type resource).to eql "Post" + end end end describe "Commenting on resource with string id" do - let(:tag){ Tag.create!(:name => "cooltags") } - let(:namespace_name){ "admin" } + let(:tag) { Tag.create!(name: "cooltags") } + let(:namespace_name) { "admin" } it "should allow commenting" do - comment = ActiveAdmin::Comment.create! :resource => tag, - :body => "Another Comment", - :namespace => namespace_name + comment = ActiveAdmin::Comment.create!( + author: user, + resource: tag, + body: "Another Comment", + namespace: namespace_name) - ActiveAdmin::Comment.find_for_resource_in_namespace(tag, namespace_name).should == [comment] + expect(ActiveAdmin::Comment.find_for_resource_in_namespace(tag, namespace_name)).to eq [comment] + end + end + + describe "commenting on child of STI resource" do + let(:publisher) { Publisher.create!(username: "tenderlove") } + let(:namespace_name) { "admin" } + + it "should assign child class as commented resource" do + ActiveAdmin::Comment.create!( + author: user, + resource: publisher, + body: "Lorem Ipsum", + namespace: namespace_name) + + expect(ActiveAdmin::Comment.find_for_resource_in_namespace(publisher, namespace_name).last.resource_type). + to eq("User") end end end describe ActiveAdmin::Comments::NamespaceHelper do describe "#comments?" do - it "should have comments when the namespace allows comments" do ns = ActiveAdmin::Namespace.new(application, :admin) - ns.allow_comments = true - ns.comments?.should be_true + ns.comments = true + expect(ns.comments?).to eq true end it "should not have comments when the namespace does not allow comments" do ns = ActiveAdmin::Namespace.new(application, :admin) - ns.allow_comments = false - ns.comments?.should be_false + ns.comments = false + expect(ns.comments?).to eq false end - end end @@ -92,15 +198,16 @@ it "should add an attr_accessor :comments to ActiveAdmin::Resource" do ns = ActiveAdmin::Namespace.new(application, :admin) resource = ActiveAdmin::Resource.new(ns, Post) - resource.comments.should be_nil + expect(resource.comments).to eq nil resource.comments = true - resource.comments.should be_true + expect(resource.comments).to eq true end + it "should disable comments if set to false" do ns = ActiveAdmin::Namespace.new(application, :admin) resource = ActiveAdmin::Resource.new(ns, Post) resource.comments = false - resource.comments?.should be_false + expect(resource.comments?).to eq false end end end diff --git a/spec/unit/component_spec.rb b/spec/unit/component_spec.rb index 7ab697d9eba..83ecc88f48a 100644 --- a/spec/unit/component_spec.rb +++ b/spec/unit/component_spec.rb @@ -1,18 +1,19 @@ -require 'spec_helper' +# frozen_string_literal: true +require "rails_helper" -class MockComponentClass < ActiveAdmin::Component; end - -describe ActiveAdmin::Component do - - let(:component_class){ MockComponentClass } - let(:component){ component_class.new } +RSpec.describe ActiveAdmin::Component do + let(:component_class) { Class.new(described_class) } + let(:component) { component_class.new } it "should be a subclass of an html div" do - ActiveAdmin::Component.ancestors.should include(Arbre::HTML::Div) + expect(ActiveAdmin::Component.ancestors).to include(Arbre::HTML::Div) end it "should render to a div, even as a subclass" do - component.tag_name.should == 'div' + expect(component.tag_name).to eq "div" end + it "should not have a CSS class name by default" do + expect(component.class_list.empty?).to eq true + end end diff --git a/spec/unit/config_shared_examples.rb b/spec/unit/config_shared_examples.rb index 26ca8fac72f..a9065bea2ee 100644 --- a/spec/unit/config_shared_examples.rb +++ b/spec/unit/config_shared_examples.rb @@ -1,13 +1,14 @@ -shared_examples_for "ActiveAdmin::Config" do +# frozen_string_literal: true +RSpec.shared_examples_for "ActiveAdmin::Resource" do describe "namespace" do it "should return the namespace" do - config.namespace.should == namespace + expect(config.namespace).to eq(namespace) end end describe "page_presenters" do it "should return an empty hash by default" do - config.page_presenters.should == {} + expect(config.page_presenters).to eq({}) end end @@ -22,38 +23,35 @@ describe "Naming" do it "implements #resource_label" do - expect { config.resource_label }.should_not raise_error + expect { config.resource_label }.to_not raise_error end it "implements #plural_resource_label" do - expect { config.plural_resource_label }.should_not raise_error + expect { config.plural_resource_label }.to_not raise_error end end describe "Menu" do describe "#menu_item_options" do - it "initializes a new menu item with defaults" do - config.menu_item_options[:label].call.should == config.plural_resource_label + expect(config.menu_item_options[:label].call).to eq(config.plural_resource_label) end it "initialize a new menu item with custom options" do - config.menu_item_options = { :label => "Hello" } - config.menu_item_options[:label].should == "Hello" + config.menu_item_options = { label: "Hello" } + expect(config.menu_item_options[:label]).to eq("Hello") end - end describe "#include_in_menu?" do it "should be included in menu by default" do - config.include_in_menu?.should == true + expect(config.include_in_menu?).to eq(true) end it "should not be included in menu when menu set to false" do config.menu_item_options = false - config.include_in_menu?.should == false + expect(config.include_in_menu?).to eq(false) end end - end end diff --git a/spec/unit/controller_filters_spec.rb b/spec/unit/controller_filters_spec.rb index 661fe27f07e..c5d4e852d10 100644 --- a/spec/unit/controller_filters_spec.rb +++ b/spec/unit/controller_filters_spec.rb @@ -1,27 +1,28 @@ -require 'spec_helper' +# frozen_string_literal: true +require "rails_helper" -describe ActiveAdmin::Application do - let(:application){ ActiveAdmin::Application.new } - let(:controllers){ [ActiveAdmin::BaseController, ActiveAdmin::Devise::SessionsController, - ActiveAdmin::Devise::UnlocksController, ActiveAdmin::Devise::PasswordsController] } +RSpec.describe ActiveAdmin::Application do + let(:application) { ActiveAdmin::Application.new } + let(:controllers) { application.controllers_for_filters } - it 'before_filter' do - controllers.each{ |c| c.should_receive(:before_filter).and_return(true) } - application.before_filter :my_filter, :only => :show + it "controllers_for_filters" do + expect(application.controllers_for_filters).to eq [ + ActiveAdmin::BaseController, ActiveAdmin::Devise::SessionsController, + ActiveAdmin::Devise::PasswordsController, ActiveAdmin::Devise::UnlocksController, + ActiveAdmin::Devise::RegistrationsController, ActiveAdmin::Devise::ConfirmationsController + ] end - it 'skip_before_filter' do - controllers.each{ |c| c.should_receive(:skip_before_filter).and_return(true) } - application.skip_before_filter :my_filter, :only => :show - end - - it 'after_filter' do - controllers.each{ |c| c.should_receive(:after_filter).and_return(true) } - application.after_filter :my_filter, :only => :show - end - - it 'around_filter' do - controllers.each{ |c| c.should_receive(:around_filter).and_return(true) } - application.around_filter :my_filter, :only => :show + %w[ + skip_before_action skip_around_action skip_after_action + append_before_action append_around_action append_after_action + prepend_before_action prepend_around_action prepend_after_action + before_action around_action after_action + ].each do |filter| + it filter do + args = [:my_filter, { only: :show }] + controllers.each { |c| expect(c).to receive(filter).with(args) } + application.public_send filter, args + end end end diff --git a/spec/unit/csv_builder_spec.rb b/spec/unit/csv_builder_spec.rb index e8adffe2ad1..95c3bd70854 100644 --- a/spec/unit/csv_builder_spec.rb +++ b/spec/unit/csv_builder_spec.rb @@ -1,32 +1,57 @@ -require 'spec_helper' +# frozen_string_literal: true +require "rails_helper" -describe ActiveAdmin::CSVBuilder do +RSpec.describe ActiveAdmin::CSVBuilder do + describe ".default_for_resource using Post" do + let(:application) { ActiveAdmin::Application.new } + let(:namespace) { ActiveAdmin::Namespace.new(application, :admin) } + let(:resource) { ActiveAdmin::Resource.new(namespace, Post, {}) } + let(:csv_builder) { ActiveAdmin::CSVBuilder.default_for_resource(resource).tap(&:exec_columns) } - describe '.default_for_resource using Post' do - let(:csv_builder) { ActiveAdmin::CSVBuilder.default_for_resource(Post) } - - it "should return a default csv_builder for Post" do - csv_builder.should be_a(ActiveAdmin::CSVBuilder) + it "returns a default csv_builder for Post" do + expect(csv_builder).to be_a(ActiveAdmin::CSVBuilder) end - specify "the first column should be Id" do - csv_builder.columns.first.name.should == 'Id' - csv_builder.columns.first.data.should == :id + it "defines Id as the first column" do + expect(csv_builder.columns.first.name).to eq "Id" + expect(csv_builder.columns.first.data).to eq :id end - specify "the following columns should be content_column" do + it "has Post's content_columns" do csv_builder.columns[1..-1].each_with_index do |column, index| - column.name.should == Post.content_columns[index].name.titleize - column.data.should == Post.content_columns[index].name.to_sym + expect(column.name).to eq resource.content_columns[index].to_s.humanize + expect(column.data).to eq resource.content_columns[index] + end + end + + context "when column has a localized name" do + let(:localized_name) { "Titulo" } + + before do + allow(Post).to receive(:human_attribute_name).and_call_original + allow(Post).to receive(:human_attribute_name).with(:title) { localized_name } + end + + it "gets name from I18n" do + title_index = resource.content_columns.index(:title) + 1 # First col is always id + expect(csv_builder.columns[title_index].name).to eq localized_name + end + end + + context "for models having sensitive attributes" do + let(:resource) { ActiveAdmin::Resource.new(namespace, User, {}) } + + it "omits sensitive fields" do + expect(csv_builder.columns.map(&:data)).to_not include :encrypted_password end end end - context 'when empty' do - let(:builder){ ActiveAdmin::CSVBuilder.new } + context "when empty" do + let(:builder) { ActiveAdmin::CSVBuilder.new.tap(&:exec_columns) } it "should have no columns" do - builder.columns.should == [] + expect(builder.columns).to eq [] end end @@ -34,22 +59,22 @@ let(:builder) do ActiveAdmin::CSVBuilder.new do column :title - end + end.tap(&:exec_columns) end it "should have one column" do - builder.columns.size.should == 1 + expect(builder.columns.size).to eq 1 end describe "the column" do - let(:column){ builder.columns.first } + let(:column) { builder.columns.first } it "should have a name of 'Title'" do - column.name.should == "Title" + expect(column.name).to eq "Title" end it "should have the data :title" do - column.data.should == :title + expect(column.data).to eq :title end end end @@ -60,44 +85,244 @@ column "My title" do # nothing end - end + end.tap(&:exec_columns) end it "should have one column" do - builder.columns.size.should == 1 + expect(builder.columns.size).to eq 1 end describe "the column" do - let(:column){ builder.columns.first } + let(:column) { builder.columns.first } it "should have a name of 'My title'" do - column.name.should == "My title" + expect(column.name).to eq "My title" end it "should have the data :title" do - column.data.should be_an_instance_of(Proc) + expect(column.data).to be_an_instance_of(Proc) + end + end + end + + context "with a humanize_name column option" do + context "with symbol column name" do + let(:builder) do + ActiveAdmin::CSVBuilder.new do + column :my_title, humanize_name: false + end.tap(&:exec_columns) + end + + describe "the column" do + let(:column) { builder.columns.first } + + it "should have a name of 'my_title'" do + expect(column.name).to eq "my_title" + end + end + end + + context "with string column name" do + let(:builder) do + ActiveAdmin::CSVBuilder.new do + column "my_title", humanize_name: false + end.tap(&:exec_columns) + end + + describe "the column" do + let(:column) { builder.columns.first } + + it "should have a name of 'my_title'" do + expect(column.name).to eq "my_title" + end end end end context "with a separator" do let(:builder) do - ActiveAdmin::CSVBuilder.new :col_sep => ";" + ActiveAdmin::CSVBuilder.new(col_sep: ";").tap(&:exec_columns) end it "should have proper separator" do - builder.options.should == {:col_sep => ";"} + expect(builder.options).to include(col_sep: ";") + end + end + + context "with humanize_name option" do + let(:builder) do + ActiveAdmin::CSVBuilder.new(humanize_name: false) do + column :my_title + end.tap(&:exec_columns) + end + + describe "the column" do + let(:column) { builder.columns.first } + + it "should have humanize_name option set" do + expect(column.options).to eq humanize_name: false + end + + it "should have a name of 'my_title'" do + expect(column.name).to eq "my_title" + end end end context "with csv_options" do let(:builder) do - ActiveAdmin::CSVBuilder.new :force_quotes => true + ActiveAdmin::CSVBuilder.new(force_quotes: true).tap(&:exec_columns) end it "should have proper separator" do - builder.options.should == {:force_quotes => true} + expect(builder.options).to include(force_quotes: true) end end + context "with access to the controller" do + let(:dummy_view_context) { double(controller: dummy_controller) } + let(:dummy_controller) { double(names: %w(title summary updated_at created_at)) } + let(:builder) do + ActiveAdmin::CSVBuilder.new do + column "id" + controller.names.each do |name| + column(name) + end + end.tap { |b| b.exec_columns(dummy_view_context) } + end + + it "should build columns provided by the controller" do + expect(builder.columns.map(&:data)).to match_array([:id, :title, :summary, :updated_at, :created_at]) + end + end + + context "build csv using the supplied order" do + before do + @post1 = Post.create!(title: "Hello1", published_date: Date.today - 2.day) + @post2 = Post.create!(title: "Hello2", published_date: Date.today - 1.day) + end + let(:dummy_controller) do + class DummyController + def in_paginated_batches(&block) + Post.order("published_date DESC").each(&block) + end + + def apply_decorator(resource) + resource + end + + def view_context + end + end + DummyController.new + end + let(:builder) do + ActiveAdmin::CSVBuilder.new do + column "id" + column "title" + column "published_date" + end + end + + it "should generate data with the supplied order" do + expect(builder).to receive(:build_row).and_return([]).once.ordered { |post| expect(post.id).to eq @post2.id } + expect(builder).to receive(:build_row).and_return([]).once.ordered { |post| expect(post.id).to eq @post1.id } + builder.build dummy_controller, [] + end + end + + context "build csv using specified encoding and encoding_options" do + let(:dummy_controller) do + class DummyController + def in_paginated_batches(&block) + Post.all.each(&block) + end + + def view_context + end + end + DummyController.new + end + let(:builder) do + ActiveAdmin::CSVBuilder.new(encoding: encoding, encoding_options: opts) do + column "おはようございます" + column "title" + end + end + + context "Shift-JIS with options" do + let(:encoding) { Encoding::Shift_JIS } + let(:opts) { { invalid: :replace, undef: :replace, replace: "?" } } + + it "encodes the CSV" do + receiver = [] + builder.build dummy_controller, receiver + line = receiver.last + expect(line.encoding).to eq(encoding) + end + end + + context "ASCII with options" do + let(:encoding) { Encoding::ASCII } + let(:opts) do + { invalid: :replace, undef: :replace, replace: "__REPLACED__" } + end + + it "encodes the CSV without errors" do + receiver = [] + builder.build dummy_controller, receiver + line = receiver.last + expect(line.encoding).to eq(encoding) + expect(line).to include("__REPLACED__") + end + end + end + + context 'csv injection' do + let(:dummy_controller) do + class DummyController + def in_paginated_batches(&block) + Post.all.each(&block) + end + + def view_context + MethodOrProcHelper + end + end + DummyController.new + end + + let(:builder) do + ActiveAdmin::CSVBuilder.new do + column(:id) + column(:title) + end + end + + ['=', '+', '-', '@', "\t", "\r"].each do |char| + it "prepends a single quote when column starts with a #{char} character" do + attack = "#{char}1+2" + + escaped_attack = "'#{attack}" + escaped_attack = "\"#{escaped_attack}\"" if char == "\r" + + post = Post.create!(title: attack) + receiver = [] + builder.build dummy_controller, receiver + line = receiver.last + expect(line).to eq "#{post.id},#{escaped_attack}\n" + end + + it "accounts for the field separator when character #{char} is used to inject a formula" do + attack = "#{char}1+2'\" ;,#{char}1+2" + escaped_attack = "\"'#{attack.gsub('"', '""')}\"" + + post = Post.create!(title: attack) + receiver = [] + builder.build dummy_controller, receiver + line = receiver.last + expect(line).to eq "#{post.id},#{escaped_attack}\n" + end + end + end end diff --git a/spec/unit/dependency_spec.rb b/spec/unit/dependency_spec.rb new file mode 100644 index 00000000000..80f5dada301 --- /dev/null +++ b/spec/unit/dependency_spec.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe ActiveAdmin::Dependency do + describe "method_missing" do + before do + allow(Gem).to receive(:loaded_specs) + .and_return "foo" => Gem::Specification.new("foo", "1.2.3") + end + + it "returns a Matcher" do + expect(described_class.foo).to be_a ActiveAdmin::Dependency::Matcher + expect(described_class.foo.inspect).to eq "" + expect(described_class.bar.inspect).to eq "" + end + + describe "`?`" do + it "base" do + expect(described_class.foo?).to eq true + expect(described_class.bar?).to eq false + end + + it "=" do + expect(described_class.foo? "= 1.2.3").to eq true + expect(described_class.foo? "= 1").to eq false + end + + it ">" do + expect(described_class.foo? "> 1").to eq true + expect(described_class.foo? "> 2").to eq false + end + + it "<" do + expect(described_class.foo? "< 2").to eq true + expect(described_class.foo? "< 1").to eq false + end + + it ">=" do + expect(described_class.foo? ">= 1.2.3").to eq true + expect(described_class.foo? ">= 1.2.2").to eq true + expect(described_class.foo? ">= 1.2.4").to eq false + end + + it "<=" do + expect(described_class.foo? "<= 1.2.3").to eq true + expect(described_class.foo? "<= 1.2.4").to eq true + expect(described_class.foo? "<= 1.2.2").to eq false + end + + it "~>" do + expect(described_class.foo? "~> 1.2.0").to eq true + expect(described_class.foo? "~> 1.1").to eq true + expect(described_class.foo? "~> 1.2.4").to eq false + end + end + + describe "`!`" do + it "raises an error if requirement not met" do + expect { described_class.foo! "5" } + .to raise_error(ActiveAdmin::DependencyError, "You provided foo 1.2.3 but we need: 5.") + end + + it "accepts multiple arguments" do + expect { described_class.foo! "> 1", "< 1.2" } + .to raise_error(ActiveAdmin::DependencyError, "You provided foo 1.2.3 but we need: > 1, < 1.2.") + end + + it "raises an error if not provided" do + expect { described_class.bar! } + .to raise_error(ActiveAdmin::DependencyError, "To use bar you need to specify it in your Gemfile.") + end + end + end + + describe "[]" do + before do + allow(Gem).to receive(:loaded_specs) + .and_return "a-b" => Gem::Specification.new("a-b", "1.2.3") + end + + it "allows access to gems with an arbitrary name" do + expect(described_class["a-b"]).to be_a ActiveAdmin::Dependency::Matcher + expect(described_class["a-b"].inspect).to eq "" + expect(described_class["c-d"].inspect).to eq "" + end + + # Note: more extensive tests for match? and match! are above. + + it "match?" do + expect(described_class["a-b"].match?).to eq true + expect(described_class["a-b"].match? "1.2.3").to eq true + expect(described_class["b-c"].match?).to eq false + end + + it "match!" do + expect(described_class["a-b"].match!).to eq nil + expect(described_class["a-b"].match! "1.2.3").to eq nil + + expect { described_class["a-b"].match! "2.5" } + .to raise_error(ActiveAdmin::DependencyError, "You provided a-b 1.2.3 but we need: 2.5.") + + expect { described_class["b-c"].match! } + .to raise_error(ActiveAdmin::DependencyError, "To use b-c you need to specify it in your Gemfile.") + end + + # Note: Ruby comparison operators are separate from the `foo? '> 1'` syntax + + describe "Ruby comparison syntax" do + it "==" do + expect(described_class["a-b"] == "1.2.3").to eq true + expect(described_class["a-b"] == "1.2").to eq false + expect(described_class["a-b"] == 1).to eq false + end + + it ">" do + expect(described_class["a-b"] > 1).to eq true + expect(described_class["a-b"] > 2).to eq false + end + + it "<" do + expect(described_class["a-b"] < 2).to eq true + expect(described_class["a-b"] < 1).to eq false + end + + it ">=" do + expect(described_class["a-b"] >= "1.2.3").to eq true + expect(described_class["a-b"] >= "1.2.2").to eq true + expect(described_class["a-b"] >= "1.2.4").to eq false + end + + it "<=" do + expect(described_class["a-b"] <= "1.2.3").to eq true + expect(described_class["a-b"] <= "1.2.4").to eq true + expect(described_class["a-b"] <= "1.2.2").to eq false + end + + it "throws a custom error if the gem is missing" do + expect { described_class["b-c"] < 23 } + .to raise_error(ActiveAdmin::DependencyError, "To use b-c you need to specify it in your Gemfile.") + end + end + end +end diff --git a/spec/unit/devise_spec.rb b/spec/unit/devise_spec.rb index 0f6e4508006..4f6575543d0 100644 --- a/spec/unit/devise_spec.rb +++ b/spec/unit/devise_spec.rb @@ -1,7 +1,7 @@ -require 'spec_helper' - -describe ActiveAdmin::Devise::Controller do +# frozen_string_literal: true +require "rails_helper" +RSpec.describe ActiveAdmin::Devise::Controller do let(:controller_class) do klass = Class.new do def self.layout(*); end @@ -12,40 +12,49 @@ def self.helper(*); end end let(:controller) { controller_class.new } + let(:action_controller_config) { Rails.configuration.action_controller } + + def with_temp_relative_url_root(relative_url_root) + previous_relative_url_root = action_controller_config[:relative_url_root] + action_controller_config[:relative_url_root] = relative_url_root - context 'with a RAILS_RELATIVE_URL_ROOT set' do + yield + ensure + action_controller_config[:relative_url_root] = previous_relative_url_root + end - before { Rails.configuration.action_controller[:relative_url_root] = '/foo' } + context "with a RAILS_RELATIVE_URL_ROOT set" do + around do |example| + with_temp_relative_url_root("/foo") { example.call } + end it "should set the root path to the default namespace" do - controller.root_path.should == "/foo/admin" + expect(controller.root_path).to eq "/foo/admin" end it "should set the root path to '/' when no default namespace" do - ActiveAdmin.application.stub!(:default_namespace => false) - controller.root_path.should == "/foo/" + allow(ActiveAdmin.application).to receive(:default_namespace).and_return(false) + expect(controller.root_path).to eq "/foo/" end - end - context 'without a RAILS_RELATIVE_URL_ROOT set' do - - before { Rails.configuration.action_controller[:relative_url_root] = nil } + context "without a RAILS_RELATIVE_URL_ROOT set" do + around do |example| + with_temp_relative_url_root(nil) { example.call } + end it "should set the root path to the default namespace" do - controller.root_path.should == "/admin" + expect(controller.root_path).to eq "/admin" end it "should set the root path to '/' when no default namespace" do - ActiveAdmin.application.stub!(:default_namespace => false) - controller.root_path.should == "/" + allow(ActiveAdmin.application).to receive(:default_namespace).and_return(false) + expect(controller.root_path).to eq "/" end - end context "within a scoped route" do - - SCOPE = '/aa_scoped' + SCOPE = "/aa_scoped" before do # Remove existing routes @@ -54,7 +63,7 @@ def self.helper(*); end # Add scoped routes routes.draw do - scope :path => SCOPE do + scope path: SCOPE do ActiveAdmin.routes(self) devise_for :admin_users, ActiveAdmin::Devise.config end @@ -67,50 +76,7 @@ def self.helper(*); end end it "should include scope path in root_path" do - controller.root_path.should == "#{SCOPE}/admin" + expect(controller.root_path).to eq "#{SCOPE}/admin" end - end - - describe "#config" do - let(:config) { ActiveAdmin::Devise.config } - - describe ":sign_out_via option" do - - subject { config[:sign_out_via] } - - context "when Devise does not implement sign_out_via (version < 1.2)" do - before do - ::Devise.should_receive(:respond_to?).with(:sign_out_via).and_return(false) - end - - it "should not contain any customization for sign_out_via" do - config.should_not have_key(:sign_out_via) - end - end - - context "when Devise implements sign_out_via (version >= 1.2)" do - before do - ::Devise.should_receive(:respond_to?).with(:sign_out_via).and_return(true) - ::Devise.stub!(:sign_out_via) { :delete } - end - - it "should contain the application.logout_link_method" do - ::Devise.should_receive(:sign_out_via).and_return(:delete) - ActiveAdmin.application.should_receive(:logout_link_method).and_return(:get) - - config[:sign_out_via].should include(:get) - end - - it "should contain Devise's logout_via_method(s)" do - ::Devise.should_receive(:sign_out_via).and_return([:delete, :post]) - ActiveAdmin.application.should_receive(:logout_link_method).and_return(:get) - - config[:sign_out_via].should == [:delete, :post, :get] - end - end - - end # describe ":sign_out_via option" - end # describe "#config" - end diff --git a/spec/unit/dsl_spec.rb b/spec/unit/dsl_spec.rb index 2b048bf8a2b..869b05ac9ac 100644 --- a/spec/unit/dsl_spec.rb +++ b/spec/unit/dsl_spec.rb @@ -1,61 +1,96 @@ -require 'spec_helper' - +# frozen_string_literal: true +require "rails_helper" module MockModuleToInclude def self.included(dsl) end end -describe ActiveAdmin::DSL do - - let(:config){ mock } +RSpec.describe ActiveAdmin::DSL do let(:application) { ActiveAdmin::Application.new } let(:namespace) { ActiveAdmin::Namespace.new application, :admin } - let(:resource_config) { ActiveAdmin::Resource.new namespace, Post } - let(:dsl){ ActiveAdmin::DSL.new(config) } + let(:resource_config) { namespace.register Post } + let(:dsl) { ActiveAdmin::DSL.new(resource_config) } describe "#include" do - it "should call the included class method on the module that is included" do - MockModuleToInclude.should_receive(:included).with(dsl) + expect(MockModuleToInclude).to receive(:included).with(dsl) dsl.run_registration_block do include MockModuleToInclude end end + end + + describe "#action_item" do + before do + @default_items_count = resource_config.action_items.size + + dsl.run_registration_block do + action_item :awesome, only: :show do + "Awesome ActionItem" + end + end + end + it "adds action_item to the action_items of config" do + expect(resource_config.action_items.size).to eq(@default_items_count + 1) + end end describe "#menu" do - it "should set the menu_item_options on the configuration" do - config.should_receive(:menu_item_options=).with({:parent => "Admin"}) + expect(resource_config).to receive(:menu_item_options=).with({ parent: "Admin" }) dsl.run_registration_block do - menu :parent => "Admin" + menu parent: "Admin" end end - end describe "#navigation_menu" do - it "should set the navigation_menu_name on the configuration" do - config.should_receive(:navigation_menu_name=).with(:admin) + expect(resource_config).to receive(:navigation_menu_name=).with(:admin) dsl.run_registration_block do navigation_menu :admin end end it "should accept a block" do - dsl = ActiveAdmin::DSL.new(resource_config) dsl.run_registration_block do navigation_menu { :dynamic_menu } end + expect(resource_config.navigation_menu_name).to eq :dynamic_menu + end + end - resource_config.navigation_menu_name.should == :dynamic_menu - + describe "#sidebar" do + before do + dsl.config.sidebar_sections << ActiveAdmin::SidebarSection.new(:email) end + it "add sidebar_section to the sidebar_sections of config" do + dsl.run_registration_block do + sidebar :help + end + expect(dsl.config.sidebar_sections.map(&:name)).to match_array ["filters", "active_search", "email", "help"] + end end + describe "#batch_action" do + it "should add a batch action by symbol" do + dsl.run_registration_block do + config.batch_actions = true + batch_action :foo + end + expect(resource_config.batch_actions.map(&:sym)).to eq [:foo, :destroy] + end + + it "should add a batch action by title" do + dsl.run_registration_block do + config.batch_actions = true + batch_action "foo bar" + end + expect(resource_config.batch_actions.map(&:sym)).to eq [:foo_bar, :destroy] + end + end end diff --git a/spec/unit/dynamic_settings_spec.rb b/spec/unit/dynamic_settings_spec.rb new file mode 100644 index 00000000000..ae46bb270e9 --- /dev/null +++ b/spec/unit/dynamic_settings_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe ActiveAdmin::DynamicSettingsNode do + subject { ActiveAdmin::DynamicSettingsNode.build } + + context "StringSymbolOrProcSetting" do + before { subject.register :foo, "bar", :string_symbol_or_proc } + + it "should pass through a string" do + subject.foo = "string" + expect(subject.foo(self)).to eq "string" + end + + it "should instance_exec if context given" do + ctx = Hash[i: 42] + subject.foo = proc { self[:i] += 1 } + expect(subject.foo(ctx)).to eq 43 + expect(subject.foo(ctx)).to eq 44 + end + + it "should send message if symbol given" do + ctx = double + expect(ctx).to receive(:quux).and_return "qqq" + subject.foo = :quux + expect(subject.foo(ctx)).to eq "qqq" + end + end +end diff --git a/spec/unit/event_spec.rb b/spec/unit/event_spec.rb deleted file mode 100644 index 19339a1fa0f..00000000000 --- a/spec/unit/event_spec.rb +++ /dev/null @@ -1,47 +0,0 @@ -require 'spec_helper' -require 'active_admin/event' - -describe ActiveAdmin::EventDispatcher do - - let(:test_event){ 'active_admin.test_event' } - let(:dispatcher){ ActiveAdmin::EventDispatcher.new } - - it "should add a subscriber for an event" do - dispatcher.subscribers(test_event).size.should == 0 - dispatcher.subscribe(test_event){ true } - dispatcher.subscribers(test_event).size.should == 1 - end - - it "should add a subscriber for multiple events" do - dispatcher.subscribe(test_event, test_event + "1"){ true } - dispatcher.subscribers(test_event).size.should == 1 - dispatcher.subscribers(test_event + "1").size.should == 1 - end - - it "should call the dispatch block with no arguments" do - dispatcher.subscribe(test_event){ raise StandardError, "From Event Handler" } - lambda { - dispatcher.dispatch(test_event) - }.should raise_error(StandardError, "From Event Handler") - end - - it "should call the dispatch block with one argument" do - arg = nil - dispatcher.subscribe(test_event){|passed_in| arg = passed_in } - dispatcher.dispatch(test_event, "My Arg") - arg.should == "My Arg" - end - - it "should clear all subscribers" do - dispatcher.subscribe(test_event){ false } - dispatcher.subscribe(test_event + "_2"){ false } - dispatcher.clear_all_subscribers! - dispatcher.subscribers(test_event).size.should == 0 - dispatcher.subscribers(test_event + "_2").size.should == 0 - end - - it "should have a dispatcher available from ActiveAdmin::Event" do - ActiveAdmin::Event.should be_an_instance_of(ActiveAdmin::EventDispatcher) - end - -end diff --git a/spec/unit/filters/active_filter_spec.rb b/spec/unit/filters/active_filter_spec.rb new file mode 100644 index 00000000000..f9012e05381 --- /dev/null +++ b/spec/unit/filters/active_filter_spec.rb @@ -0,0 +1,257 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe ActiveAdmin::Filters::ActiveFilter do + let(:namespace) do + ActiveAdmin::Namespace.new(ActiveAdmin::Application.new, :admin) + end + + let(:resource) do + namespace.register(Post) + end + + let(:user) { User.create! first_name: "John", last_name: "Doe" } + let(:category) { Category.create! name: "Category" } + let(:post) { Post.create! title: "Hello World", category: category, author: user } + + let(:search) do + Post.ransack(title_eq: post.title) + end + + let(:condition) do + search.conditions[0] + end + + subject do + ActiveAdmin::Filters::ActiveFilter.new(resource, condition) + end + + it "should have valid values" do + expect(subject.values).to eq([post.title]) + end + + describe "label" do + context "by default" do + it "should have valid label" do + expect(subject.label).to eq("Title equals") + end + end + + context "with formtastic translations" do + it "should pick up formtastic label" do + with_translation %i[formtastic labels title], "Supertitle" do + expect(subject.label).to eq("Supertitle equals") + end + end + end + end + + it "should pick predicate name translation" do + expect(subject.predicate_name).to eq(I18n.t("ransack.predicates.eq")) + end + + context "search by belongs_to association" do + let(:search) do + Post.ransack(custom_category_id_eq: category.id) + end + + it "should have valid values" do + expect(subject.values[0]).to be_a(Category) + end + + it "should have valid label" do + expect(subject.label).to eq("Category equals") + end + + it "should pick predicate name translation" do + expect(subject.predicate_name).to eq(Ransack::Translate.predicate("eq")) + end + end + + context "search by polymorphic association" do + let(:resource) do + namespace.register(ActiveAdmin::Comment) + end + + let(:search) do + ActiveAdmin::Comment.ransack(resource_id_eq: post.id, resource_type_eq: post.class.to_s) + end + + context "id filter" do + let(:condition) do + search.conditions[0] + end + it "should have valid values" do + expect(subject.values[0]).to eq(post.id) + end + + it "should have valid label" do + expect(subject.label).to eq("Resource equals") + end + end + + context "type filter" do + let(:condition) do + search.conditions[1] + end + + it "should have valid values" do + expect(subject.values[0]).to eq(post.class.to_s) + end + + it "should have valid label" do + expect(subject.label).to eq("Resource type equals") + end + end + end + + context "search by has many association" do + let(:resource) do + namespace.register(Category) + end + + let(:search) do + Category.ransack(posts_id_eq: post.id) + end + + it "should have valid values" do + expect(subject.values[0]).to be_a(Post) + end + + it "should have valid label" do + expect(subject.label).to eq("Post equals") + end + + context "search by has many through association" do + let(:resource) do + namespace.register(User) + end + + let(:search) do + User.ransack(posts_category_id_eq: category.id) + end + + it "should have valid values" do + expect(subject.values[0]).to be_a(Category) + end + + it "should have valid label" do + expect(subject.label).to eq("Category equals") + end + end + end + + context "search has no matching records" do + let(:search) { Post.ransack(author_id_eq: "foo") } + + it "should not produce and error" do + expect { subject.values }.not_to raise_error + end + + it "should return an enumerable" do + expect(subject.values).to respond_to(:map) + end + end + + context "a label is set on the filter" do + it "should use the filter label as the label prefix" do + label = "#{user.first_name}'s Post Title" + resource.add_filter(:title, label: label) + + expect(subject.label).to eq("#{label} equals") + end + + it "should use the filter label as the label prefix" do + label = proc { "#{user.first_name}'s Post Title" } + resource.add_filter(:title, label: label) + + expect(subject.label).to eq("#{label.call} equals") + end + + context "when filter condition has a predicate" do + let(:search) do + Post.ransack(title_cont: "Hello") + end + + it "should use the filter label as the label prefix" do + label = "#{user.first_name}'s Post" + resource.add_filter(:title_cont, label: label) + expect(subject.label).to eq("#{label} contains") + end + end + + context "when filter condition has multiple fields" do + let(:search) do + Post.ransack(title_or_body_cont: "Hello World") + end + + it "should use the filter label as the label prefix" do + label = "#{user.first_name}'s Post" + resource.add_filter(:title_or_body_cont, label: label) + expect(subject.label).to eq("#{label} contains") + end + end + end + + context "the association uses a different primary_key than the related class' primary_key" do + let(:resource_klass) do + Class.new(Post) do + belongs_to :kategory, class_name: "Category", primary_key: :name, foreign_key: :title + + def self.name + "SuperPost" + end + end + end + + let(:resource) do + namespace.register(resource_klass) + end + + let(:user) { User.create! first_name: "John", last_name: "Doe" } + let!(:category) { Category.create! name: "Category" } + + let(:post) { resource_klass.create! title: "Category", author: user } + + let(:search) do + resource_klass.ransack(title_eq: post.title) + end + + it "should use the association's primary key to find the associated record" do + stub_const("::SuperPost", resource_klass) + + resource.add_filter(:kategory) + + expect(subject.values.first).to eq category + end + end + + context "when the resource has a custom primary key" do + let(:resource_klass) do + Class.new(Store) do + self.primary_key = "name" + belongs_to :user + + def self.name + "SubStore" + end + end + end + + let(:resource) do + namespace.register(resource_klass) + end + + let(:user) { User.create! first_name: "John", last_name: "Doe" } + + let(:search) do + resource_klass.ransack(user_id_eq: user.id) + end + + it "should use the association's primary key to find the associated record" do + stub_const("::#{resource_klass.name}", resource_klass) + + expect(subject.values.first).to eq user + end + end +end diff --git a/spec/unit/filters/active_spec.rb b/spec/unit/filters/active_spec.rb new file mode 100644 index 00000000000..f823fb02205 --- /dev/null +++ b/spec/unit/filters/active_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe ActiveAdmin::Filters::Active do + let(:resource) do + namespace = ActiveAdmin::Namespace.new(ActiveAdmin::Application.new, :admin) + namespace.register(Post) + end + + subject { described_class.new(resource, search) } + + let(:params) do + ::ActionController::Parameters.new(q: { author_id_eq: 1 }) + end + + let(:search) do + Post.ransack(params[:q]) + end + + it "should have filters" do + expect(subject.filters.size).to eq(1) + end +end diff --git a/spec/unit/filters/filter_form_builder_spec.rb b/spec/unit/filters/filter_form_builder_spec.rb deleted file mode 100644 index 452fc31ca96..00000000000 --- a/spec/unit/filters/filter_form_builder_spec.rb +++ /dev/null @@ -1,338 +0,0 @@ -require 'spec_helper' - -describe ActiveAdmin::Filters::ViewHelper do - - # Setup an ActionView::Base object which can be used for - # generating the form for. - let(:helpers) do - view = action_view - def view.collection_path - "/posts" - end - - def view.protect_against_forgery? - false - end - - def view.a_helper_method - "A Helper Method" - end - - view - end - - def render_filter(search, filters) - render_arbre_component({:filter_args => [search, filters]}, helpers) do - text_node active_admin_filters_form_for(*assigns[:filter_args]) - end - end - - def filter(name, options = {}) - render_filter scope, @filters.push(options.merge(:attribute => name)) - end - - let(:scope) { Post.search } - - before(:each) { @filters = [] } - - - describe "the form in general" do - let(:body) { filter :title } - - it "should generate a form which submits via get" do - body.should have_tag("form", :attributes => { :method => 'get', :class => 'filter_form' }) - end - - it "should generate a filter button" do - body.should have_tag("input", :attributes => { :type => "submit", - :value => "Filter" }) - end - - it "should only generate the form once" do - body.to_s.scan(/q\[title_contains\]/).size.should == 1 - end - - it "should generate a clear filters link" do - body.should have_tag("a", "Clear Filters", :attributes => { :class => "clear_filters_btn" }) - end - end - - describe "string attribute" do - let(:body) { filter :title } - - it "should generate a select option for starts with" do - body.should have_tag("option", "Starts with", :attributes => { :value => 'title_starts_with' }) - end - - it "should generate a select option for ends with" do - body.should have_tag("option", "Ends with", :attributes => { :value => 'title_ends_with' }) - end - - it "should generate a select option for contains" do - body.should have_tag("option", "Contains", :attributes => { :value => 'title_contains' }) - end - - it "should generate a text field for input" do - body.should have_tag("input", :attributes => { :name => 'q[title_contains]' }) - end - - it "should have a proper label" do - body.should have_tag('label', 'Title') - end - - it "should translate the label for text field" do - begin - I18n.backend.store_translations(:en, :activerecord => { :attributes => { :post => { :title => "Name" } } }) - body.should have_tag('label', 'Name') - ensure - I18n.backend.reload! - end - end - - it "should select the option which is currently being filtered" - - end - - describe "string attribute with sub filters" do - let(:body) { filter :title_contains } - - it "should generate a search field for a string attribute with query contains" do - body.should have_tag("input", :attributes => { :name => "q[title_contains]"}) - body.should have_tag('label', 'Title contains') - end - - it "should NOT generate a select option for contains" do - body.should_not have_tag("option", "Contains", :attributes => { :value => 'title_contains' }) - end - - context "using starts_with and as" do - let(:body) { filter :title_starts_with } - - it "should generate a search field for a string attribute with query starts_with" do - body.should have_tag("input", :attributes => { :name => "q[title_starts_with]" }) - end - end - - context "using ends_with and as" do - let(:body) { filter :title_ends_with } - - it "should generate a search field for a string attribute with query starts_with" do - body.should have_tag("input", :attributes => { :name => "q[title_ends_with]" }) - end - end - - context "using contains and NO AS defined" do - let(:body) { filter :title_contains } - - it "should generate a search field for a string attribute with query contains" do - body.should have_tag("input", :attributes => { :name => "q[title_contains]" }) - end - end - end - - describe "text attribute" do - let(:body) { filter :body } - - it "should generate a search field for a text attribute" do - body.should have_tag("input", :attributes => { :name => "q[body_contains]"}) - end - - it "should have a proper label" do - body.should have_tag('label', 'Body') - end - end - - describe "text attribute, as a select" do - let(:body) { filter :title, as: :select } - let(:builder) { ActiveAdmin::Inputs::FilterSelectInput } - - context "when loading collection from DB" do - it "should use pluck for efficiency" do - builder.any_instance.should_receive(:pluck_column) { [] } - body - end - - it "should remove original ordering to prevent PostgreSQL error" do - scope.base.should_receive(:reorder).with('title asc') { - m = mock uniq: mock(pluck: ['A Title']) - m.uniq.should_receive(:pluck).with :title - m - } - body - end - end - end unless Rails::VERSION::MAJOR == 3 && Rails::VERSION::MINOR < 2 - - describe "datetime attribute" do - let(:body) { filter :created_at } - - it "should generate a date greater than" do - body.should have_tag("input", :attributes => { :name => "q[created_at_gte]", :class => "datepicker"}) - end - it "should generate a seperator" do - body.should have_tag("span", :attributes => { :class => "seperator"}) - end - it "should generate a date less than" do - body.should have_tag("input", :attributes => { :name => "q[created_at_lte]", :class => "datepicker"}) - end - end - - describe "integer attribute" do - let(:body) { filter :id } - - it "should generate a select option for equal to" do - body.should have_tag("option", "Equals", :attributes => { :value => 'id_equals' }) - end - it "should generate a select option for greater than" do - body.should have_tag("option", "Greater than") - end - it "should generate a select option for less than" do - body.should have_tag("option", "Less than") - end - it "should generate a text field for input" do - body.should have_tag("input", :attributes => { :name => 'q[id_equals]' }) - end - it "should select the option which is currently being filtered" - end - - describe "boolean attribute" do - context "boolean datatypes" do - let(:body) { filter :starred } - - it "should create a check box for equals to" do - body.should have_tag("input", :attributes => { - :name => "q[starred_eq]", - :type => "checkbox" }) - end - - it "should translate the label for boolean field" do - begin - I18n.backend.store_translations(:en, :activerecord => { :attributes => { :post => { :starred => "Faved" } } }) - body.should have_tag('label', 'Faved') - ensure - I18n.backend.reload! - end - end - end - - context "non-boolean data types" do - let(:body) { filter :title_is_present, :as => :boolean } - - it "should create a check box for equals to" do - body.should have_tag("input", :attributes => { - :name => "q[title_is_present]", - :type => "checkbox" }) - end - end - end - - describe "belongs_to" do - before do - @john = User.create :first_name => "John", :last_name => "Doe", :username => "john_doe" - @jane = User.create :first_name => "Jane", :last_name => "Doe", :username => "jane_doe" - end - - context "when given as the _id attribute name" do - let(:body) { filter :author_id } - - it "should generate a numeric filter" do - body.should have_tag 'label', 'Author' # really this should be Author ID :/ - body.should have_tag 'option', :attributes => { :value => 'author_id_less_than' } - body.should have_tag 'input', :attributes => { :id => 'q_author_id', :name => 'q[author_id_equals]'} - end - end - - context "when given as the name of the relationship" do - let(:body) { filter :author } - - it "should generate a select" do - body.should have_tag "select", :attributes => { :name => "q[author_id_eq]" } - end - it "should set the default text to 'Any'" do - body.should have_tag "option", "Any", :attributes => { :value => "" } - end - it "should create an option for each related object" do - body.should have_tag "option", "John Doe", :attributes => { :value => @john.id } - body.should have_tag "option", "Jane Doe", :attributes => { :value => @jane.id } - end - - context "with a proc" do - let :body do - filter :title, :as => :select, :collection => proc{ ['Title One', 'Title Two'] } - end - - it "should use call the proc as the collection" do - body.should have_tag("option", "Title One") - body.should have_tag("option", "Title Two") - end - - it "should render the collection in the context of the view" do - body = filter(:title, :as => :select, :collection => proc{[a_helper_method]}) - body.should have_tag("option", "A Helper Method") - end - end - end - - context "as check boxes" do - let(:body) { filter :author, :as => :check_boxes } - - it "should create a check box for each related object" do - body.should have_tag("input", :attributes => { - :name => "q[author_id_in][]", - :type => "checkbox", - :value => @john.id }) - body.should have_tag("input", :attributes => { - :name => "q[author_id_in][]", - :type => "checkbox", - :value => @jane.id }) - end - end - - context "when polymorphic relationship" do - it "should raise an error if a collection isn't provided" do - expect { - search = ActiveAdmin::Comment.search - render_filter(search, [{:attribute => :resource}]) - }.to raise_error Formtastic::PolymorphicInputWithoutCollectionError - end - end - end # belongs to - - describe "has_and_belongs_to_many" do - pending "add HABTM models so this can be mocked out" - end - - describe "conditional display" do - - context "with :if block" do - let(:body) do - filter :body, :if => proc{true} - filter :author, :if => proc{false} - end - - it "should be displayed if true" do - body.should have_tag("input", :attributes => { :name => "q[body_contains]"}) - end - - it "should NOT be displayed if false" do - body.should_not have_tag("input", :attributes => { :name => "q[author_id_eq]"}) - end - end - - context "with :unless block" do - let(:body) do - filter :created_at, :unless => proc{false} - filter :updated_at, :unless => proc{true} - end - - it "should be displayed if false" do - body.should have_tag("input", :attributes => { :name => "q[created_at_gte]"}) - end - - it "should NOT be displayed if true" do - body.should_not have_tag("input", :attributes => { :name => "q[updated_at_gte]"}) - end - end - end - -end diff --git a/spec/unit/filters/resource_spec.rb b/spec/unit/filters/resource_spec.rb index 55611ecd2d0..dc253d39e64 100644 --- a/spec/unit/filters/resource_spec.rb +++ b/spec/unit/filters/resource_spec.rb @@ -1,87 +1,133 @@ -require 'spec_helper' - -describe ActiveAdmin::Filters::ResourceExtension do +# frozen_string_literal: true +require "rails_helper" +RSpec.describe ActiveAdmin::Filters::ResourceExtension do let(:resource) do namespace = ActiveAdmin::Namespace.new(ActiveAdmin::Application.new, :admin) namespace.register(Post) end + it "should return a Hash" do + expect(resource.filters).to be_a Hash + end + it "should return the defaults if no filters are set" do - resource.filters.map{|f| f[:attribute].to_s }.sort.should == %w{ - author body category created_at published_at starred title updated_at - } + expect(resource.filters.keys).to match_array( + [ + :author, :body, :category, :created_at, :custom_created_at_searcher, :custom_title_searcher, :custom_searcher_numeric, :position, :published_date, :starred, :taggings, :tags, :title, :updated_at, :foo_id + ] + ) end it "should not have defaults when filters are disabled on the resource" do resource.filters = false - resource.filters.should be_empty + expect(resource.filters).to be_empty end it "should not have defaults when the filters are disabled on the namespace" do resource.namespace.filters = false - resource.filters.should be_empty + expect(resource.filters).to be_empty end it "should not have defaults when the filters are disabled on the application" do resource.namespace.application.filters = false - resource.filters.should be_empty + expect(resource.filters).to be_empty end - context "filter removal" do + it "should return the defaults without associations if default association filters are disabled on the namespace" do + resource.namespace.include_default_association_filters = false + expect(resource.filters.keys).to match_array( + [ + :body, :created_at, :custom_created_at_searcher, :custom_title_searcher, :custom_searcher_numeric, :position, :published_date, :starred, :title, :updated_at, :foo_id + ] + ) + end + + describe "removing a filter" do it "should work" do - resource.filters.should include :attribute => :author + expect(resource.filters.keys).to include :author resource.remove_filter :author - resource.filters.should_not include :attribute => :author + expect(resource.filters.keys).to_not include :author + end + + it "should work as a string" do + expect(resource.filters.keys).to include :author + resource.remove_filter "author" + expect(resource.filters.keys).to_not include :author end it "should be lazy" do - resource.should_not_receive :default_filters # this hits the DB + expect(resource).to_not receive :default_filters # this hits the DB resource.remove_filter :author end it "should not prevent the default filters from being added" do resource.remove_filter :author - resource.filters.should_not be_empty + expect(resource.filters).to_not be_empty + end + + it "should raise an exception when filters are disabled" do + resource.filters = false + expect { resource.remove_filter :author }.to raise_error(ActiveAdmin::Filters::Disabled, /Cannot remove a filter/) end end - context "adding a filter" do + describe "removing a multiple filters inline" do + it "should work" do + expect(resource.filters.keys).to include :author, :body + resource.remove_filter :author, :body + expect(resource.filters.keys).to_not include :author, :body + end + end + + describe "adding a filter" do it "should work" do resource.add_filter :title - resource.filters.should == [{:attribute => :title}] + expect(resource.filters).to eq title: {} + end + + it "should work as a string" do + resource.add_filter "title" + expect(resource.filters).to eq title: {} end it "should work with specified options" do - resource.add_filter :title, :as => :string - resource.filters.should == [{:attribute => :title, :as => :string}] + resource.add_filter :title, as: :string + expect(resource.filters).to eq title: { as: :string } + end + + it "should override an existing filter" do + resource.add_filter :title, one: :two + resource.add_filter :title, three: :four + + expect(resource.filters).to eq title: { three: :four } end it "should preserve default filters" do resource.preserve_default_filters! - resource.add_filter :count, :as => :string - resource.filters.map { |f| f[:attribute].to_s }.sort.should == %w{ - author body category count created_at published_at starred title updated_at - } + resource.add_filter :count, as: :string + + expect(resource.filters.keys).to match_array( + [ + :author, :body, :category, :count, :created_at, :custom_created_at_searcher, :custom_title_searcher, :custom_searcher_numeric, :position, :published_date, :starred, :taggings, :tags, :title, :updated_at, :foo_id + ] + ) end it "should raise an exception when filters are disabled" do resource.filters = false - expect { - resource.add_filter :title - }.should raise_error(RuntimeError) + expect { resource.add_filter :title }.to raise_error(ActiveAdmin::Filters::Disabled, /Cannot add a filter/) end end it "should reset filters" do resource.add_filter :title - resource.filters.size.should == 1 + expect(resource.filters.size).to eq 1 resource.reset_filters! - resource.filters.size.should > 1 + expect(resource.filters.size).to be > 1 end it "should add a sidebar section for the filters" do - resource.sidebar_sections.first.name.should == :filters + expect(resource.sidebar_sections.first.name).to eq "filters" end - end diff --git a/spec/unit/form_builder_spec.rb b/spec/unit/form_builder_spec.rb index ecdbf09777a..a4d6201b9de 100644 --- a/spec/unit/form_builder_spec.rb +++ b/spec/unit/form_builder_spec.rb @@ -1,11 +1,12 @@ -require 'spec_helper' - -describe ActiveAdmin::FormBuilder do +# frozen_string_literal: true +require "rails_helper" +require "rspec/mocks/standalone" +RSpec.describe ActiveAdmin::FormBuilder do # Setup an ActionView::Base object which can be used for # generating the form for. let(:helpers) do - view = action_view + view = mock_action_view def view.posts_path "/posts" end @@ -15,28 +16,92 @@ def view.protect_against_forgery? end def view.url_for(*args) - if args.first == {:action => "index"} + if args.first == { action: "index" } posts_path else super end end - def view.a_helper_method - "A Helper Method" + def view.action_name + "edit" end view end - def build_form(options = {}, form_object = Post.new, &block) - options = {:url => helpers.posts_path}.merge(options) + def form_html(options = {}, form_object = Post.new, &block) + options = { url: helpers.posts_path }.merge(options) - render_arbre_component({:form_object => form_object, :form_options => options, :form_block => block}, helpers)do - text_node active_admin_form_for(assigns[:form_object], assigns[:form_options], &assigns[:form_block]) + render_arbre_component({ form_object: form_object, form_options: options, form_block: block }, helpers) do + active_admin_form_for(assigns[:form_object], assigns[:form_options], &assigns[:form_block]) end.to_s end + def build_form(options = {}, form_object = Post.new, &block) + form = form_html(options, form_object, &block) + Capybara.string(form) + end + + context "in general" do + context "without custom settings" do + let :body do + build_form do |f| + f.inputs do + f.input :title + f.input :body + end + end + end + + it "should generate a fieldset with a inputs class" do + expect(body).to have_css("fieldset.inputs") + end + end + + context "with custom settings" do + let :body do + build_form do |f| + f.inputs class: "custom_class", name: "custom_name", custom_attr: "custom_attr", data: { test: "custom" } do + f.input :title + f.input :body + end + end + end + + it "should generate a fieldset with a inputs and custom class" do + expect(body).to have_css("fieldset.custom_class") + end + + it "should generate a fieldset with a custom legend" do + expect(body).to have_css("legend", text: "custom_name") + end + + it "should generate a fieldset with a custom attributes" do + expect(body).to have_css("fieldset[custom_attr='custom_attr']") + end + + it "should use the rails helper for rendering attributes" do + expect(body).to have_css("fieldset[data-test='custom']") + end + end + + context "with XSS payload as name" do + let :body do + build_form do |f| + f.inputs name: '' do + f.input :title + f.input :body + end + end + end + + it "should generate a fieldset with the proper legend" do + expect(body).to have_css("legend", text: "") + end + end + end + context "in general with actions" do let :body do build_form do |f| @@ -45,66 +110,65 @@ def build_form(options = {}, form_object = Post.new, &block) f.input :body end f.actions do - f.action :submit, :label => "Submit Me" - f.action :submit, :label => "Another Button" + f.action :submit, label: "Submit Me" + f.action :submit, label: "Another Button" end end end - it "should generate a text input" do - body.should have_tag("input", :attributes => { :type => "text", - :name => "post[title]" }) + it "should generate a text input" do + expect(body).to have_field("post[title]", type: "text") end + it "should generate a textarea" do - body.should have_tag("textarea", :attributes => { :name => "post[body]" }) + expect(body).to have_css("textarea[name='post[body]']") end + it "should only generate the form once" do - body.scan(/Title/).size.should == 1 + expect(body).to have_css("form", count: 1) end + it "should generate actions" do - body.should have_tag("input", :attributes => { :type => "submit", - :value => "Submit Me" }) - body.should have_tag("input", :attributes => { :type => "submit", - :value => "Another Button" }) + expect(body).to have_button("Submit Me") + expect(body).to have_button("Another Button") end end context "when polymorphic relationship" do it "should raise error" do - lambda { + expect do comment = ActiveAdmin::Comment.new - build_form({:url => "admins/comments"}, comment) do |f| + build_form({ url: "admins/comments" }, comment) do |f| f.inputs :resource end - }.should raise_error(Formtastic::PolymorphicInputWithoutCollectionError) + end.to raise_error(Formtastic::PolymorphicInputWithoutCollectionError) end end describe "passing in options with actions" do let :body do - build_form :html => { :multipart => true } do |f| + build_form html: { multipart: true } do |f| f.inputs :title f.actions end end it "should pass the options on to the form" do - body.should have_tag("form", :attributes => { :enctype => "multipart/form-data" }) + expect(body).to have_css("form[enctype='multipart/form-data']") end end - describe "passing in options with actions" do + context "file input present" do let :body do - build_form :html => { :multipart => true } do |f| - f.inputs :title - f.actions + build_form do |f| + f.input :body, as: :file end end - it "should pass the options on to the form" do - body.should have_tag("form", :attributes => { :enctype => "multipart/form-data" }) + + it "adds multipart attribute automatically" do + expect(body).to have_css("form[enctype='multipart/form-data']") end end - context "with actions" do it "should generate the form once" do body = build_form do |f| @@ -113,54 +177,92 @@ def build_form(options = {}, form_object = Post.new, &block) end f.actions end - body.scan(/id="post_title"/).size.should == 1 + expect(body).to have_css("[id=post_title]", count: 1) + end + + context "create another checkbox" do + subject do + build_form do |f| + f.actions + end + end + + %w(new create).each do |action_name| + it "generates create another checkbox on #{action_name} page" do + expect(helpers).to receive(:action_name) { action_name } + allow(helpers).to receive(:active_admin_config) { instance_double(ActiveAdmin::Resource, create_another: true) } + + is_expected.to have_css("[type=checkbox]", count: 1) + .and have_css("[name=create_another]", count: 1) + end + end + + %w(show edit update).each do |action_name| + it "doesn't generate create another checkbox on #{action_name} page" do + is_expected.to have_no_css("[name=create_another]", count: 1) + end + end end - it "should generate one button and a cancel link" do + + it "should generate one button create another checkbox and a cancel link" do body = build_form do |f| f.actions end - body.scan(/type="submit"/).size.should == 1 - body.scan(/class="cancel"/).size.should == 1 + expect(body).to have_css("[type=submit]", count: 1) + expect(body).to have_css(".cancel", count: 1) end + it "should generate multiple actions" do body = build_form do |f| f.actions do - f.action :submit, :label => "Create & Continue" - f.action :submit, :label => "Create & Edit" + f.action :submit, label: "Create & Continue" + f.action :submit, label: "Create & Edit" end end - body.scan(/type="submit"/).size.should == 2 - body.scan(/class="cancel"/).size.should == 0 + expect(body).to have_css("[type=submit]", count: 2) + expect(body).to have_css(".cancel", count: 0) end - end - context "with actions" do - it "should generate the form once" do + context "with Arbre inside" do + it "should render the Arbre in the expected place" do body = build_form do |f| + div do + h1 "Heading" + end f.inputs do + span "Top note" f.input :title + span "Bottom note" end + h3 "Footer" f.actions end - body.scan(/id="post_title"/).size.should == 1 - end - it "should generate one button and a cancel link" do - body = build_form do |f| - f.actions - end - body.scan(/type="submit"/).size.should == 1 - body.scan(/class="cancel"/).size.should == 1 + + expect(body).to have_css("div > h1") + expect(body).to have_css("h1", count: 1) + expect(body).to have_css(".inputs > ol > span") + expect(body).to have_css("span", count: 2) end - it "should generate multiple actions" do + + it "should allow a simplified syntax" do body = build_form do |f| - f.actions do - f.action :submit, :label => "Create & Continue" - f.action :submit, :label => "Create & Edit" + div do + h1 "Heading" + end + inputs do + span "Top note" + input :title + span "Bottom note" end + h3 "Footer" + actions end - body.scan(/type="submit"/).size.should == 2 - body.scan(/class="cancel"/).size.should == 0 + + expect(body).to have_css("div > h1") + expect(body).to have_css("h1", count: 1) + expect(body).to have_css(".inputs > ol > span") + expect(body).to have_css("span", count: 2) end end @@ -171,11 +273,11 @@ def build_form(options = {}, form_object = Post.new, &block) end end it "should have a title input" do - body.should have_tag("input", :attributes => { :type => "text", - :name => "post[title]" }) + expect(body).to have_field("post[title]", type: "text") end + it "should have a body textarea" do - body.should have_tag("textarea", :attributes => { :name => "post[body]" }) + expect(body).to have_css("textarea[name='post[body]']") end end @@ -186,7 +288,7 @@ def build_form(options = {}, form_object = Post.new, &block) f.input :title f.input :body end - f.instance_eval do + f.form_builder.instance_eval do @object.author = User.new end f.semantic_fields_for :author do |author| @@ -195,38 +297,115 @@ def build_form(options = {}, form_object = Post.new, &block) end end it "should generate a nested text input once" do - body.scan("post_author_attributes_first_name_input").size.should == 1 + expect(body).to have_css("[id=post_author_attributes_first_name_input]", count: 1) end end context "with collection inputs" do before do - User.create :first_name => "John", :last_name => "Doe" - User.create :first_name => "Jane", :last_name => "Doe" + User.create first_name: "John", last_name: "Doe" + User.create first_name: "Jane", last_name: "Doe" end describe "as select" do let :body do build_form do |f| - f.input :author + f.input :author, include_blank: false end end it "should create 2 options" do - body.scan(/
' + + expect(table.find_by_tag("tr").last.to_s.split("\n").first.lstrip). + to eq '' + end + it "should allow html options for the row itself" do - table = render_arbre_component(assigns) { + table = render_arbre_component(assigns) do attributes_table_for(post) do - row("Wee", :class => "custom_row", :style => "custom_style") { } + row("Wee", class: "custom_row", style: "custom_style") {} end - } - table.find_by_tag("tr").first.to_s. - split("\n").first.lstrip. - should == '' + end + expect(table.find_by_tag("tr").first.to_s.split("\n").first.lstrip). + to eq '' end it "should allow html content inside the attributes table" do - table = render_arbre_component(assigns) { + table = render_arbre_component(assigns) do attributes_table_for(post) do - row("ID"){ span(post.id, :class => 'id') } + row("ID") { span(post.id, class: "id") } end - } - table.find_by_tag("td").first.content.chomp.strip.should == "1" + end + expect(table.find_by_tag("td").first.content.chomp.strip).to eq "1" end - it "should check if an association exists when an attribute has id in it" do - post.author = User.new :username => 'john_doe', :first_name => 'John', :last_name => 'Doe' - table = render_arbre_component(assigns) { - attributes_table_for post, :author_id - } - table.find_by_tag('td').first.content.should == 'John Doe' + context "an attribute ending in _id" do + before do + post.foo_id = 23 + post.author = User.new username: "john_doe", first_name: "John", last_name: "Doe" + end + it "should call the association if one exists" do + table = render_arbre_component assigns do + attributes_table_for post, :author + end + expect(table.find_by_tag("th").first.content).to eq "Author" + expect(table.find_by_tag("td").first.content).to eq "John Doe" + end + + it "should not attempt to call a nonexistent association" do + table = render_arbre_component assigns do + attributes_table_for post, :foo_id + end + expect(table.find_by_tag("th").first.content).to eq "Foo" + expect(table.find_by_tag("td").first.content).to eq "23" + end end - end + context "with a collection" do + let(:posts) do + [Post.new(title: "Hello World", id: 1), Post.new(title: "Multi Column", id: 2)].each_with_index do |post, index| + allow(post).to receive(:id).and_return(index + 1) + allow(post).to receive(:new_record?).and_return(false) + end + end + + let(:assigns) { { posts: posts } } + + let(:table) do + render_arbre_component(assigns) do + attributes_table_for posts, :id, :title + end + end + + it "does not set id on the table" do + expect(table.attr(:id)).to eq nil + end + + context "colgroup" do + let(:cols) { table.find_by_tag "col" } + + it "contains a col for each record (plus headers)" do + expect(cols.size).to eq(2 + 1) + end + it "assigns an id to each col" do + cols[1..-1].each_with_index do |col, index| + expect(col.id).to eq "attributes_table_post_#{index + 1}" + end + end + end + + context "when rendering the rows" do + it "should contain 3 columns" do + expect(table.find_by_tag("tr").first.children.size).to eq 3 + end + + [ + ["Id", "1", "2"], + ["Title", "Hello World", "Multi Column"], + ].each_with_index do |set, i| + describe "for #{set[0]}" do + let(:title) { set[0] } + let(:current_row) { table.find_by_tag("tr")[i] } + + it "should have the title '#{set[0]}'" do + expect(current_row.find_by_tag("th").first.content).to eq title + end + + context "with defined attribute name translation" do + it "should have the translated attribute name in the title" do + with_translation %i[activerecord attributes post title], "Translated Title" do + with_translation %i[activerecord attributes post id], "Translated Id" do + expect(current_row.find_by_tag("th").first.content).to eq "Translated #{title}" + end + end + end + end + + set[1..-1].each_with_index do |content, index| + it "column #{index} should have the content '#{content}'" do + expect(current_row.find_by_tag("td")[index].content.chomp.strip).to eq content + end + end + end + end + end # describe rendering rows + end # with a collection + + context "when using a single Hash" do + let(:table) do + render_arbre_component nil, helpers do + attributes_table_for foo: 1, bar: 2 do + row :foo + row :bar + end + end + end + it "should render" do + expect(table.find_by_tag("th")[0].content).to eq "Foo" + expect(table.find_by_tag("th")[1].content).to eq "Bar" + expect(table.find_by_tag("td")[0].content).to eq "1" + expect(table.find_by_tag("td")[1].content).to eq "2" + end + end + + context "when using an Array of Hashes" do + let(:table) do + render_arbre_component nil, helpers do + attributes_table_for [{ foo: 1 }, { foo: 2 }] do + row :foo + end + end + end + it "should render" do + expect(table.find_by_tag("th")[0].content).to eq "Foo" + expect(table.find_by_tag("td")[0].content).to eq "1" + expect(table.find_by_tag("td")[1].content).to eq "2" + end + end + + context "when using custom string labels with acronyms" do + let(:table) do + render_arbre_component(assigns) do + attributes_table_for post do + row("Registration ID") { post.id } + row("LMnO") { post.title } + end + end + end + + it "should preserve acronym capitalization in custom string labels" do + rows = table.find_by_tag("tr") + + expect(rows[0].find_by_tag("th").first.content).to eq "Registration ID" + expect(rows[1].find_by_tag("th").first.content).to eq "LMnO" + + expect(rows[0].find_by_tag("th").first.content).not_to eq "Registration Id" + expect(rows[1].find_by_tag("th").first.content).not_to eq "L Mn O" + end + end + end end diff --git a/spec/unit/views/components/batch_action_popover_spec.rb b/spec/unit/views/components/batch_action_popover_spec.rb deleted file mode 100644 index 98cc11f47f7..00000000000 --- a/spec/unit/views/components/batch_action_popover_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'spec_helper' -require 'active_admin/batch_actions/views/batch_action_popover' - -describe ActiveAdmin::BatchActions::BatchActionPopover do - - let(:the_popover) do - render_arbre_component do - batch_action_popover do - action ActiveAdmin::BatchAction.new( :action_1, "Action 1" ) - action ActiveAdmin::BatchAction.new( :action_2, "Action 2" ) - action ActiveAdmin::BatchAction.new( :action_3, "Action 3" ) - end - end - end - - it "should have an id" do - the_popover.id.should == "batch_actions_popover" - end - - describe "the action list" do - subject do - the_popover.find_by_class("popover_contents").first - end - - its(:tag_name) { should eql("ul") } - - its(:content){ should include("
  • Action 1 Selected
  • ") } - its(:content){ should include("
  • Action 2 Selected
  • ") } - its(:content){ should include("
  • Action 3 Selected
  • ") } - - end - -end diff --git a/spec/unit/views/components/blank_slate_spec.rb b/spec/unit/views/components/blank_slate_spec.rb deleted file mode 100644 index 967fbc26535..00000000000 --- a/spec/unit/views/components/blank_slate_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'spec_helper' - -describe ActiveAdmin::Views::BlankSlate do - - describe "#blank_slate" do - subject do - render_arbre_component do - blank_slate("There are no Posts yet. Create one") - end - end - - its(:tag_name) { should eql 'div' } - its(:class_list) { should include('blank_slate_container') } - - its(:content) { should include 'There are no Posts yet. Create one' } - end -end diff --git a/spec/unit/views/components/columns_spec.rb b/spec/unit/views/components/columns_spec.rb deleted file mode 100644 index f62ee3cdb8e..00000000000 --- a/spec/unit/views/components/columns_spec.rb +++ /dev/null @@ -1,142 +0,0 @@ -require 'spec_helper' - -describe ActiveAdmin::Views::Columns do - - describe "Rendering one column" do - let(:cols) do - render_arbre_component do - columns do - column { span "Hello World" } - end - end - end - - it "should have the class .columns" do - cols.class_list.should include("columns") - end - - it "should have one column" do - cols.children.size.should == 1 - cols.children.first.class_list.should include("column") - end - - it "should have one column with the width 100.0%" do - cols.children.first.attr(:style).should include("width: 100.0%") - end - end - - describe "Rendering two columns" do - let(:cols) do - render_arbre_component do - columns do - column { span "Hello World" } - column { span "Hello World" } - end - end - end - - it "should have two columns" do - cols.children.size.should == 2 - end - - it "should have a first column with width 49% and margin 2%" do - cols.children.first.attr(:style).should == "width: 49.0%; margin-right: 2%;" - end - - it "should have a second column with width 49% and no right margin" do - cols.children.last.attr(:style).should == "width: 49.0%;" - end - end - - describe "Rendering four columns" do - let(:cols) do - render_arbre_component do - columns do - column { span "Hello World" } - column { span "Hello World" } - column { span "Hello World" } - column { span "Hello World" } - end - end - end - - it "should have four columns" do - cols.children.size.should == 4 - end - - - (0..2).to_a.each do |index| - it "should have column #{index + 1} with width 49% and margin 2%" do - cols.children[index].attr(:style).should == "width: 23.5%; margin-right: 2%;" - end - end - - it "should have column 4 with width 49% and no margin" do - cols.children[3].attr(:style).should == "width: 23.5%;" - end - end - - - describe "Column Spans" do - let(:cols) do - render_arbre_component do - columns do - column(:span => 2){ "Hello World" } - column(){ "Hello World" } - column(){ "Hello World" } - end - end - end - - it "should set the span when declared" do - cols.children.first.attr(:style).should == "width: 49.0%; margin-right: 2%;" - end - - it "should default to 1 if not passed in" do - cols.children.last.attr(:style).should == "width: 23.5%;" - end - end - - describe "Column max width" do - - let(:cols) do - render_arbre_component do - columns do - column(:max_width => "100px"){ "Hello World" } - column(){ "Hello World" } - end - end - end - - it "should set the max with if passed in" do - cols.children.first.attr(:style).should == "width: 49.0%; max-width: 100px; margin-right: 2%;" - end - - it "should omit the value if not presetn" do - cols.children.last.attr(:style).should == "width: 49.0%;" - end - - end - - describe "Column min width" do - - let(:cols) do - render_arbre_component do - columns do - column(:min_width => "100px"){ "Hello World" } - column(){ "Hello World" } - end - end - end - - it "should set the min with if passed in" do - cols.children.first.attr(:style).should == "width: 49.0%; min-width: 100px; margin-right: 2%;" - end - - it "should omit the value if not presetn" do - cols.children.last.attr(:style).should == "width: 49.0%;" - end - - end - -end diff --git a/spec/unit/views/components/index_list_spec.rb b/spec/unit/views/components/index_list_spec.rb index f387c92449f..a6c0be2b316 100644 --- a/spec/unit/views/components/index_list_spec.rb +++ b/spec/unit/views/components/index_list_spec.rb @@ -1,32 +1,64 @@ -require 'spec_helper' +# frozen_string_literal: true +require "rails_helper" -describe ActiveAdmin::Views::IndexList do +RSpec.describe ActiveAdmin::Views::IndexList do + let(:custom_index_as) do + Class.new(ActiveAdmin::Component) do + def build(page_presenter, collection) + add_class "index" + resource_selection_toggle_panel if active_admin_config.batch_actions.any? + collection.each do |obj| + instance_exec(obj, &page_presenter.block) + end + end - describe "#index_list_renderer" do + def self.index_name + "custom" + end + end + end + describe "#index_list_renderer" do + let(:index_classes) { [ActiveAdmin::Views::IndexAsTable, custom_index_as] } - let(:index_classes) { [ActiveAdmin::Views::IndexAsTable, ActiveAdmin::Views::IndexAsBlock] } + let(:collection) do + Post.create(title: "First Post", starred: true) + Post.where(nil) + end let(:helpers) do helpers = mock_action_view - helpers.stub! :url_for => "/" - helpers.stub!(:params).and_return({:as => "table"}) + allow(helpers).to receive(:url_for) { |url| "/?#{ url.to_query }" } + allow(helpers.request).to receive(:query_parameters).and_return as: "table", q: { title_cont: "terms" } + allow(helpers).to receive(:params).and_return(ActionController::Parameters.new(as: "table", q: { title_cont: "terms" })) + allow(helpers).to receive(:collection).and_return(collection) helpers end subject do - render_arbre_component({:index_classes => index_classes}, helpers) do - index_list_renderer(index_classes) + render_arbre_component({ index_classes: index_classes }, helpers) do + insert_tag(ActiveAdmin::Views::IndexList, index_classes) end end - its(:tag_name) { should == 'ul' } + describe "#tag_name" do + subject { super().tag_name } + it { is_expected.to eq "div" } + end it "should contain the names of available indexes in links" do a_tags = subject.find_by_tag("a") - a_tags.size.should == 2 - a_tags.first.to_s.should include("Table") - a_tags.last.to_s.should include("List") + expect(a_tags.size).to eq 2 + expect(a_tags.first.to_s).to include("Table") + expect(a_tags.last.to_s).to include("Custom") + end + + it "should maintain index filter parameters" do + a_tags = subject.find_by_tag("a") + expect(a_tags.first.attributes[:href]) + .to eq("/?#{ { as: "table", q: { title_cont: "terms" } }.to_query }") + expect(a_tags.last.attributes[:href]) + .to eq("/?#{ { as: "custom", q: { title_cont: "terms" } }.to_query }") end end end diff --git a/spec/unit/views/components/index_table_for_spec.rb b/spec/unit/views/components/index_table_for_spec.rb new file mode 100644 index 00000000000..9ab6ba25240 --- /dev/null +++ b/spec/unit/views/components/index_table_for_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe ActiveAdmin::Views::IndexAsTable::IndexTableFor do + describe "creating with the dsl" do + let(:collection) do + [Post.new(title: "First Post", starred: true)] + end + let(:active_admin_config) do + namespace = ActiveAdmin::Namespace.new(ActiveAdmin::Application.new, :admin) + namespace.batch_actions = [ActiveAdmin::BatchAction.new(:flag, "Flag") {}] + namespace + end + + let(:assigns) do + { + collection: collection, + active_admin_config: active_admin_config, + resource_class: User, + } + end + let(:helpers) { mock_action_view } + + context "when creating a selectable column" do + let(:table) do + render_arbre_component assigns, helpers do + insert_tag(ActiveAdmin::Views::IndexAsTable::IndexTableFor, collection, { sortable: true }) do + selectable_column(class: "selectable") + end + end + end + + context "creates a table header based on the selectable column" do + let(:header) do + table.find_by_tag("th").first + end + + it "with selectable column class name" do + expect(header.attributes[:class]).to include "selectable" + end + + it "not sortable" do + expect(header.attributes).not_to include("data-sortable": "") + end + end + end + + context "when creating an id column" do + before { allow(helpers).to receive(:url_target) { 'routing_stub' } } + + def build_index_table(&block) + render_arbre_component assigns, helpers do + insert_tag(ActiveAdmin::Views::IndexAsTable::IndexTableFor, collection, { sortable: true }) do + instance_exec(&block) + end + end + end + + it "use primary key as title by default" do + table = build_index_table { id_column } + header = table.find_by_tag("th").first + expect(header.content).to include("id") + end + + it "supports title customization" do + table = build_index_table { id_column 'Res. Id' } + header = table.find_by_tag("th").first + expect(header.content).to include("Res. Id") + end + + it "is sortable by default" do + table = build_index_table { id_column } + header = table.find_by_tag("th").first + expect(header.attributes).to include("data-sortable": "") + end + + it "supports sortable: false" do + table = build_index_table { id_column sortable: false } + header = table.find_by_tag("th").first + expect(header.attributes).not_to include("data-sortable": "") + end + + it "supports sortable column names" do + table = build_index_table { id_column sortable: :created_at } + header = table.find_by_tag("th").first + expect(header.attributes).to include("data-sortable": "") + end + + it 'supports title customization and options' do + table = build_index_table { id_column 'Res. Id', sortable: :created_at } + header = table.find_by_tag("th").first + expect(header.content).to include("Res. Id") + expect(header.attributes).to include("data-sortable": "") + end + end + end +end diff --git a/spec/unit/views/components/paginated_collection_spec.rb b/spec/unit/views/components/paginated_collection_spec.rb index 0148c4686a9..b4e36669d85 100644 --- a/spec/unit/views/components/paginated_collection_spec.rb +++ b/spec/unit/views/components/paginated_collection_spec.rb @@ -1,75 +1,93 @@ -require 'spec_helper' +# frozen_string_literal: true +require "rails_helper" -describe ActiveAdmin::Views::PaginatedCollection do +RSpec.describe ActiveAdmin::Views::PaginatedCollection do describe "creating with the dsl" do - - before :all do - load_defaults! - reload_routes! + around do |example| + with_resources_during(example) { ActiveAdmin.register Post } end let(:view) do view = mock_action_view - view.request.stub!(:query_parameters).and_return({:controller => 'admin/posts', :action => 'index', :page => '1'}) - view.controller.params = {:controller => 'admin/posts', :action => 'index'} + allow(view.request).to receive(:query_parameters).and_return page: "1" + allow(view.request).to receive(:path_parameters).and_return controller: "admin/posts", action: "index" + allow(view).to receive(:build_download_formats).and_return([:csv, :xml, :json]) view end # Helper to render paginated collections within an arbre context def paginated_collection(*args) - render_arbre_component({:paginated_collection_args => args}, view) do + render_arbre_component({ paginated_collection_args: args }, view) do paginated_collection(*paginated_collection_args) end end let(:collection) do - posts = [Post.new(:title => "First Post"), Post.new(:title => "Second Post"), Post.new(:title => "Third Post")] + posts = [Post.new(title: "First Post"), Post.new(title: "Second Post"), Post.new(title: "Third Post")] Kaminari.paginate_array(posts).page(1).per(5) end before do - collection.stub!(:reorder) { collection } + allow(collection).to receive(:except) { collection } unless collection.respond_to? :except + allow(collection).to receive(:group_values) { [] } unless collection.respond_to? :group_values end - context "when specifying collection" do - let(:pagination) do - paginated_collection(collection) - end + let(:pagination) { paginated_collection collection } - it "should set :collection as the passed in collection" do - pagination.find_by_class('pagination_information').first.content.should == "Displaying all 3 posts" - end + it "should set :collection as the passed in collection" do + expect(pagination.find_by_class("pagination-information").first.content).to eq "Showing all 3" + end - it "should raise error if collection has no pagination scope" do - lambda { - paginated_collection([Post.new, Post.new]) - }.should raise_error(StandardError, "Collection is not a paginated scope. Set collection.page(params[:page]).per(10) before calling :paginated_collection.") - end + it "should raise error if collection has no pagination scope" do + expect do + paginated_collection([Post.new, Post.new]) + end.to raise_error(StandardError, "Collection is not a paginated scope. Set collection.page(params[:page]).per(10) before calling :paginated_collection.") + end + + it "should preserve custom query params" do + allow(view.request).to receive(:query_parameters).and_return page: "1", something: "else" + pagination_content = pagination.content + expect(pagination_content).to include "/admin/posts.csv?page=1&something=else" + expect(pagination_content).to include "/admin/posts.xml?page=1&something=else" + expect(pagination_content).to include "/admin/posts.json?page=1&something=else" end context "when specifying :param_name option" do let(:collection) do - posts = 10.times.inject([]) {|m, _| m << Post.new } + posts = Array.new(10) { Post.new } Kaminari.paginate_array(posts).page(1).per(5) end - let(:pagination) { paginated_collection(collection, :param_name => :post_page) } + let(:pagination) { paginated_collection(collection, param_name: :post_page) } it "should customize the page number parameter in pagination links" do - pagination.children.last.content.should match(/\/admin\/posts\?post_page=2/) + expect(pagination.to_s).to match(/\/admin\/posts\?post_page=2/) + end + end + + context "when specifying :params option" do + let(:collection) do + posts = Array.new(10) { Post.new } + Kaminari.paginate_array(posts).page(1).per(5) + end + + let(:pagination) { paginated_collection(collection, param_name: :post_page, params: { anchor: "here" }) } + + it "should pass it through to Kaminari" do + expect(pagination.to_s).to match(/\/admin\/posts\?post_page=2#here/) end end - context "when specifying :download_links => false option" do + context "when specifying download_links: false option" do let(:collection) do - posts = 10.times.inject([]) {|m, _| m << Post.new } + posts = Array.new(10) { Post.new } Kaminari.paginate_array(posts).page(1).per(5) end - let(:pagination) { paginated_collection(collection, :download_links => false) } + let(:pagination) { paginated_collection(collection, download_links: false) } it "should not render download links" do - pagination.find_by_tag('div').last.content.should_not match(/Download:/) + expect(pagination.find_by_tag("div").last.content).to_not match(/Download:/) end end @@ -79,18 +97,18 @@ def paginated_collection(*args) Kaminari.paginate_array(posts).page(1).per(5) end - let(:pagination) { paginated_collection(collection, :entry_name => "message") } + let(:pagination) { paginated_collection(collection, entry_name: "message") } it "should use :entry_name as the collection name" do - pagination.find_by_class('pagination_information').first.content.should == "Displaying 1 message" + expect(pagination.find_by_class("pagination-information").first.content).to eq "Showing 1 of 1" end end context "when specifying :entry_name option with multiple items" do - let(:pagination) { paginated_collection(collection, :entry_name => "message") } + let(:pagination) { paginated_collection(collection, entry_name: "message") } it "should use :entry_name as the collection name" do - pagination.find_by_class('pagination_information').first.content.should == "Displaying all 3 messages" + expect(pagination.find_by_class("pagination-information").first.content).to eq "Showing all 3" end end @@ -100,18 +118,18 @@ def paginated_collection(*args) Kaminari.paginate_array(posts).page(1).per(5) end - let(:pagination) { paginated_collection(collection, :entry_name => "singular", :entries_name => "plural") } + let(:pagination) { paginated_collection(collection, entry_name: "singular", entries_name: "plural") } it "should use :entry_name as the collection name" do - pagination.find_by_class('pagination_information').first.content.should == "Displaying 1 singular" + expect(pagination.find_by_class("pagination-information").first.content).to eq "Showing 1 of 1" end end context "when specifying :entry_name and :entries_name option with a multiple items" do - let(:pagination) { paginated_collection(collection, :entry_name => "singular", :entries_name => "plural") } + let(:pagination) { paginated_collection(collection, entry_name: "singular", entries_name: "plural") } it "should use :entries_name as the collection name" do - pagination.find_by_class('pagination_information').first.content.should == "Displaying all 3 plural" + expect(pagination.find_by_class("pagination-information").first.content).to eq "Showing all 3" end end @@ -121,28 +139,24 @@ def paginated_collection(*args) Kaminari.paginate_array(posts).page(1).per(5) end - let(:pagination) { paginated_collection(collection) } - it "should use 'post' as the collection name when there is no I18n translation" do - pagination.find_by_class('pagination_information').first.content.should == "Displaying 1 post" + expect(pagination.find_by_class("pagination-information").first.content).to eq "Showing 1 of 1" end it "should use 'Singular' as the collection name when there is an I18n translation" do - I18n.stub(:translate) { "Singular" } - pagination.find_by_class('pagination_information').first.content.should == "Displaying 1 Singular" + allow(I18n).to receive(:translate) { "Singular" } + expect(pagination.find_by_class("pagination-information").first.content).to eq "Showing 1 of 1" end end context "when omitting :entry_name with multiple items" do - let(:pagination) { paginated_collection(collection) } - it "should use 'posts' as the collection name when there is no I18n translation" do - pagination.find_by_class('pagination_information').first.content.should == "Displaying all 3 posts" + expect(pagination.find_by_class("pagination-information").first.content).to eq "Showing all 3" end it "should use 'Plural' as the collection name when there is an I18n translation" do - I18n.stub(:translate) { "Plural" } - pagination.find_by_class('pagination_information').first.content.should == "Displaying all 3 Plural" + allow(I18n).to receive(:translate) { "Plural" } + expect(pagination.find_by_class("pagination-information").first.content).to eq "Showing all 3" end end @@ -152,54 +166,138 @@ def paginated_collection(*args) Kaminari.paginate_array(posts).page(1).per(5) end - let(:pagination) { paginated_collection(collection) } - it "should display 'No entries found'" do - pagination.find_by_class('pagination_information').first.content.should == "No entries found" + expect(pagination.find_by_class("pagination-information").first.content).to eq "No entries found" end end context "when collection comes from find with GROUP BY" do let(:collection) do - %w{Foo Foo Bar}.each {|title| Post.create(:title => title) } + %w{Foo Foo Bar}.each { |title| Post.create(title: title) } Post.select(:title).group(:title).page(1).per(5) end - let(:pagination) { paginated_collection(collection) } - it "should display proper message (including number and not hash)" do - pagination.find_by_class('pagination_information').first.content.should == "Displaying all 2 posts" + expect(pagination.find_by_class("pagination-information").first.content).to eq "Showing all 2" end end context "when collection with many pages comes from find with GROUP BY" do let(:collection) do - %w{Foo Foo Bar Baz}.each {|title| Post.create(:title => title) } + %w{Foo Foo Bar Baz}.each { |title| Post.create(title: title) } Post.select(:title).group(:title).page(1).per(2) end - let(:pagination) { paginated_collection(collection) } - it "should display proper message (including number and not hash)" do - pagination.find_by_class('pagination_information').first.content. - gsub(' ',' ').should == "Displaying posts 1 - 2 of 3 in total" + expect(pagination.find_by_class("pagination-information").first.content.gsub(" ", " ")). + to eq "Showing 1-2 of 3" end end context "when viewing the last page of a collection that has multiple pages" do let(:collection) do - posts = [Post.new] * 81 - Kaminari.paginate_array(posts).page(3).per(30) + Kaminari.paginate_array([Post.new] * 81).page(3).per(30) end - let(:pagination) { paginated_collection(collection) } - it "should show the proper item counts" do - pagination.find_by_class('pagination_information').first.content. - gsub(' ',' ').should == "Displaying posts 61 - 81 of 81 in total" + expect(pagination.find_by_class("pagination-information").first.content.gsub(" ", " ")). + to eq "Showing 61-81 of 81" + end + end + + context "with :pagination_total" do + let(:collection) do + Kaminari.paginate_array([Post.new] * 256).page(1).per(30) + end + + describe "set to false" do + it "should not show the total item counts" do + expect(collection).not_to receive(:total_pages) + pagination = paginated_collection(collection, pagination_total: false) + info = pagination.find_by_class("pagination-information").first.content.gsub(" ", " ") + expect(info).to eq "Showing 1-30" + end + end + + describe "set to true" do + let(:pagination) { paginated_collection(collection, pagination_total: true) } + + it "should show the total item counts" do + info = pagination.find_by_class("pagination-information").first.content.gsub(" ", " ") + expect(info).to eq "Showing 1-30 of 256" + end end end + describe "when pagination_total is false" do + it "makes no expensive COUNT queries" do + undecorated_collection = Post.all.page(1).per(30) + + expect { paginated_collection(undecorated_collection, pagination_total: false) } + .not_to perform_database_query("SELECT COUNT(*) FROM \"posts\"") + + decorated_collection = controller_with_decorator("index", PostDecorator).apply_collection_decorator(undecorated_collection.reset) + + expect { paginated_collection(decorated_collection, pagination_total: false) } + .not_to perform_database_query("SELECT COUNT(*) FROM \"posts\"") + end + + it "makes a performant COUNT query to figure out if we are on the last page" do + # "SELECT COUNT(*) FROM (SELECT 1". Let's make sure the subquery has LIMIT and OFFSET. It shouldn't have ORDER BY + count_query = %r{SELECT COUNT\(\*\) FROM \(SELECT 1 .*FROM "posts" (?=.*OFFSET \?)(?=.*LIMIT \?)(?!.*ORDER BY)} + + undecorated_collection = Post.all.page(1).per(30) + + expect { paginated_collection(undecorated_collection, pagination_total: false) } + .to perform_database_query(count_query) + undecorated_sorted_collection = undecorated_collection.reset.order(id: :desc) + + expect { paginated_collection(undecorated_sorted_collection, pagination_total: false) } + .to perform_database_query(count_query) + + decorated_collection = controller_with_decorator("index", PostDecorator).apply_collection_decorator(undecorated_collection.reset) + + expect { paginated_collection(decorated_collection, pagination_total: false) } + .to perform_database_query(count_query) + + decorated_sorted_collection = controller_with_decorator("index", PostDecorator).apply_collection_decorator(undecorated_sorted_collection.reset) + + expect { paginated_collection(decorated_sorted_collection, pagination_total: false) } + .to perform_database_query(count_query) + end + end + + it "makes no COUNT queries to figure out the last element of each page" do + undecorated_collection = Post.all.page(1).per(30) + + expect { paginated_collection(undecorated_collection) } + .not_to perform_database_query("SELECT COUNT(*) FROM (SELECT") + end + + context "when specifying per_page: array option" do + let(:collection) do + posts = Array.new(10) { Post.new } + Kaminari.paginate_array(posts).page(1).per(5) + end + + let(:pagination) { paginated_collection(collection, per_page: [1, 2, 3]) } + let(:pagination_html) { pagination.find_by_class("paginated-collection-footer").first } + let(:pagination_node) { Capybara.string(pagination_html.to_s) } + + it "should render per_page select tag" do + expect(pagination_html.content).to match(/Per page/) + expect(pagination_node).to have_css("select option", count: 3) + end + + context "with pagination_total: false" do + let(:pagination) { paginated_collection(collection, per_page: [1, 2, 3], pagination_total: false) } + + it "should render per_page select tag" do + info = pagination.find_by_class("pagination-information").first.content.gsub(" ", " ") + expect(info).to eq "Showing 1-5" + end + end + end end end diff --git a/spec/unit/views/components/panel_spec.rb b/spec/unit/views/components/panel_spec.rb index 2188df76ff0..ff9d101e3f6 100644 --- a/spec/unit/views/components/panel_spec.rb +++ b/spec/unit/views/components/panel_spec.rb @@ -1,8 +1,8 @@ -require 'spec_helper' +# frozen_string_literal: true +require "rails_helper" -describe ActiveAdmin::Views::Panel do - - let(:the_panel) do +RSpec.describe ActiveAdmin::Views::Panel do + let(:arbre_panel) do render_arbre_component do panel "My Title" do span("Hello World") @@ -10,34 +10,43 @@ end end + let(:panel_html) { Capybara.string(arbre_panel.to_s) } + it "should have a title h3" do - the_panel.find_by_tag("h3").first.content.should == "My Title" + expect(panel_html).to have_css "h3", text: "My Title" end it "should have a contents div" do - the_panel.find_by_tag("div").first.class_list.should include("panel_contents") + expect(panel_html).to have_css "div.panel-body" end it "should add children to the contents div" do - the_panel.find_by_tag("span").first.parent.should == the_panel.find_by_tag("div").first + expect(panel_html).to have_css "div.panel-body > span", text: "Hello World" end - it "should set the icon" do - the_panel = render_arbre_component do - panel("Title", :icon => :arrow_down) + context "with html-safe title" do + let(:arbre_panel) do + title_with_html = %q[Title with HTML].html_safe + render_arbre_component do + panel(title_with_html) + end + end + + it "should allow a html_safe title" do + expect(panel_html).to have_css "h3", text: "Title with HTML" + expect(panel_html).to have_css "h3 > abbr", text: "HTML" end - the_panel.find_by_tag("h3").first.content.should include("span class=\"icon") end describe "#children?" do - - it "returns false if no children have been added to the panel" do - the_panel = render_arbre_component do + let(:arbre_panel) do + render_arbre_component do panel("A Panel") end - the_panel.children?.should == false end + it "returns false if no children have been added to the panel" do + expect(arbre_panel.children?).to eq false + end end - end diff --git a/spec/unit/views/components/popover_spec.rb b/spec/unit/views/components/popover_spec.rb deleted file mode 100644 index d750706c974..00000000000 --- a/spec/unit/views/components/popover_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'spec_helper' - -describe ActiveAdmin::Views::Popover do - - let(:the_popover) do - render_arbre_component do - popover :id => "my_awesome_popover" do - para "Hello World, this is popover content baby." - end - end - end - - it "should have the class of 'sidebar_section'" do - the_popover.class_list.should include("popover") - end - - it "should have an inner content element class of 'popover_contents'" do - the_popover.find_by_tag("div").first.class_list.should include("popover_contents") - end - - it "should have the popover content in the inner content element" do - the_popover.find_by_class("popover_contents").first.content.should include("Hello World, this is popover content baby.") - end - - it "should have an id" do - the_popover.id.should == "my_awesome_popover" - end - - it "should be hidden" do - the_popover.attributes.should include(:style => "display: none"); - end - -end diff --git a/spec/unit/views/components/scopes_spec.rb b/spec/unit/views/components/scopes_spec.rb new file mode 100644 index 00000000000..8194551a9b2 --- /dev/null +++ b/spec/unit/views/components/scopes_spec.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true +require "rails_helper" + +RSpec.describe ActiveAdmin::Views::Scopes do + describe "the scopes list" do + let(:collection) { Post.all } + let(:active_admin_config) { ActiveAdmin.register(Post) } + + let(:assigns) do + { + active_admin_config: active_admin_config, + collection_before_scope: collection + } + end + + let(:helpers) do + helpers = mock_action_view + + allow(helpers.request) + .to receive(:path_parameters) + .and_return(controller: "admin/posts", action: "index") + + helpers + end + + let(:configured_scopes) do + [ + ActiveAdmin::Scope.new(:all), + ActiveAdmin::Scope.new(:published) { |posts| posts.where.not(published_date: nil) } + ] + end + + let(:scope_options) do + { scope_count: true } + end + + let(:scopes) do + scopes_to_render = configured_scopes + options = scope_options + + render_arbre_component assigns, helpers do + insert_tag(ActiveAdmin::Views::Scopes, scopes_to_render, options) + end + end + + before do + allow(ActiveAdmin::AsyncCount).to receive(:new).and_call_original + end + + around do |example| + with_resources_during(example) { active_admin_config } + end + + it "renders the scopes component" do + html = Capybara.string(scopes.to_s) + expect(html).to have_css("div.scopes") + + configured_scopes.each do |scope| + expect(html).to have_css("a[href='/admin/posts?scope=#{scope.id}']") + end + end + + context "when scopes are configured to query their counts asynchronously" do + let(:configured_scopes) do + [ + ActiveAdmin::Scope.new(:all, nil, show_count: :async), + ActiveAdmin::Scope.new(:published, nil, show_count: :async) { |posts| posts.where.not(published_date: nil) } + ] + end + + it "raises an error when ActiveRecord async_count is unavailable", unless: Post.respond_to?(:async_count) do + expect { scopes }.to raise_error(ActiveAdmin::AsyncCount::NotSupportedError, %r{does not support :async_count}) + end + + context "when async_count is available in Rails", if: Post.respond_to?(:async_count) do + it "uses AsyncCounts" do + scopes + + expect(ActiveAdmin::AsyncCount).to have_received(:new).with(Post.all) + expect(ActiveAdmin::AsyncCount).to have_received(:new).with(Post.where.not(published_date: nil)) + end + + context "when an individual scope is configured to show its count async" do + let(:configured_scopes) do + [ + ActiveAdmin::Scope.new(:all), + ActiveAdmin::Scope.new(:published, nil, show_count: :async) { |posts| posts.where.not(published_date: nil) } + ] + end + + it "only uses AsyncCounts for the configured scopes" do + scopes + + expect(ActiveAdmin::AsyncCount).not_to have_received(:new).with(Post.all) + expect(ActiveAdmin::AsyncCount).to have_received(:new).with(Post.where.not(published_date: nil)) + end + end + + context "when an individual scope is configured to hide its count" do + let(:configured_scopes) do + [ + ActiveAdmin::Scope.new(:all, nil, show_count: false), + ActiveAdmin::Scope.new(:published, nil, show_count: :async) { |posts| posts.where.not(published_date: nil) } + ] + end + + it "only uses AsyncCounts for the configured scopes" do + scopes + + expect(ActiveAdmin::AsyncCount).not_to have_received(:new).with(Post.all) + expect(ActiveAdmin::AsyncCount).to have_received(:new).with(Post.where.not(published_date: nil)) + end + end + + context "when a scope is not to be displayed" do + let(:configured_scopes) do + [ + ActiveAdmin::Scope.new(:all, nil, show_count: :async, if: -> { false }) + ] + end + + it "avoids AsyncCounts" do + scopes + + expect(ActiveAdmin::AsyncCount).not_to have_received(:new) + end + end + end + + context "when :show_count is configured as false" do + let(:scope_options) do + { scope_count: false } + end + + it "avoids AsyncCounts" do + scopes + + expect(ActiveAdmin::AsyncCount).not_to have_received(:new) + end + end + end + end +end diff --git a/spec/unit/views/components/sidebar_section_spec.rb b/spec/unit/views/components/sidebar_section_spec.rb deleted file mode 100644 index 97eca8e4896..00000000000 --- a/spec/unit/views/components/sidebar_section_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'spec_helper' - -describe ActiveAdmin::Views::SidebarSection do - - let(:section) do - ActiveAdmin::SidebarSection.new("Help Section") do - span "Help Me" - end - end - - let(:html) do - render_arbre_component :section => section do - sidebar_section(assigns[:section]) - end - end - - it "should have a title h3" do - html.find_by_tag("h3").first.content.should == "Help Section" - end - - it "should have the class of 'sidebar_section'" do - html.class_list.should include("sidebar_section") - end - - it "should have an id based on the title" do - html.id.should == "help-section_sidebar_section" - end - - it "should have a contents div" do - html.find_by_tag("div").first.class_list.should include("panel_contents") - end - - it "should add children to the contents div" do - html.find_by_tag("span").first.parent.should == html.find_by_tag("div").first - end - -end diff --git a/spec/unit/views/components/site_title_spec.rb b/spec/unit/views/components/site_title_spec.rb deleted file mode 100644 index 110ecd636e1..00000000000 --- a/spec/unit/views/components/site_title_spec.rb +++ /dev/null @@ -1,78 +0,0 @@ -require 'spec_helper' - -describe ActiveAdmin::Views::SiteTitle do - - let(:helpers){ mock_action_view } - - def build_title(namespace) - render_arbre_component({:namespace => namespace}, helpers) do - insert_tag ActiveAdmin::Views::SiteTitle, assigns[:namespace] - end - end - - context "when a value" do - - it "renders the string when a string is passed in" do - namespace = mock :site_title => "Hello World", - :site_title_image => nil, - :site_title_link => nil - - site_title = build_title(namespace) - site_title.content.should == "Hello World" - end - - it "renders the return value of a method when a symbol" do - helpers.should_receive(:hello_world).and_return("Hello World") - - namespace = mock :site_title => :hello_world, - :site_title_image => nil, - :site_title_link => nil - - site_title = build_title(namespace) - site_title.content.should == "Hello World" - end - - it "renders the return value of a proc" do - namespace = mock :site_title => proc{ "Hello World" }, - :site_title_image => nil, - :site_title_link => nil - - site_title = build_title(namespace) - site_title.content.should == "Hello World" - end - - end - - context "when an image" do - - it "renders the string when a string is passed in" do - helpers.should_receive(:image_tag). - with("an/image.png", {:alt => nil, :id => "site_title_image"}). - and_return("".html_safe) - - namespace = mock :site_title => nil, - :site_title_image => "an/image.png", - :site_title_link => nil - - site_title = build_title(namespace) - site_title.content.strip.should == "" - end - - end - - context "when a link is present" do - - it "renders the string when a string is passed in" do - namespace = mock :site_title => "Hello World", - :site_title_image => nil, - :site_title_link => "/" - - site_title = build_title(namespace) - site_title.content.should == "Hello World" - end - - end - - - -end diff --git a/spec/unit/views/components/status_tag_spec.rb b/spec/unit/views/components/status_tag_spec.rb index dd647c06ccf..c921912a29d 100644 --- a/spec/unit/views/components/status_tag_spec.rb +++ b/spec/unit/views/components/status_tag_spec.rb @@ -1,95 +1,310 @@ -require 'spec_helper' +# frozen_string_literal: true +require "rails_helper" -describe ActiveAdmin::Views::StatusTag do +RSpec.describe ActiveAdmin::Views::StatusTag do + # Helper method to build StatusTag objects in an Arbre context + def status_tag(*args) + render_arbre_component(status_tag_args: args) do + status_tag(*assigns[:status_tag_args]) + end + end - describe "#status_tag" do + describe "#tag_name" do + subject { status_tag(nil).tag_name } + it { is_expected.to eq "span" } + end - # Helper method to build StatusTag objects in an Arbre context - def status_tag(*args) - render_arbre_component(:status_tag_args => args) do - status_tag(*assigns[:status_tag_args]) - end + context "when status is 'completed'" do + subject { status_tag("completed") } + + describe "#class_list" do + subject { super().class_list.to_a } + it { is_expected.to contain_exactly("status-tag") } end - subject { status_tag(nil) } + describe "#content" do + subject { super().content } + it { is_expected.to eq "Completed" } + end + + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "completed") } + end + end + + context "when status is :in_progress" do + subject { status_tag(:in_progress) } + + describe "#class_list" do + subject { super().class_list.to_a } + it { is_expected.to contain_exactly("status-tag") } + end + + describe "#content" do + subject { super().content } + it { is_expected.to eq "In Progress" } + end + + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "in_progress") } + end + end + + context "when status is 'in_progress'" do + subject { status_tag("in_progress") } + + describe "#class_list" do + subject { super().class_list.to_a } + it { is_expected.to contain_exactly("status-tag") } + end + + describe "#content" do + subject { super().content } + it { is_expected.to eq "In Progress" } + end + + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "in_progress") } + end + end + + context "when status is 'In progress'" do + subject { status_tag("In progress") } + + describe "#class_list" do + subject { super().class_list.to_a } + it { is_expected.to contain_exactly("status-tag") } + end + + describe "#content" do + subject { super().content } + it { is_expected.to eq "In Progress" } + end + + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "in_progress") } + end + end + + context "when status is an empty string" do + subject { status_tag("") } + + describe "#class_list" do + subject { super().class_list.to_a } + it { is_expected.to contain_exactly("status-tag") } + end + + describe "#content" do + subject { super().content } + it { is_expected.to eq "" } + end + + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "") } + end + end + + context "when status is 'true'" do + subject { status_tag("true") } + + describe "#class_list" do + subject { super().class_list.to_a } + it { is_expected.to contain_exactly("status-tag") } + end + + describe "#content" do + subject { super().content } + it { is_expected.to eq("Yes") } + end + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "yes") } + end + end + + context "when status is true" do + subject { status_tag(true) } + + describe "#class_list" do + subject { super().class_list.to_a } + it { is_expected.to contain_exactly("status-tag") } + end + + describe "#content" do + subject { super().content } + it { is_expected.to eq("Yes") } + end + + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "yes") } + end + end + + context "when status is 'false'" do + subject { status_tag("false") } + + describe "#class_list" do + subject { super().class_list.to_a } + it { is_expected.to contain_exactly("status-tag") } + end + + describe "#content" do + subject { super().content } + it { is_expected.to eq("No") } + end + + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "no") } + end + end + + context "when status is false" do + subject { status_tag(false) } + + describe "#class_list" do + subject { super().class_list.to_a } + it { is_expected.to contain_exactly("status-tag") } + end + + describe "#content" do + subject { super().content } + it { is_expected.to eq("No") } + end - its(:tag_name) { should == 'span' } - its(:class_list) { should include('status_tag') } + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "no") } + end + end + + context "when status is nil" do + subject { status_tag(nil) } + + describe "#class_list" do + subject { super().class_list.to_a } + it { is_expected.to contain_exactly("status-tag") } + end - context "when status is 'completed'" do - subject { status_tag('completed') } + describe "#content" do + subject { super().content } + it { is_expected.to eq("Unknown") } + end - its(:tag_name) { should == 'span' } - its(:class_list) { should include('status_tag') } - its(:class_list) { should include('completed') } - its(:content) { should == 'Completed' } + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "unset") } end - context "when status is 'in_progress'" do - subject { status_tag('in_progress') } + describe "with locale override" do + around do |example| + with_translation %i[active_admin status_tag unset], "Unspecified" do + example.run + end + end + + describe "#class_list" do + subject { super().class_list.to_a } + it { is_expected.to contain_exactly("status-tag") } + end + + describe "#content" do + subject { super().content } + it { is_expected.to eq("Unspecified") } + end - its(:class_list) { should include('in_progress') } - its(:content) { should == 'In Progress' } + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "unset") } + end end + end - context "when status is 'In progress'" do - subject { status_tag('In progress') } + context "when status is 'Active' and class is 'ok'" do + subject { status_tag("Active", class: "ok") } - its(:class_list) { should include('in_progress') } - its(:content) { should == 'In Progress' } + describe "#content" do + subject { super().content } + it { is_expected.to eq "Active" } end - context "when status is an empty string" do - subject { status_tag('') } + describe "#class_list" do + subject { super().class_list.to_a } + it { is_expected.to contain_exactly("status-tag", "ok") } + end - its(:class_list) { should include('status_tag') } - its(:content) { should == '' } + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "active") } end + end - context "when status is nil" do - subject { status_tag(nil) } + context "when status is 'Active' and label is 'on'" do + subject { status_tag("Active", label: "on") } - its(:class_list) { should include('status_tag') } - its(:content) { should == '' } + describe "#content" do + subject { super().content } + it { is_expected.to eq "on" } end - context "when status is 'Active' and type is :ok" do - subject { status_tag('Active', :ok) } + describe "#class_list" do + subject { super().class_list.to_a } + it { is_expected.to contain_exactly("status-tag") } + end - its(:class_list) { should include('status_tag') } - its(:class_list) { should include('active') } - its(:class_list) { should include('ok') } + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "active") } end + end + + context "when status is 'So useless', class is 'woot awesome' and id is 'useless'" do + subject { status_tag("So useless", class: "woot awesome", id: "useless") } - context "when status is 'Active' and class is 'ok'" do - subject { status_tag('Active', :class => 'ok') } + describe "#content" do + subject { super().content } + it { is_expected.to eq "So Useless" } + end - its(:class_list) { should include('status_tag') } - its(:class_list) { should include('active') } - its(:class_list) { should include('ok') } + describe "#class_list" do + subject { super().class_list.to_a } + it { is_expected.to contain_exactly("status-tag", "woot", "awesome") } end - context "when status is 'Active' and label is 'on'" do - subject { status_tag('Active', :label => 'on') } + describe "#id" do + subject { super().id } + it { is_expected.to eq "useless" } + end - its(:content) { should == 'on' } - its(:class_list) { should include('status_tag') } - its(:class_list) { should include('active') } - its(:class_list) { should_not include('on') } + describe "#attributes" do + subject { super().attributes } + it { is_expected.to include("data-status": "so_useless") } end + end - context "when status is 'So useless', type is :ok, class is 'woot awesome' and id is 'useless'" do - subject { status_tag('So useless', :ok, :class => 'woot awesome', :id => 'useless') } + context "when status is set to a Fixnum" do + subject { status_tag(42) } - its(:content) { should == 'So Useless' } - its(:class_list) { should include('status_tag') } - its(:class_list) { should include('ok') } - its(:class_list) { should include('so_useless') } - its(:class_list) { should include('woot') } - its(:class_list) { should include('awesome') } - its(:id) { should == 'useless' } + describe "#content" do + subject { super().content } + it { is_expected.to eq "42" } end - end # describe "#status_tag" + describe "#class_list" do + subject { super().class_list.to_a } + it { is_expected.to contain_exactly("status-tag") } + end + + describe "#attributes" do + subject { super().attributes } + it { is_expected.to_not include("data-status": "42") } + end + end end diff --git a/spec/unit/views/components/table_for_spec.rb b/spec/unit/views/components/table_for_spec.rb index bb41671d64b..7778d255884 100644 --- a/spec/unit/views/components/table_for_spec.rb +++ b/spec/unit/views/components/table_for_spec.rb @@ -1,14 +1,104 @@ -require 'spec_helper' +# frozen_string_literal: true +require "rails_helper" -describe ActiveAdmin::Views::TableFor do +RSpec.describe ActiveAdmin::Views::TableFor do describe "creating with the dsl" do - let(:collection) do - [Post.new(:title => "First Post"), Post.new(:title => "Second Post"), Post.new(:title => "Third Post")] + [ + Post.new(title: "First Post", starred: true), + Post.new(title: "Second Post"), + Post.new(title: "Third Post", starred: false) + ] + end + + let(:assigns) { { collection: collection } } + let(:helpers) { mock_action_view } + + context "when creating a column using symbol argument" do + let(:table) do + render_arbre_component assigns, helpers do + table_for(collection, :title) + end + end + + it "should create a table header based on the symbol" do + expect(table.find_by_tag("th").first.content).to eq "Title" + end + + it "should create a table row for each element in the collection" do + expect(table.find_by_tag("tr").size).to eq 4 # 1 for head, 3 for rows + end + + ["First Post", "Second Post", "Third Post"].each_with_index do |content, index| + it "should create a cell with #{content}" do + expect(table.find_by_tag("td")[index].content).to eq content + end + end + end + + context "when creating many columns using symbol arguments" do + let(:table) do + render_arbre_component assigns, helpers do + table_for(collection, :title, :created_at) + end + end + + it "should create a table header based on the symbol" do + expect(table.find_by_tag("th").first.content).to eq "Title" + expect(table.find_by_tag("th").last.content).to eq "Created At" + end + + it "should add a data attribute to each table header based on the column name" do + expect(table.find_by_tag("th").first.attributes).to include("data-column": "title") + expect(table.find_by_tag("th").last.attributes).to include("data-column": "created_at") + end + + it "should create a table row for each element in the collection" do + expect(table.find_by_tag("tr").size).to eq 4 # 1 for head, 3 for rows + end + + it "should create a cell for each column" do + expect(table.find_by_tag("td").size).to eq 6 + end + + it "should add a data attribute for each cell based on the column name" do + expect(table.find_by_tag("td").first.attributes).to include("data-column": "title") + expect(table.find_by_tag("td").last.attributes).to include("data-column": "created_at") + end end - let(:assigns){ { :collection => collection } } - let(:helpers){ mock_action_view } + context "when creating a column using symbol arguments and another using block" do + let(:table) do + render_arbre_component assigns, helpers do + table_for(collection, :title) do + column :created_at + end + end + end + + it "should create a table header based on the symbol" do + expect(table.find_by_tag("th").first.content).to eq "Title" + expect(table.find_by_tag("th").last.content).to eq "Created At" + end + + it "should add a data attribute to each table header based on the column name" do + expect(table.find_by_tag("th").first.attributes).to include("data-column": "title") + expect(table.find_by_tag("th").last.attributes).to include("data-column": "created_at") + end + + it "should create a table row for each element in the collection" do + expect(table.find_by_tag("tr").size).to eq 4 # 1 for head, 3 for rows + end + + it "should create a cell for each column" do + expect(table.find_by_tag("td").size).to eq 6 + end + + it "should add a data attribute for each cell based on the column name" do + expect(table.find_by_tag("td").first.attributes).to include("data-column": "title") + expect(table.find_by_tag("td").last.attributes).to include("data-column": "created_at") + end + end context "when creating a column with a symbol" do let(:table) do @@ -20,16 +110,16 @@ end it "should create a table header based on the symbol" do - table.find_by_tag("th").first.content.should == "Title" + expect(table.find_by_tag("th").first.content).to eq "Title" end it "should create a table row for each element in the collection" do - table.find_by_tag("tr").size.should == 4 # 1 for head, 3 for rows + expect(table.find_by_tag("tr").size).to eq 4 # 1 for head, 3 for rows end ["First Post", "Second Post", "Third Post"].each_with_index do |content, index| it "should create a cell with #{content}" do - table.find_by_tag("td")[index].content.should == content + expect(table.find_by_tag("td")[index].content).to eq content end end end @@ -45,26 +135,26 @@ end it "should create a table header based on the symbol" do - table.find_by_tag("th").first.content.should == "Title" - table.find_by_tag("th").last.content.should == "Created At" + expect(table.find_by_tag("th").first.content).to eq "Title" + expect(table.find_by_tag("th").last.content).to eq "Created At" end - it "should add a class to each table header based on the col name" do - table.find_by_tag("th").first.class_list.should include("title") - table.find_by_tag("th").last.class_list.should include("created_at") + it "should add a data attribute to each table header based on the column name" do + expect(table.find_by_tag("th").first.attributes).to include("data-column": "title") + expect(table.find_by_tag("th").last.attributes).to include("data-column": "created_at") end it "should create a table row for each element in the collection" do - table.find_by_tag("tr").size.should == 4 # 1 for head, 3 for rows + expect(table.find_by_tag("tr").size).to eq 4 # 1 for head, 3 for rows end it "should create a cell for each column" do - table.find_by_tag("td").size.should == 6 + expect(table.find_by_tag("td").size).to eq 6 end - it "should add a class for each cell based on the col name" do - table.find_by_tag("td").first.class_list.should include("title") - table.find_by_tag("td").last.class_list.should include("created_at") + it "should add a data attribute for each cell based on the column name" do + expect(table.find_by_tag("td").first.attributes).to include("data-column": "title") + expect(table.find_by_tag("td").last.attributes).to include("data-column": "created_at") end end @@ -79,15 +169,16 @@ end end - it "should add a class to each table header based on the col name" do - table.find_by_tag("th").first.class_list.should include("title") + it "should add a data attribute to each table header based on the column name" do + expect(table.find_by_tag("th").first.attributes).to include("data-column": "title") end [ "First Post", "Second Post", - "Third Post" ].each_with_index do |content, index| + "Third Post" + ].each_with_index do |content, index| it "should create a cell with #{content}" do - table.find_by_tag("td")[index].content.strip.should == content + expect(table.find_by_tag("td")[index].content.strip).to eq content end end end @@ -106,41 +197,239 @@ 3.times do |index| it "should create a cell with multiple elements in row #{index}" do - table.find_by_tag("td")[index].find_by_tag("span").size.should == 2 + expect(table.find_by_tag("td")[index].find_by_tag("span").size).to eq 2 end end end - context "when creating many columns with symbols, blocks and strings" do let(:table) do render_arbre_component assigns, helpers do table_for(collection) do column "My Custom Title", :title - column :created_at , :class=>"datetime" + column :created_at, class: "datetime" + end + end + end + + it "should add a data attribute to each header based on class option or the column name" do + expect(table.find_by_tag("th").first.attributes).to include("data-column": "my_custom_title") + expect(table.find_by_tag("th").last.attributes).to include("data-column": "created_at") + expect(table.find_by_tag("th").last.class_list.to_s).to eq "datetime" + end + + it "should add a class to each cell based on class option" do + expect(table.find_by_tag("td").first.class_list.to_s).to eq "" + expect(table.find_by_tag("td").last.class_list.to_s).to eq "datetime" + end + + it "should add a data attribute for each cell based on the column name" do + expect(table.find_by_tag("td").first.attributes).to include("data-column": "my_custom_title") + expect(table.find_by_tag("td").last.attributes).to include("data-column": "created_at") + end + end + + context "when adding html to a sortable column header" do + before { allow(helpers).to receive(:url_target) { "routing_stub" } } + + let(:active_admin_config) { ActiveAdmin::Namespace.new(ActiveAdmin::Application.new, :admin) } + let(:assigns) { { collection: collection, active_admin_config: active_admin_config } } + let(:table) do + render_arbre_component assigns, helpers do + span = content_tag :span, 'Title' + table_for(collection, sortable: true) do + column(span, sortable: :title) { |post| post.title } + end + end + end + + it "should not escape the inner html" do + expect(table.find_by_tag("th").first.content).to include(" e { "starred" if e.starred }) do + column :starred end end end + it "should render boolean attribute within status tag" do + trs = table.find_by_tag("tr") + expect(trs.size).to eq 4 + expect(trs.first.class_list.to_s).to eq "" + expect(trs.second.class_list.to_s).to eq "starred" + expect(trs.third.class_list.to_s).to eq "" + expect(trs.fourth.class_list.to_s).to eq "" + end + end + + context "with row_html options (takes precedence over deprecated row_class)" do + let(:table) do + render_arbre_component assigns, helpers do + table_for( + collection, + row_class: -> e { "foo" }, + row_html: -> e { + { + class: ("starred" if e.starred), + data: { title: e.title }, + } + } + ) do + column :starred + end + end + end - it "should add a class to each table header based on class option or the col name" do - table.find_by_tag("th").first.class_list.should include("my_custom_title") - table.find_by_tag("th").last.class_list.should include("datetime") + it "should render html attributes within collection row" do + trs = table.find_by_tag("tr") + expect(trs.size).to eq 4 + expect(trs.first.attributes).to be_empty + expect(trs.second.attributes).to include(class: "starred", data: { title: "First Post" }) + expect(trs.third.attributes).to include(class: nil, data: { title: "Second Post" }) + expect(trs.fourth.attributes).to include(class: nil, data: { title: "Third Post" }) end + end - it "should add a class to each cell based on class option or the col name" do - table.find_by_tag("td").first.class_list.should include("my_custom_title") - table.find_by_tag("td").last.class_list.should include("datetime") + context "when i18n option is specified" do + around do |example| + with_translation %i[activerecord attributes post title], "Name" do + example.call + end end + let(:table) do + render_arbre_component assigns, helpers do + table_for(collection, i18n: Post) do + column :title + end + end + end + it "should use localized column key" do + expect(table.find_by_tag("th").first.content).to eq "Name" + end end + context "when i18n option is not specified" do + around do |example| + with_translation %i[activerecord attributes post title], "Name" do + example.call + end + end + + let(:collection) do + Post.create( + [ + { title: "First Post", starred: true }, + { title: "Second Post" }, + ] + ) + Post.where(starred: true) + end + + let(:table) do + render_arbre_component assigns, helpers do + table_for(collection) do + column :title + end + end + end + it "should predict localized key based on AR collection klass" do + expect(table.find_by_tag("th").first.content).to eq "Name" + end + end end describe "column sorting" do - def build_column(*args, &block) ActiveAdmin::Views::TableFor::Column.new(*args, &block) end @@ -148,35 +437,78 @@ def build_column(*args, &block) subject { table_column } context "when default" do - let(:table_column){ build_column(:username) } - it { should be_sortable } - its(:sort_key){ should == "username" } + let(:table_column) { build_column(:username) } + it { is_expected.to be_sortable } + + describe "#sort_key" do + subject { super().sort_key } + it { is_expected.to eq("username") } + end end context "when a block given with no sort key" do - let(:table_column){ build_column("Username"){ } } - it { should_not be_sortable } + let(:table_column) { build_column("Username") {} } + it { is_expected.to be_sortable } + + describe "#sort_key" do + subject { super().sort_key } + it { is_expected.to eq("Username") } + end end context "when a block given with a sort key" do - let(:table_column){ build_column("Username", :sortable => :username){ } } - it { should be_sortable } - its(:sort_key){ should == "username" } + let(:table_column) { build_column("Username", sortable: :username) {} } + it { is_expected.to be_sortable } + + describe "#sort_key" do + subject { super().sort_key } + it { is_expected.to eq("username") } + end + end + + context "when a block given with virtual attribute and no sort key" do + let(:table_column) { build_column(:virtual, nil, Post) {} } + it { is_expected.not_to be_sortable } end - context "when :sortable => false with a symbol" do - let(:table_column){ build_column(:username, :sortable => false) } - it { should_not be_sortable } + context "when symbol given as a data column should be sortable" do + let(:table_column) { build_column("Username column", :username) } + it { is_expected.to be_sortable } + + describe "#sort_key" do + subject { super().sort_key } + it { is_expected.to eq "username" } + end end - context "when :sortable => false with a symbol and string" do - let(:table_column){ build_column("Username", :username, :sortable => false) } - it { should_not be_sortable } + context "when sortable: true with a symbol and string" do + let(:table_column) { build_column("Username column", :username, sortable: true) } + it { is_expected.to be_sortable } + + describe "#sort_key" do + subject { super().sort_key } + it { is_expected.to eq "username" } + end + end + + context "when sortable: false with a symbol" do + let(:table_column) { build_column(:username, sortable: false) } + it { is_expected.not_to be_sortable } + end + + context "when sortable: false with a symbol and string" do + let(:table_column) { build_column("Username", :username, sortable: false) } + it { is_expected.not_to be_sortable } end context "when :sortable column is an association" do - let(:table_column){ build_column("Category", :category, Post) } - it { should_not be_sortable } + let(:table_column) { build_column("Category", :category, Post) } + it { is_expected.not_to be_sortable } + end + + context "when :sortable column is an association and block given" do + let(:table_column) { build_column("Category", :category, Post) {} } + it { is_expected.not_to be_sortable } end end end diff --git a/spec/unit/views/pages/layout_spec.rb b/spec/unit/views/pages/layout_spec.rb deleted file mode 100644 index 102456f9c2f..00000000000 --- a/spec/unit/views/pages/layout_spec.rb +++ /dev/null @@ -1,63 +0,0 @@ -require 'spec_helper' - -describe ActiveAdmin::Views::Pages::Layout do - - let(:assigns){ {} } - let(:helpers) do - helpers = mock_action_view - - helpers.stub :active_admin_application => active_admin_application, - :active_admin_config => mock('Config', :action_items? => nil, :breadcrumb => nil, :sidebar_sections? => nil), - :active_admin_namespace => active_admin_namespace, - :breadcrumb_links => [], - :content_for => "", - :csrf_meta_tag => "", - :current_active_admin_user => nil, - :current_active_admin_user? => false, - :current_menu => mock('Menu', :items => []), - :flash => {}, - :javascript_path => "/dummy/", - :link_to => "", - :render_or_call_method_or_proc_on => "", - :stylesheet_link_tag => mock(:html_safe => ""), - :view_factory => view_factory, - :params => {:controller => 'UsersController', :action => 'edit'} - - helpers - end - - let(:active_admin_namespace){ ActiveAdmin::Namespace.new(ActiveAdmin::Application.new, :myspace) } - let(:active_admin_application){ ActiveAdmin.application } - let(:view_factory) { ActiveAdmin::ViewFactory.new } - - let(:layout) do - render_arbre_component assigns, helpers do - insert_tag ActiveAdmin::Views::Pages::Layout - end - end - - it "should be the @page_title if assigned in the controller" do - assigns[:page_title] = "My Page Title" - - layout.title.should == "My Page Title" - end - - it "should be the default translation" do - helpers.params[:action] = "edit" - - layout.title.should == "Edit" - end - - describe "the body" do - - it "should have class 'active_admin'" do - layout.build.class_list.should include 'active_admin' - end - - it "should have namespace class" do - layout.build.class_list.should include "#{active_admin_namespace.name}_namespace" - end - - end - -end diff --git a/spec/unit/views/pages/show_spec.rb b/spec/unit/views/pages/show_spec.rb deleted file mode 100644 index 60a3a19c1d2..00000000000 --- a/spec/unit/views/pages/show_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'spec_helper' - - -describe ActiveAdmin::Views::Pages::Show do - - describe "the resource" do - let(:helpers) { mock(:resource => resource) } - let(:arbre_context) { OpenStruct.new(:helpers => helpers) } - - context 'when the resource does not respond to #decorator' do - let(:resource) { 'Test Resource' } - - it "normally returns the resource" do - page = ActiveAdmin::Views::Pages::Show.new(arbre_context) - page.resource.should == 'Test Resource' - end - end - - end - -end diff --git a/spec/unit/views/tabbed_navigation_spec.rb b/spec/unit/views/tabbed_navigation_spec.rb deleted file mode 100644 index 993b7037575..00000000000 --- a/spec/unit/views/tabbed_navigation_spec.rb +++ /dev/null @@ -1,149 +0,0 @@ -require 'spec_helper' - -include ActiveAdmin -describe ActiveAdmin::Views::TabbedNavigation do - - let(:menu){ ActiveAdmin::Menu.new } - - let(:assigns){ { :active_admin_menu => menu } } - let(:helpers){ mock_action_view } - - let(:tabbed_navigation) do - arbre(assigns, helpers) { - insert_tag(ActiveAdmin::Views::TabbedNavigation, active_admin_menu) - }.children.first - end - - let(:html) { tabbed_navigation.to_s } - - before do - helpers.stub!(:admin_logged_in?).and_return(false) - end - - describe "rendering a menu" do - - before do - menu.add :label => "Blog Posts", :url => :admin_posts_path - - menu.add :label => "Reports", :url => "/admin/reports" do |reports| - reports.add :label => "A Sub Reports", :url => "/admin/a-sub-reports" - reports.add :label => "B Sub Reports", :url => "/admin/b-sub-reports" - reports.add :label => proc{ "Label Proc Sub Reports" }, :url => "/admin/label-proc-sub-reports", :id => "Label Proc Sub Reports" - end - - menu.add :label => "Administration", :url => "/admin/administration" do |administration| - administration.add :label => "User administration", - :url => '/admin/user-administration', - :priority => 10, - :if => proc { false } - end - - menu.add :label => "Management", :url => "#" do |management| - management.add :label => "Order management", - :url => '/admin/order-management', - :priority => 10, - :if => proc { false } - management.add :label => "Bill management", - :url => '/admin/bill-management', - :priority => 10, - :if => :admin_logged_in? - end - end - - it "should generate a ul" do - html.should have_tag("ul") - end - - it "should generate an li for each item" do - html.should have_tag("li", :parent => { :tag => "ul" }) - end - - it "should generate a link for each item" do - html.should have_tag("a", "Blog Posts", :attributes => { :href => '/admin/posts' }) - end - - it "should generate a nested list for children" do - html.should have_tag("ul", :parent => { :tag => "li" }) - end - - it "should generate a nested list with li for each child" do - html.should have_tag("li", :parent => { :tag => "ul" }, :attributes => {:id => "a_sub_reports"}) - html.should have_tag("li", :parent => { :tag => "ul" }, :attributes => {:id => "b_sub_reports"}) - end - - it "should generate a valid id from a label proc" do - html.should have_tag("li", :parent => { :tag => "ul" }, :attributes => {:id => "label_proc_sub_reports"}) - end - - it "should not generate a link for user administration" do - html.should_not have_tag("a", "User administration", :attributes => { :href => '/admin/user-administration' }) - end - - it "should generate the administration parent menu" do - html.should have_tag("a", "Administration", :attributes => { :href => '/admin/administration' }) - end - - it "should not generate a link for order management" do - html.should_not have_tag("a", "Order management", :attributes => { :href => '/admin/order-management' }) - end - - it "should not generate a link for bill management" do - html.should_not have_tag("a", "Bill management", :attributes => { :href => '/admin/bill-management' }) - end - - it "should not generate the management parent menu" do - html.should_not have_tag("a", "Management", :attributes => { :href => '#' }) - end - - describe "marking current item" do - - it "should add the 'current' class to the li" do - assigns[:current_tab] = menu["Blog Posts"] - html.should have_tag("li", :attributes => { :class => "current" }) - end - - it "should add the 'current' and 'has_nested' classes to the li and 'current' to the sub li" do - assigns[:current_tab] = menu["Reports"]["A Sub Reports"] - html.should have_tag("li", :attributes => { :id => "reports", :class => /current/ }) - html.should have_tag("li", :attributes => { :id => "reports", :class => /has_nested/ }) - html.should have_tag("li", :attributes => { :id => "a_sub_reports", :class => "current" }) - end - - end - - end - - describe "returning the menu items to display" do - - it "should return one item with no if block" do - menu.add :label => "Hello World", :url => "/" - tabbed_navigation.menu_items.should == menu.items - end - - it "should not include menu items with an if block that returns false" do - menu.add :label => "Don't Show", :url => "/", :priority => 10, :if => proc{ false } - tabbed_navigation.menu_items.should be_empty - end - - it "should not include menu items with an if block that calls a method that returns false" do - menu.add :label => "Don't Show", :url => "/", :priority => 10, :if => :admin_logged_in? - tabbed_navigation.menu_items.should be_empty - end - - it "should not display any items that have no children to display" do - menu.add :label => "Parent", :url => "#" do |p| - p.add :label => "Child", :url => "/", :priority => 10, :if => proc{ false } - end - tabbed_navigation.menu_items.should be_empty - end - - it "should display a parent that has a child to display" do - menu.add :label => "Parent", :url => "#" do |p| - p.add :label => "Hidden Child", :url => "/", :priority => 10, :if => proc{ false } - p.add :label => "Child", :url => "/" - end - tabbed_navigation.should have(1).menu_items - end - - end -end diff --git a/tasks/bug_report_template.rb b/tasks/bug_report_template.rb new file mode 100644 index 00000000000..0b6c0b92eeb --- /dev/null +++ b/tasks/bug_report_template.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: true +require "bundler/inline" + +gemfile(true) do + source "https://rubygems.org" + + # Use `ACTIVE_ADMIN_PATH=. ruby tasks/bug_report_template.rb` to run + # locally, otherwise run against the default branch. + if ENV["ACTIVE_ADMIN_PATH"] + gem "activeadmin", path: ENV["ACTIVE_ADMIN_PATH"], require: false + else + gem "activeadmin", github: "activeadmin/activeadmin", require: false + end + + # Change Rails version if necessary. + gem "rails", "~> 8.1.0" + + gem "sprockets", "~> 4.0" + gem "importmap-rails", "~> 2.0" + gem "sqlite3", force_ruby_platform: true, platform: :mri + + # Fixes an issue on CI with default gems when using inline bundle with default + # gems that are already activated + # Ref: rubygems/rubygems#6386 + if ENV["CI"] + require "net/protocol" + require "timeout" + + gem "net-protocol", Net::Protocol::VERSION + gem "timeout", Timeout::VERSION + end +end + +require "active_record" + +ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") +ActiveRecord::Base.logger = Logger.new(STDOUT) + +ActiveRecord::Schema.define do + create_table :active_admin_comments, force: true do |_t| + end + + create_table :users, force: true do |t| + t.string :full_name + end +end + +require "action_controller/railtie" +require "action_view/railtie" +require "active_admin" + +class TestApp < Rails::Application + config.root = __dir__ + config.hosts << ".example.com" + config.session_store :cookie_store, key: "cookie_store_key" + config.secret_key_base = "secret_key_base" + config.eager_load = false + + config.logger = Logger.new($stdout) + Rails.logger = config.logger +end + +class ApplicationController < ActionController::Base + include Rails.application.routes.url_helpers +end + +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class + + def self.ransackable_attributes(auth_object = nil) + authorizable_ransackable_attributes + end + + def self.ransackable_associations(auth_object = nil) + authorizable_ransackable_associations + end +end + +class User < ApplicationRecord +end + +ActiveAdmin.setup do |config| + # Authentication disabled by default. Override if necessary. + config.authentication_method = false + config.current_user_method = false +end + +Rails.application.initialize! + +ActiveAdmin.register_page "Dashboard" do + menu priority: 1, label: proc { I18n.t("active_admin.dashboard") } + content do + "Test Me" + end +end + +ActiveAdmin.register User do +end + +Rails.application.routes.draw do + ActiveAdmin.routes(self) +end + +require "minitest/autorun" +require "rack/test" +require "rails/test_help" + +# Replace this with the code necessary to make your test fail. +class BugTest < ActionDispatch::IntegrationTest + + def test_admin_root_success? + get admin_root_url + assert_match "Test Me", response.body # has content + assert_match "Users", response.body # has 'Your Models' in menu + assert_response :success + end + + def test_admin_users + User.create! full_name: "John Doe" + get admin_users_url + assert_match "John Doe", response.body # has created row + assert_response :success + end + + private + + def app + Rails.application + end +end diff --git a/tasks/dependencies.rake b/tasks/dependencies.rake new file mode 100644 index 00000000000..970f35967cb --- /dev/null +++ b/tasks/dependencies.rake @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +namespace :dependencies do + desc "Copy package.json dependencies into vendor/javascript" + task :vendor do + node_modules = File.expand_path("../node_modules", __dir__) + vendor = File.expand_path("../vendor/javascript", __dir__) + + # Copy flowbite to vendor + FileUtils.cp( + File.join(node_modules, 'flowbite', 'dist', 'flowbite.min.js'), + File.join(vendor, 'flowbite.js') + ) + + # Delete sourcemaps refs + Dir.glob(File.join(vendor, '**', '*.js')).each do |file| + content = File.read(file) + + File.write(file, content.gsub(/\/\/# sourceMappingURL=\S+/, '')) + end + rescue Errno::ENOENT + puts "Error: Missing node_modules. Run `npm install`." + end +end diff --git a/tasks/docs.rake b/tasks/docs.rake deleted file mode 100644 index f16f594b448..00000000000 --- a/tasks/docs.rake +++ /dev/null @@ -1,39 +0,0 @@ -namespace :docs do - - def rdoc_to_markdown(content) - content.gsub(/^ ?(=+) /) do |m| - m.gsub('=', '#') - end - end - - def prepare_docstring(content) - content = rdoc_to_markdown(content) - "\n\n#{content}" - end - - def filename_from_module(mod) - mod.name.to_s.underscore.gsub('_', '-') - end - - def write_docstrings_to(path, mods) - mods.each do |mod| - File.open("#{path}/#{filename_from_module(mod)}.md", 'w+') do |f| - f << prepare_docstring(mod.docstring) - end - end - end - - desc "Update docs in the docs folder" - task :build do - require 'yard' - require 'active_support/all' - - YARD::Registry.load! - views = YARD::Registry.at("ActiveAdmin::Views") - - # Index Types - index_types = views.children.select{|obj| obj.name.to_s =~ /^IndexAs/ } - write_docstrings_to "docs/3-index-pages", index_types - end - -end diff --git a/tasks/js.rake b/tasks/js.rake deleted file mode 100644 index e292d7f373b..00000000000 --- a/tasks/js.rake +++ /dev/null @@ -1,32 +0,0 @@ -namespace :js do - - desc "Compile the JS for Rails apps without Asset Pipeline" - task :compile do - require 'sprockets' - require 'uglifier' - require 'fileutils' - - root_dir = File.expand_path(File.join("..", ".."), __FILE__) - js_dir = File.join(root_dir, "app", "assets", "javascripts", "active_admin") - generated_file = File.join(root_dir, 'lib', 'generators', 'active_admin', 'assets', 'templates', '3.0', 'active_admin.js') - - # The base.js file requires jquery. We don't need jquery to - # compile the assets, however Sprockets will try to look it up - # and raise an exception. Insteaad, we move the file out of the directory - # then put it back after we compile. - base_js = File.join(js_dir, "base.js") - tmp_base_js = File.join(root_dir, "base.js") - FileUtils.mv base_js, tmp_base_js - - env = Sprockets::Environment.new - env.js_compressor = ::Uglifier.new - env.append_path js_dir - - File.open generated_file, "w+" do |f| - f << env["application"] - end - - FileUtils.mv tmp_base_js, base_js - end - -end diff --git a/tasks/local.rake b/tasks/local.rake new file mode 100644 index 00000000000..112da1c1cd6 --- /dev/null +++ b/tasks/local.rake @@ -0,0 +1,31 @@ +# frozen_string_literal: true +desc "Run a command against the local sample application" +task :local do + require_relative "test_application" + + test_application = ActiveAdmin::TestApplication.new( + rails_env: "development", + template: "rails_template_with_data" + ) + + test_application.soft_generate + + # Discard the "local" argument (name of the task) + argv = ARGV[1..-1] + + if argv.any? + if %w(server s).include?(argv[0]) + command = "foreman start -f Procfile.dev" + # If it's a rails command, auto add the rails script + elsif %w(generate console dbconsole g c routes runner).include?(argv[0]) || argv[0].include?('db:') + argv.unshift("rails") + command = ["bundle", "exec", *argv].join(" ") + end + + env = { "BUNDLE_GEMFILE" => test_application.expanded_gemfile, "RAILS_ENV" => "development" } + + Dir.chdir(test_application.app_dir) do + Bundler.with_original_env { Kernel.exec(env, command) } + end + end +end diff --git a/tasks/parallel_tests.rake b/tasks/parallel_tests.rake deleted file mode 100644 index 6266fd5e766..00000000000 --- a/tasks/parallel_tests.rake +++ /dev/null @@ -1,60 +0,0 @@ -require 'parallel' - -desc "Run the full suite using parallel_tests to run on multiple cores" -task :parallel_tests => ['parallel:setup_parallel_tests', 'parallel:spec', 'parallel:features', 'cucumber:class_reloading'] - -namespace :parallel do - - def rails_app_rake(task) - require 'rails/version' - system "cd spec/rails/rails-#{Rails::VERSION::STRING}; rake #{task}" - end - - task :after_setup_hook do - rails_app_rake "parallel:load_schema" - rails_app_rake "parallel:create_cucumber_db" - rails_app_rake "parallel:load_schema_cucumber_db" - end - - def parallel_tests_setup? - require 'rails/version' - database_config = File.join "spec", "rails", "rails-#{Rails::VERSION::STRING}", "config", "database.yml" - File.exists?(database_config) && File.read(database_config).include?("TEST_ENV_NUMBER") - end - - desc "Setup parallel_tests DBs" - task :setup_parallel_tests do - unless parallel_tests_setup? - puts "parallel_tests is not set up. (Re)building spec/rails/rails-#{Rails::VERSION::STRING} App. Please wait." - require 'rails/version' - system("rm -Rf spec/rails/rails-#{Rails::VERSION::STRING}") - Rake::Task['setup'].invoke - end - end - - def run_in_parallel(command) - system("ENV['TEST_ENV_NUMBER']=#{Parallel.processor_count} #{command}") - end - - desc "Run the specs in parallel" - task :spec => :setup_parallel_tests do - run_in_parallel "parallel_rspec spec/" - end - - namespace :spec do - - %w(unit integration).each do |type| - desc "Run the #{type} specs in parallel" - task type => :setup_parallel_tests do - run_in_parallel "parallel_rspec spec/#{type}" - end - end - - end - - desc "Run the cucumber features in parallel" - task :features => :setup_parallel_tests do - run_in_parallel "parallel_cucumber features/" - end - -end diff --git a/tasks/release.rake b/tasks/release.rake new file mode 100644 index 00000000000..119f0be502a --- /dev/null +++ b/tasks/release.rake @@ -0,0 +1,13 @@ +# frozen_string_literal: true +require "open3" + +namespace :release do + desc "Publish npm package" + task :npm_push do + npm_version, _error, _status = Open3.capture3("npm pkg get version") + npm_tag = npm_version.include?("-") ? "pre" : "latest" + system "npm", "publish", "--tag", npm_tag, exception: true + end +end + +task(:release).enhance ["release:npm_push"] diff --git a/tasks/test.rake b/tasks/test.rake index 1b3d32619fa..fa4b1625530 100644 --- a/tasks/test.rake +++ b/tasks/test.rake @@ -1,79 +1,76 @@ -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") - system "bundle exec rails new spec/rails/rails-#{Rails::VERSION::STRING} -m spec/support/rails_template.rb" - Rake::Task['parallel:after_setup_hook'].invoke -end +# frozen_string_literal: true +desc "Run the full suite using parallel_tests to run on multiple cores" +task test: [:setup, :spec, :cucumber] -# Run specs and cukes -# If run by Travis, submit test coverage data to www.coveralls.io -desc "Run the full suite using 1 core" -task test: ['spec:unit', 'spec:integration', 'cucumber', 'cucumber:class_reloading'] do - require 'coveralls' and Coveralls.push! if ENV['TRAVIS'] -end +desc "Create a test rails app for the parallel specs to run against if it doesn't exist already" +task setup: :"setup:create" -namespace :test do +namespace :setup do + desc "Forcefully create a test rails app for the parallel specs to run against" + task :force, [:rails_env, :template] => [:require, :rm, :run] - def run_tests_against(*versions) - current_version = detect_rails_version if File.exists?("Gemfile.lock") + desc "Create a test rails app for the parallel specs to run against if it doesn't exist already" + task :create, [:rails_env, :template] => [:require, :run] - versions.each do |version| - puts - puts "== Using Rails #{version}" + desc "Makes test app creation code available" + task :require do + if ENV["COVERAGE"] == "true" + require_relative "../spec/support/simplecov_helper" - cmd "./script/use_rails #{version}" - cmd "bundle exec rspec spec" - cmd "bundle exec cucumber features" - cmd "bundle exec cucumber -p class-reloading features" + ActiveAdmin::TestSupport::SimpleCovHelper.start("test app creation") end - cmd "./script/use_rails #{current_version}" if current_version + require_relative "test_application" end - desc "Run the full suite against the important versions of rails" - task :major_supported_rails do - run_tests_against *TRAVIS_RAILS_VERSIONS + desc "Create a test rails app for the parallel specs to run against" + task :run, [:rails_env, :template] do |_t, opts| + ActiveAdmin::TestApplication.new(opts).soft_generate end - desc "Alias for major_supported_rails" - task :all => :major_supported_rails + task :rm, [:rails_env, :template] do |_t, opts| + test_app = ActiveAdmin::TestApplication.new(opts) + FileUtils.rm_rf test_app.app_dir + end end -require 'rspec/core/rake_task' - -RSpec::Core::RakeTask.new(:spec) +task spec: :"spec:all" namespace :spec do + desc "Run all specs" + task all: [:regular, :filesystem_changes] - desc "Run the unit specs" - RSpec::Core::RakeTask.new(:unit) do |t| - t.pattern = "spec/unit/**/*_spec.rb" + desc "Run the standard specs in parallel" + task :regular do + sh("bin/parallel_rspec") end - desc "Run the integration specs" - RSpec::Core::RakeTask.new(:integration) do |t| - t.pattern = "spec/integration/**/*_spec.rb" + desc "Run the specs that change the filesystem sequentially" + task :filesystem_changes do + sh({ "RSPEC_FILESYSTEM_CHANGES" => "true" }, "bin/rspec") end - end - -require 'cucumber/rake/task' - -Cucumber::Rake::Task.new(:cucumber) do |t| - t.profile = 'default' -end +desc "Run the cucumber scenarios in parallel" +task cucumber: :"cucumber:all" namespace :cucumber do + desc "Run all cucumber suites" + task all: [:regular, :filesystem_changes, :reloading] - Cucumber::Rake::Task.new(:wip, "Run the cucumber scenarios with the @wip tag") do |t| - t.profile = 'wip' + desc "Run the standard cucumber scenarios in parallel" + task :regular do + sh("bin/parallel_cucumber") end - Cucumber::Rake::Task.new(:class_reloading, "Run the cucumber scenarios that test reloading") do |t| - t.profile = 'class-reloading' + desc "Run the cucumber scenarios that change the filesystem sequentially" + task :filesystem_changes do + sh("bin/cucumber --profile filesystem-changes") end + desc "Run the cucumber scenarios that test reloading" + task :reloading do + sh("bin/cucumber --profile class-reloading") + end end diff --git a/tasks/test_application.rb b/tasks/test_application.rb new file mode 100644 index 00000000000..9e895db784e --- /dev/null +++ b/tasks/test_application.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true +require "fileutils" + +module ActiveAdmin + class TestApplication + attr_reader :rails_env, :template + + def initialize(opts = {}) + @rails_env = opts[:rails_env] || "test" + @template = opts[:template] || "rails_template" + end + + def soft_generate + if File.exist? app_dir + puts "test app #{app_dir} already exists; skipping test app generation" + else + generate + end + Bundler.with_original_env do + Kernel.system("npm install") # so tailwindcss/plugin is available for test app + Kernel.system("rake dependencies:vendor") # ensure flowbite is updated for test app + Dir.chdir(app_dir) do + Kernel.system("npm install @activeadmin/activeadmin") + Kernel.system('npm pkg set scripts.build:css="npx @tailwindcss/cli -i ./app/assets/stylesheets/active_admin.css -o ./app/assets/builds/active_admin.css --minify"') + Kernel.system("npm install") + Kernel.system("npm run build:css") + end + end + end + + def generate + FileUtils.mkdir_p base_dir + args = %W( + -m spec/support/#{template}.rb + --skip-action-cable + --skip-action-mailbox + --skip-action-text + --skip-active-storage + --skip-bootsnap + --skip-brakeman + --skip-bundler-audit + --skip-ci + --skip-decrypted-diffs + --skip-dev-gems + --skip-docker + --skip-git + --skip-hotwire + --skip-jbuilder + --skip-kamal + --skip-rubocop + --skip-solid + --skip-system-test + --skip-test + --skip-thruster + --javascript=importmap + ) + + command = ["bundle", "exec", "rails", "new", app_dir, *args].join(" ") + + env = { "BUNDLE_GEMFILE" => expanded_gemfile, "RAILS_ENV" => rails_env } + + Bundler.with_original_env do + Kernel.system(env, command) + end + end + + def full_app_dir + File.expand_path(app_dir) + end + + def app_dir + @app_dir ||= "#{base_dir}/#{app_name}" + end + + def expanded_gemfile + return gemfile if Pathname.new(gemfile).absolute? + + File.expand_path(gemfile) + end + + private + + def base_dir + @base_dir ||= "tmp/#{rails_env}_apps" + end + + def app_name + return "rails_81" if main_app? + + File.basename(File.dirname(gemfile)) + end + + def main_app? + expanded_gemfile == File.expand_path("Gemfile") + end + + def gemfile + gemfile_from_env || "Gemfile" + end + + def gemfile_from_env + ENV["BUNDLE_GEMFILE"] + end + end +end diff --git a/tasks/yard.rake b/tasks/yard.rake deleted file mode 100644 index fcf52c4c25a..00000000000 --- a/tasks/yard.rake +++ /dev/null @@ -1,6 +0,0 @@ -require 'yard' -require 'yard/rake/yardoc_task' - -YARD::Rake::YardocTask.new do |t| - t.files = ['lib/**/*.rb'] -end diff --git a/vendor/javascript/flowbite.js b/vendor/javascript/flowbite.js new file mode 100644 index 00000000000..5024f70bfa0 --- /dev/null +++ b/vendor/javascript/flowbite.js @@ -0,0 +1 @@ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define("Flowbite",[],e):"object"==typeof exports?exports.Flowbite=e():t.Flowbite=e()}(self,(function(){return function(){"use strict";var t={765:function(t,e,i){i.r(e)},853:function(t,e,i){i.r(e),i.d(e,{afterMain:function(){return k},afterRead:function(){return b},afterWrite:function(){return D},applyStyles:function(){return T},arrow:function(){return Q},auto:function(){return s},basePlacements:function(){return d},beforeMain:function(){return _},beforeRead:function(){return y},beforeWrite:function(){return E},bottom:function(){return r},clippingParents:function(){return u},computeStyles:function(){return it},createPopper:function(){return Tt},createPopperBase:function(){return St},createPopperLite:function(){return Mt},detectOverflow:function(){return mt},end:function(){return l},eventListeners:function(){return rt},flip:function(){return bt},hide:function(){return kt},left:function(){return a},main:function(){return w},modifierPhases:function(){return O},offset:function(){return Et},placements:function(){return v},popper:function(){return p},popperGenerator:function(){return Ct},popperOffsets:function(){return xt},preventOverflow:function(){return Dt},read:function(){return m},reference:function(){return f},right:function(){return o},start:function(){return c},top:function(){return n},variationPlacements:function(){return g},viewport:function(){return h},write:function(){return x}});var n="top",r="bottom",o="right",a="left",s="auto",d=[n,r,o,a],c="start",l="end",u="clippingParents",h="viewport",p="popper",f="reference",g=d.reduce((function(t,e){return t.concat([e+"-"+c,e+"-"+l])}),[]),v=[].concat(d,[s]).reduce((function(t,e){return t.concat([e,e+"-"+c,e+"-"+l])}),[]),y="beforeRead",m="read",b="afterRead",_="beforeMain",w="main",k="afterMain",E="beforeWrite",x="write",D="afterWrite",O=[y,m,b,_,w,k,E,x,D];function L(t){return t?(t.nodeName||"").toLowerCase():null}function I(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function A(t){return t instanceof I(t).Element||t instanceof Element}function C(t){return t instanceof I(t).HTMLElement||t instanceof HTMLElement}function S(t){return"undefined"!=typeof ShadowRoot&&(t instanceof I(t).ShadowRoot||t instanceof ShadowRoot)}var T={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},r=e.elements[t];C(r)&&L(r)&&(Object.assign(r.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?r.removeAttribute(t):r.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],r=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});C(n)&&L(n)&&(Object.assign(n.style,o),Object.keys(r).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function M(t){return t.split("-")[0]}var H=Math.max,P=Math.min,j=Math.round;function V(){var t=navigator.userAgentData;return null!=t&&t.brands?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function B(){return!/^((?!chrome|android).)*safari/i.test(V())}function z(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),r=1,o=1;e&&C(t)&&(r=t.offsetWidth>0&&j(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&j(n.height)/t.offsetHeight||1);var a=(A(t)?I(t):window).visualViewport,s=!B()&&i,d=(n.left+(s&&a?a.offsetLeft:0))/r,c=(n.top+(s&&a?a.offsetTop:0))/o,l=n.width/r,u=n.height/o;return{width:l,height:u,top:c,right:d+l,bottom:c+u,left:d,x:d,y:c}}function F(t){var e=z(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function N(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&S(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function W(t){return I(t).getComputedStyle(t)}function q(t){return["table","td","th"].indexOf(L(t))>=0}function R(t){return((A(t)?t.ownerDocument:t.document)||window.document).documentElement}function Y(t){return"html"===L(t)?t:t.assignedSlot||t.parentNode||(S(t)?t.host:null)||R(t)}function K(t){return C(t)&&"fixed"!==W(t).position?t.offsetParent:null}function U(t){for(var e=I(t),i=K(t);i&&q(i)&&"static"===W(i).position;)i=K(i);return i&&("html"===L(i)||"body"===L(i)&&"static"===W(i).position)?e:i||function(t){var e=/firefox/i.test(V());if(/Trident/i.test(V())&&C(t)&&"fixed"===W(t).position)return null;var i=Y(t);for(S(i)&&(i=i.host);C(i)&&["html","body"].indexOf(L(i))<0;){var n=W(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function J(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function X(t,e,i){return H(t,P(e,i))}function $(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function G(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}var Q={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,s=t.name,c=t.options,l=i.elements.arrow,u=i.modifiersData.popperOffsets,h=M(i.placement),p=J(h),f=[a,o].indexOf(h)>=0?"height":"width";if(l&&u){var g=function(t,e){return $("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:G(t,d))}(c.padding,i),v=F(l),y="y"===p?n:a,m="y"===p?r:o,b=i.rects.reference[f]+i.rects.reference[p]-u[p]-i.rects.popper[f],_=u[p]-i.rects.reference[p],w=U(l),k=w?"y"===p?w.clientHeight||0:w.clientWidth||0:0,E=b/2-_/2,x=g[y],D=k-v[f]-g[m],O=k/2-v[f]/2+E,L=X(x,O,D),I=p;i.modifiersData[s]=((e={})[I]=L,e.centerOffset=L-O,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&N(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Z(t){return t.split("-")[1]}var tt={top:"auto",right:"auto",bottom:"auto",left:"auto"};function et(t){var e,i=t.popper,s=t.popperRect,d=t.placement,c=t.variation,u=t.offsets,h=t.position,p=t.gpuAcceleration,f=t.adaptive,g=t.roundOffsets,v=t.isFixed,y=u.x,m=void 0===y?0:y,b=u.y,_=void 0===b?0:b,w="function"==typeof g?g({x:m,y:_}):{x:m,y:_};m=w.x,_=w.y;var k=u.hasOwnProperty("x"),E=u.hasOwnProperty("y"),x=a,D=n,O=window;if(f){var L=U(i),A="clientHeight",C="clientWidth";if(L===I(i)&&"static"!==W(L=R(i)).position&&"absolute"===h&&(A="scrollHeight",C="scrollWidth"),d===n||(d===a||d===o)&&c===l)D=r,_-=(v&&L===O&&O.visualViewport?O.visualViewport.height:L[A])-s.height,_*=p?1:-1;if(d===a||(d===n||d===r)&&c===l)x=o,m-=(v&&L===O&&O.visualViewport?O.visualViewport.width:L[C])-s.width,m*=p?1:-1}var S,T=Object.assign({position:h},f&&tt),M=!0===g?function(t){var e=t.x,i=t.y,n=window.devicePixelRatio||1;return{x:j(e*n)/n||0,y:j(i*n)/n||0}}({x:m,y:_}):{x:m,y:_};return m=M.x,_=M.y,p?Object.assign({},T,((S={})[D]=E?"0":"",S[x]=k?"0":"",S.transform=(O.devicePixelRatio||1)<=1?"translate("+m+"px, "+_+"px)":"translate3d("+m+"px, "+_+"px, 0)",S)):Object.assign({},T,((e={})[D]=E?_+"px":"",e[x]=k?m+"px":"",e.transform="",e))}var it={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,r=void 0===n||n,o=i.adaptive,a=void 0===o||o,s=i.roundOffsets,d=void 0===s||s,c={placement:M(e.placement),variation:Z(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:r,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,et(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:a,roundOffsets:d})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,et(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:d})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}},nt={passive:!0};var rt={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,r=n.scroll,o=void 0===r||r,a=n.resize,s=void 0===a||a,d=I(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,nt)})),s&&d.addEventListener("resize",i.update,nt),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,nt)})),s&&d.removeEventListener("resize",i.update,nt)}},data:{}},ot={left:"right",right:"left",bottom:"top",top:"bottom"};function at(t){return t.replace(/left|right|bottom|top/g,(function(t){return ot[t]}))}var st={start:"end",end:"start"};function dt(t){return t.replace(/start|end/g,(function(t){return st[t]}))}function ct(t){var e=I(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function lt(t){return z(R(t)).left+ct(t).scrollLeft}function ut(t){var e=W(t),i=e.overflow,n=e.overflowX,r=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+r+n)}function ht(t){return["html","body","#document"].indexOf(L(t))>=0?t.ownerDocument.body:C(t)&&ut(t)?t:ht(Y(t))}function pt(t,e){var i;void 0===e&&(e=[]);var n=ht(t),r=n===(null==(i=t.ownerDocument)?void 0:i.body),o=I(n),a=r?[o].concat(o.visualViewport||[],ut(n)?n:[]):n,s=e.concat(a);return r?s:s.concat(pt(Y(a)))}function ft(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function gt(t,e,i){return e===h?ft(function(t,e){var i=I(t),n=R(t),r=i.visualViewport,o=n.clientWidth,a=n.clientHeight,s=0,d=0;if(r){o=r.width,a=r.height;var c=B();(c||!c&&"fixed"===e)&&(s=r.offsetLeft,d=r.offsetTop)}return{width:o,height:a,x:s+lt(t),y:d}}(t,i)):A(e)?function(t,e){var i=z(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):ft(function(t){var e,i=R(t),n=ct(t),r=null==(e=t.ownerDocument)?void 0:e.body,o=H(i.scrollWidth,i.clientWidth,r?r.scrollWidth:0,r?r.clientWidth:0),a=H(i.scrollHeight,i.clientHeight,r?r.scrollHeight:0,r?r.clientHeight:0),s=-n.scrollLeft+lt(t),d=-n.scrollTop;return"rtl"===W(r||i).direction&&(s+=H(i.clientWidth,r?r.clientWidth:0)-o),{width:o,height:a,x:s,y:d}}(R(t)))}function vt(t,e,i,n){var r="clippingParents"===e?function(t){var e=pt(Y(t)),i=["absolute","fixed"].indexOf(W(t).position)>=0&&C(t)?U(t):t;return A(i)?e.filter((function(t){return A(t)&&N(t,i)&&"body"!==L(t)})):[]}(t):[].concat(e),o=[].concat(r,[i]),a=o[0],s=o.reduce((function(e,i){var r=gt(t,i,n);return e.top=H(r.top,e.top),e.right=P(r.right,e.right),e.bottom=P(r.bottom,e.bottom),e.left=H(r.left,e.left),e}),gt(t,a,n));return s.width=s.right-s.left,s.height=s.bottom-s.top,s.x=s.left,s.y=s.top,s}function yt(t){var e,i=t.reference,s=t.element,d=t.placement,u=d?M(d):null,h=d?Z(d):null,p=i.x+i.width/2-s.width/2,f=i.y+i.height/2-s.height/2;switch(u){case n:e={x:p,y:i.y-s.height};break;case r:e={x:p,y:i.y+i.height};break;case o:e={x:i.x+i.width,y:f};break;case a:e={x:i.x-s.width,y:f};break;default:e={x:i.x,y:i.y}}var g=u?J(u):null;if(null!=g){var v="y"===g?"height":"width";switch(h){case c:e[g]=e[g]-(i[v]/2-s[v]/2);break;case l:e[g]=e[g]+(i[v]/2-s[v]/2)}}return e}function mt(t,e){void 0===e&&(e={});var i=e,a=i.placement,s=void 0===a?t.placement:a,c=i.strategy,l=void 0===c?t.strategy:c,g=i.boundary,v=void 0===g?u:g,y=i.rootBoundary,m=void 0===y?h:y,b=i.elementContext,_=void 0===b?p:b,w=i.altBoundary,k=void 0!==w&&w,E=i.padding,x=void 0===E?0:E,D=$("number"!=typeof x?x:G(x,d)),O=_===p?f:p,L=t.rects.popper,I=t.elements[k?O:_],C=vt(A(I)?I:I.contextElement||R(t.elements.popper),v,m,l),S=z(t.elements.reference),T=yt({reference:S,element:L,strategy:"absolute",placement:s}),M=ft(Object.assign({},L,T)),H=_===p?M:S,P={top:C.top-H.top+D.top,bottom:H.bottom-C.bottom+D.bottom,left:C.left-H.left+D.left,right:H.right-C.right+D.right},j=t.modifiersData.offset;if(_===p&&j){var V=j[s];Object.keys(P).forEach((function(t){var e=[o,r].indexOf(t)>=0?1:-1,i=[n,r].indexOf(t)>=0?"y":"x";P[t]+=V[i]*e}))}return P}var bt={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,l=t.name;if(!e.modifiersData[l]._skip){for(var u=i.mainAxis,h=void 0===u||u,p=i.altAxis,f=void 0===p||p,y=i.fallbackPlacements,m=i.padding,b=i.boundary,_=i.rootBoundary,w=i.altBoundary,k=i.flipVariations,E=void 0===k||k,x=i.allowedAutoPlacements,D=e.options.placement,O=M(D),L=y||(O===D||!E?[at(D)]:function(t){if(M(t)===s)return[];var e=at(t);return[dt(t),e,dt(e)]}(D)),I=[D].concat(L).reduce((function(t,i){return t.concat(M(i)===s?function(t,e){void 0===e&&(e={});var i=e,n=i.placement,r=i.boundary,o=i.rootBoundary,a=i.padding,s=i.flipVariations,c=i.allowedAutoPlacements,l=void 0===c?v:c,u=Z(n),h=u?s?g:g.filter((function(t){return Z(t)===u})):d,p=h.filter((function(t){return l.indexOf(t)>=0}));0===p.length&&(p=h);var f=p.reduce((function(e,i){return e[i]=mt(t,{placement:i,boundary:r,rootBoundary:o,padding:a})[M(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}(e,{placement:i,boundary:b,rootBoundary:_,padding:m,flipVariations:E,allowedAutoPlacements:x}):i)}),[]),A=e.rects.reference,C=e.rects.popper,S=new Map,T=!0,H=I[0],P=0;P=0,F=z?"width":"height",N=mt(e,{placement:j,boundary:b,rootBoundary:_,altBoundary:w,padding:m}),W=z?B?o:a:B?r:n;A[F]>C[F]&&(W=at(W));var q=at(W),R=[];if(h&&R.push(N[V]<=0),f&&R.push(N[W]<=0,N[q]<=0),R.every((function(t){return t}))){H=j,T=!1;break}S.set(j,R)}if(T)for(var Y=function(t){var e=I.find((function(e){var i=S.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return H=e,"break"},K=E?3:1;K>0;K--){if("break"===Y(K))break}e.placement!==H&&(e.modifiersData[l]._skip=!0,e.placement=H,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function _t(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function wt(t){return[n,o,r,a].some((function(e){return t[e]>=0}))}var kt={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,r=e.rects.popper,o=e.modifiersData.preventOverflow,a=mt(e,{elementContext:"reference"}),s=mt(e,{altBoundary:!0}),d=_t(a,n),c=_t(s,r,o),l=wt(d),u=wt(c);e.modifiersData[i]={referenceClippingOffsets:d,popperEscapeOffsets:c,isReferenceHidden:l,hasPopperEscaped:u},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":l,"data-popper-escaped":u})}};var Et={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,r=t.name,s=i.offset,d=void 0===s?[0,0]:s,c=v.reduce((function(t,i){return t[i]=function(t,e,i){var r=M(t),s=[a,n].indexOf(r)>=0?-1:1,d="function"==typeof i?i(Object.assign({},e,{placement:t})):i,c=d[0],l=d[1];return c=c||0,l=(l||0)*s,[a,o].indexOf(r)>=0?{x:l,y:c}:{x:c,y:l}}(i,e.rects,d),t}),{}),l=c[e.placement],u=l.x,h=l.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=u,e.modifiersData.popperOffsets.y+=h),e.modifiersData[r]=c}};var xt={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=yt({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}};var Dt={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,s=t.name,d=i.mainAxis,l=void 0===d||d,u=i.altAxis,h=void 0!==u&&u,p=i.boundary,f=i.rootBoundary,g=i.altBoundary,v=i.padding,y=i.tether,m=void 0===y||y,b=i.tetherOffset,_=void 0===b?0:b,w=mt(e,{boundary:p,rootBoundary:f,padding:v,altBoundary:g}),k=M(e.placement),E=Z(e.placement),x=!E,D=J(k),O="x"===D?"y":"x",L=e.modifiersData.popperOffsets,I=e.rects.reference,A=e.rects.popper,C="function"==typeof _?_(Object.assign({},e.rects,{placement:e.placement})):_,S="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),T=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,j={x:0,y:0};if(L){if(l){var V,B="y"===D?n:a,z="y"===D?r:o,N="y"===D?"height":"width",W=L[D],q=W+w[B],R=W-w[z],Y=m?-A[N]/2:0,K=E===c?I[N]:A[N],$=E===c?-A[N]:-I[N],G=e.elements.arrow,Q=m&&G?F(G):{width:0,height:0},tt=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},et=tt[B],it=tt[z],nt=X(0,I[N],Q[N]),rt=x?I[N]/2-Y-nt-et-S.mainAxis:K-nt-et-S.mainAxis,ot=x?-I[N]/2+Y+nt+it+S.mainAxis:$+nt+it+S.mainAxis,at=e.elements.arrow&&U(e.elements.arrow),st=at?"y"===D?at.clientTop||0:at.clientLeft||0:0,dt=null!=(V=null==T?void 0:T[D])?V:0,ct=W+ot-dt,lt=X(m?P(q,W+rt-dt-st):q,W,m?H(R,ct):R);L[D]=lt,j[D]=lt-W}if(h){var ut,ht="x"===D?n:a,pt="x"===D?r:o,ft=L[O],gt="y"===O?"height":"width",vt=ft+w[ht],yt=ft-w[pt],bt=-1!==[n,a].indexOf(k),_t=null!=(ut=null==T?void 0:T[O])?ut:0,wt=bt?vt:ft-I[gt]-A[gt]-_t+S.altAxis,kt=bt?ft+I[gt]+A[gt]-_t-S.altAxis:yt,Et=m&&bt?function(t,e,i){var n=X(t,e,i);return n>i?i:n}(wt,ft,kt):X(m?wt:vt,ft,m?kt:yt);L[O]=Et,j[O]=Et-ft}e.modifiersData[s]=j}},requiresIfExists:["offset"]};function Ot(t,e,i){void 0===i&&(i=!1);var n,r,o=C(e),a=C(e)&&function(t){var e=t.getBoundingClientRect(),i=j(e.width)/t.offsetWidth||1,n=j(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),s=R(e),d=z(t,a,i),c={scrollLeft:0,scrollTop:0},l={x:0,y:0};return(o||!o&&!i)&&(("body"!==L(e)||ut(s))&&(c=(n=e)!==I(n)&&C(n)?{scrollLeft:(r=n).scrollLeft,scrollTop:r.scrollTop}:ct(n)),C(e)?((l=z(e,!0)).x+=e.clientLeft,l.y+=e.clientTop):s&&(l.x=lt(s))),{x:d.left+c.scrollLeft-l.x,y:d.top+c.scrollTop-l.y,width:d.width,height:d.height}}function Lt(t){var e=new Map,i=new Set,n=[];function r(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&r(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||r(t)})),n}var It={placement:"bottom",modifiers:[],strategy:"absolute"};function At(){for(var t=arguments.length,e=new Array(t),i=0;it.length)&&(e=t.length);for(var i=0,n=Array(e);i1?e-1:0),n=1;n=e)&&(void 0===i||t<=i)}function E(t,e,i){return ti?i:t}function x(t,e){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},n=arguments.length>3&&void 0!==arguments[3]?arguments[3]:0,r=arguments.length>4&&void 0!==arguments[4]?arguments[4]:"",o=Object.keys(i).reduce((function(t,e){var r=i[e];return"function"==typeof r&&(r=r(n)),"".concat(t," ").concat(e,'="').concat(r,'"')}),t);r+="<".concat(o,">");var a=n+1;return a\s+/g,">").replace(/\s+2&&void 0!==arguments[2]?arguments[2]:0,n=new Date(t).getDay();return A(t,T(e,i)-T(n,i))}function H(t,e){var i=new Date(t).getFullYear();return Math.floor(i/e)*e}Object.defineProperty(e,"__esModule",{value:!0});var P=/dd?|DD?|mm?|MM?|yy?(?:yy)?/,j=/[\s!-/:-@[-`{-~年月日]+/,V={},B={y:function(t,e){return new Date(t).setFullYear(parseInt(e,10))},m:function(t,e,i){var n=new Date(t),r=parseInt(e,10)-1;if(isNaN(r)){if(!e)return NaN;var o=e.toLowerCase(),a=function(t){return t.toLowerCase().startsWith(o)};if((r=i.monthsShort.findIndex(a))<0&&(r=i.months.findIndex(a)),r<0)return NaN}return n.setMonth(r),n.getMonth()!==F(r)?n.setDate(0):n.getTime()},d:function(t,e){return new Date(t).setDate(parseInt(e,10))}},z={d:function(t){return t.getDate()},dd:function(t){return N(t.getDate(),2)},D:function(t,e){return e.daysShort[t.getDay()]},DD:function(t,e){return e.days[t.getDay()]},m:function(t){return t.getMonth()+1},mm:function(t){return N(t.getMonth()+1,2)},M:function(t,e){return e.monthsShort[t.getMonth()]},MM:function(t,e){return e.months[t.getMonth()]},y:function(t){return t.getFullYear()},yy:function(t){return N(t.getFullYear(),2).slice(-2)},yyyy:function(t){return N(t.getFullYear(),4)}};function F(t){return t>-1?t%12:F(t+12)}function N(t,e){return t.toString().padStart(e,"0")}function W(t){if("string"!=typeof t)throw new Error("Invalid date format.");if(t in V)return V[t];var e=t.split(P),i=t.match(new RegExp(P,"g"));if(0===e.length||!i)throw new Error("Invalid date format.");var n=i.map((function(t){return z[t]})),r=Object.keys(B).reduce((function(t,e){return i.find((function(t){return"D"!==t[0]&&t[0].toLowerCase()===e}))&&t.push(e),t}),[]);return V[t]={parser:function(t,e){var n=t.split(j).reduce((function(t,e,n){if(e.length>0&&i[n]){var r=i[n][0];"M"===r?t.m=e:"D"!==r&&(t[r]=e)}return t}),{});return r.reduce((function(t,i){var r=B[i](t,n[i],e);return isNaN(r)?t:r}),L())},formatter:function(t,i){return n.reduce((function(n,r,o){return n+"".concat(e[o]).concat(r(t,i))}),"")+b(e)}}}function q(t,e,i){if(t instanceof Date||"number"==typeof t){var n=O(t);return isNaN(n)?void 0:n}if(t){if("today"===t)return L();if(e&&e.toValue){var r=e.toValue(t,e,i);return isNaN(r)?void 0:O(r)}return W(e).parser(t,i)}}function R(t,e,i){if(isNaN(t)||!t&&0!==t)return"";var n="number"==typeof t?new Date(t):t;return e.toDisplay?e.toDisplay(n,e,i):W(e).formatter(n,i)}var Y=new WeakMap,K=EventTarget.prototype,U=K.addEventListener,J=K.removeEventListener;function X(t,e){var i=Y.get(t);i||(i=[],Y.set(t,i)),e.forEach((function(t){U.call.apply(U,f(t)),i.push(t)}))}function $(t){var e=Y.get(t);e&&(e.forEach((function(t){J.call.apply(J,f(t))})),Y.delete(t))}if(!Event.prototype.composedPath){var G=function t(e){var i,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[];return n.push(e),e.parentNode?i=e.parentNode:e.host?i=e.host:e.defaultView&&(i=e.defaultView),i?t(i,n):n};Event.prototype.composedPath=function(){return G(this.target)}}function Q(t,e,i){var n=arguments.length>3&&void 0!==arguments[3]?arguments[3]:0,r=t[n];return e(r)?r:r!==i&&r.parentElement?Q(t,e,i,n+1):void 0}function Z(t,e){var i="function"==typeof e?e:function(t){return t.matches(e)};return Q(t.composedPath(),i,t.currentTarget)}var tt={en:{days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],daysShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],daysMin:["Su","Mo","Tu","We","Th","Fr","Sa"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],monthsShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],today:"Today",clear:"Clear",titleFormat:"MM y"}},et={autohide:!1,beforeShowDay:null,beforeShowDecade:null,beforeShowMonth:null,beforeShowYear:null,calendarWeeks:!1,clearBtn:!1,dateDelimiter:",",datesDisabled:[],daysOfWeekDisabled:[],daysOfWeekHighlighted:[],defaultViewDate:void 0,disableTouchKeyboard:!1,format:"mm/dd/yyyy",language:"en",maxDate:null,maxNumberOfDates:1,maxView:3,minDate:null,nextArrow:'',orientation:"auto",pickLevel:0,prevArrow:'',showDaysOfWeek:!0,showOnClick:!0,showOnFocus:!0,startView:0,title:"",todayBtn:!1,todayBtnMode:0,todayHighlight:!1,updateOnBlur:!0,weekStart:0},it=null;function nt(t){return null==it&&(it=document.createRange()),it.createContextualFragment(t)}function rt(t){"none"!==t.style.display&&(t.style.display&&(t.dataset.styleDisplay=t.style.display),t.style.display="none")}function ot(t){"none"===t.style.display&&(t.dataset.styleDisplay?(t.style.display=t.dataset.styleDisplay,delete t.dataset.styleDisplay):t.style.display="")}function at(t){t.firstChild&&(t.removeChild(t.firstChild),at(t))}var st=et.language,dt=et.format,ct=et.weekStart;function lt(t,e){return t.length<6&&e>=0&&e<7?_(t,e):t}function ut(t){return(t+6)%7}function ht(t,e,i,n){var r=q(t,e,i);return void 0!==r?r:n}function pt(t,e){var i=arguments.length>2&&void 0!==arguments[2]?arguments[2]:3,n=parseInt(t,10);return n>=0&&n<=i?n:e}function ft(t,e){var i,n=Object.assign({},t),r={},o=e.constructor.locales,a=e.config||{},s=a.format,d=a.language,c=a.locale,l=a.maxDate,u=a.maxView,h=a.minDate,p=a.pickLevel,f=a.startView,g=a.weekStart;if(n.language&&(n.language!==d&&(o[n.language]?i=n.language:void 0===o[i=n.language.split("-")[0]]&&(i=!1)),delete n.language,i)){d=r.language=i;var v=c||o[st];c=Object.assign({format:dt,weekStart:ct},o[st]),d!==st&&Object.assign(c,o[d]),r.locale=c,s===v.format&&(s=r.format=c.format),g===v.weekStart&&(g=r.weekStart=c.weekStart,r.weekEnd=ut(c.weekStart))}if(n.format){var y="function"==typeof n.format.toDisplay,b="function"==typeof n.format.toValue,w=P.test(n.format);(y&&b||w)&&(s=r.format=n.format),delete n.format}var k=h,E=l;if(void 0!==n.minDate&&(k=null===n.minDate?I(0,0,1):ht(n.minDate,s,c,k),delete n.minDate),void 0!==n.maxDate&&(E=null===n.maxDate?void 0:ht(n.maxDate,s,c,E),delete n.maxDate),E=0&&(r.maxNumberOfDates=O,r.multidate=1!==O),delete n.maxNumberOfDates}n.dateDelimiter&&(r.dateDelimiter=String(n.dateDelimiter),delete n.dateDelimiter);var L=p;void 0!==n.pickLevel&&(L=pt(n.pickLevel,2),delete n.pickLevel),L!==p&&(p=r.pickLevel=L);var A=u;void 0!==n.maxView&&(A=pt(n.maxView,u),delete n.maxView),(A=p>A?p:A)!==u&&(u=r.maxView=A);var C=f;if(void 0!==n.startView&&(C=pt(n.startView,C),delete n.startView),Cu&&(C=u),C!==f&&(r.startView=C),n.prevArrow){var S=nt(n.prevArrow);S.childNodes.length>0&&(r.prevArrow=S.childNodes),delete n.prevArrow}if(n.nextArrow){var T=nt(n.nextArrow);T.childNodes.length>0&&(r.nextArrow=T.childNodes),delete n.nextArrow}if(void 0!==n.disableTouchKeyboard&&(r.disableTouchKeyboard="ontouchstart"in document&&!!n.disableTouchKeyboard,delete n.disableTouchKeyboard),n.orientation){var M=n.orientation.toLowerCase().split(/\s+/g);r.orientation={x:M.find((function(t){return"left"===t||"right"===t}))||"auto",y:M.find((function(t){return"top"===t||"bottom"===t}))||"auto"},delete n.orientation}if(void 0!==n.todayBtnMode){switch(n.todayBtnMode){case 0:case 1:r.todayBtnMode=n.todayBtnMode}delete n.todayBtnMode}return Object.keys(n).forEach((function(t){void 0!==n[t]&&m(et,t)&&(r[t]=n[t])})),r}var gt=D(''),vt=D('
    \n
    '.concat(x("span",7,{class:"dow block flex-1 leading-9 border-0 rounded-lg cursor-default text-center text-gray-900 font-semibold text-sm"}),'
    \n
    ').concat(x("span",42,{class:"block flex-1 leading-9 border-0 rounded-lg cursor-default text-center text-gray-900 font-semibold text-sm h-6 leading-6 text-sm font-medium text-gray-500 dark:text-gray-400"}),"
    \n
    ")),yt=D('
    \n
    \n
    '.concat(x("span",6,{class:"week block flex-1 leading-9 border-0 rounded-lg cursor-default text-center text-gray-900 font-semibold text-sm"}),"
    \n
    ")),mt=function(){return a((function t(e,i){r(this,t),Object.assign(this,i,{picker:e,element:nt('
    ').firstChild,selected:[]}),this.init(this.picker.datepicker.config)}),[{key:"init",value:function(t){void 0!==t.pickLevel&&(this.isMinView=this.id===t.pickLevel),this.setOptions(t),this.updateFocus(),this.updateSelection()}},{key:"performBeforeHook",value:function(t,e,i){var n=this.beforeShow(new Date(i));switch(v(n)){case"boolean":n={enabled:n};break;case"string":n={classes:n}}if(n){if(!1===n.enabled&&(t.classList.add("disabled"),_(this.disabled,e)),n.classes){var r,o=n.classes.split(/\s+/);(r=t.classList).add.apply(r,f(o)),o.includes("disabled")&&_(this.disabled,e)}n.content&&function(t,e){at(t),e instanceof DocumentFragment?t.appendChild(e):"string"==typeof e?t.appendChild(nt(e)):"function"==typeof e.forEach&&e.forEach((function(e){t.appendChild(e)}))}(t,n.content)}}}])}(),bt=function(t){function e(t){return r(this,e),n(this,e,[t,{id:0,name:"days",cellClass:"day"}])}return c(e,t),a(e,[{key:"init",value:function(t){var i=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];if(i){var n=nt(vt).firstChild;this.dow=n.firstChild,this.grid=n.lastChild,this.element.appendChild(n)}s(d(e.prototype),"init",this).call(this,t)}},{key:"setOptions",value:function(t){var e,i=this;if(m(t,"minDate")&&(this.minDate=t.minDate),m(t,"maxDate")&&(this.maxDate=t.maxDate),t.datesDisabled&&(this.datesDisabled=t.datesDisabled),t.daysOfWeekDisabled&&(this.daysOfWeekDisabled=t.daysOfWeekDisabled,e=!0),t.daysOfWeekHighlighted&&(this.daysOfWeekHighlighted=t.daysOfWeekHighlighted),void 0!==t.todayHighlight&&(this.todayHighlight=t.todayHighlight),void 0!==t.weekStart&&(this.weekStart=t.weekStart,this.weekEnd=t.weekEnd,e=!0),t.locale){var n=this.locale=t.locale;this.dayNames=n.daysMin,this.switchLabelFormat=n.titleFormat,e=!0}if(void 0!==t.beforeShowDay&&(this.beforeShow="function"==typeof t.beforeShowDay?t.beforeShowDay:void 0),void 0!==t.calendarWeeks)if(t.calendarWeeks&&!this.calendarWeeks){var r=nt(yt).firstChild;this.calendarWeeks={element:r,dow:r.firstChild,weeks:r.lastChild},this.element.insertBefore(r,this.element.firstChild)}else this.calendarWeeks&&!t.calendarWeeks&&(this.element.removeChild(this.calendarWeeks.element),this.calendarWeeks=null);void 0!==t.showDaysOfWeek&&(t.showDaysOfWeek?(ot(this.dow),this.calendarWeeks&&ot(this.calendarWeeks.dow)):(rt(this.dow),this.calendarWeeks&&rt(this.calendarWeeks.dow))),e&&Array.from(this.dow.children).forEach((function(t,e){var n=(i.weekStart+e)%7;t.textContent=i.dayNames[n],t.className=i.daysOfWeekDisabled.includes(n)?"dow disabled text-center h-6 leading-6 text-sm font-medium text-gray-500 dark:text-gray-400 cursor-not-allowed":"dow text-center h-6 leading-6 text-sm font-medium text-gray-500 dark:text-gray-400"}))}},{key:"updateFocus",value:function(){var t=new Date(this.picker.viewDate),e=t.getFullYear(),i=t.getMonth(),n=I(e,i,1),r=M(n,this.weekStart,this.weekStart);this.first=n,this.last=I(e,i+1,0),this.start=r,this.focused=this.picker.viewDate}},{key:"updateSelection",value:function(){var t=this.picker.datepicker,e=t.dates,i=t.rangepicker;this.selected=e,i&&(this.range=i.dates)}},{key:"render",value:function(){var t=this;this.today=this.todayHighlight?L():void 0,this.disabled=f(this.datesDisabled);var e=R(this.focused,this.switchLabelFormat,this.locale);if(this.picker.setViewSwitchLabel(e),this.picker.setPrevBtnDisabled(this.first<=this.minDate),this.picker.setNextBtnDisabled(this.last>=this.maxDate),this.calendarWeeks){var i=M(this.first,1,1);Array.from(this.calendarWeeks.weeks.children).forEach((function(t,e){t.textContent=function(t){var e=M(t,4,1),i=M(new Date(e).setMonth(0,4),4,1);return Math.round((e-i)/6048e5)+1}(A(i,7*e))}))}Array.from(this.grid.children).forEach((function(e,i){var n=e.classList,r=A(t.start,i),o=new Date(r),a=o.getDay();if(e.className="datepicker-cell hover:bg-gray-100 dark:hover:bg-gray-600 block flex-1 leading-9 border-0 rounded-lg cursor-pointer text-center text-gray-900 dark:text-white font-semibold text-sm ".concat(t.cellClass),e.dataset.date=r,e.textContent=o.getDate(),rt.last&&n.add("next","text-gray-500","dark:text-white"),t.today===r&&n.add("today","bg-gray-100","dark:bg-gray-600"),(rt.maxDate||t.disabled.includes(r))&&(n.add("disabled","cursor-not-allowed","text-gray-400","dark:text-gray-500"),n.remove("hover:bg-gray-100","dark:hover:bg-gray-600","text-gray-900","dark:text-white","cursor-pointer")),t.daysOfWeekDisabled.includes(a)&&(n.add("disabled","cursor-not-allowed","text-gray-400","dark:text-gray-500"),n.remove("hover:bg-gray-100","dark:hover:bg-gray-600","text-gray-900","dark:text-white","cursor-pointer"),_(t.disabled,r)),t.daysOfWeekHighlighted.includes(a)&&n.add("highlighted"),t.range){var s=h(t.range,2),d=s[0],c=s[1];r>d&&ri&&re||s1&&void 0!==arguments[1])||arguments[1];i&&(this.grid=this.element,this.element.classList.add("months","datepicker-grid","w-64","grid","grid-cols-4"),this.grid.appendChild(nt(x("span",12,{"data-month":function(t){return t}})))),s(d(e.prototype),"init",this).call(this,t)}},{key:"setOptions",value:function(t){if(t.locale&&(this.monthNames=t.locale.monthsShort),m(t,"minDate"))if(void 0===t.minDate)this.minYear=this.minMonth=this.minDate=void 0;else{var e=new Date(t.minDate);this.minYear=e.getFullYear(),this.minMonth=e.getMonth(),this.minDate=e.setDate(1)}if(m(t,"maxDate"))if(void 0===t.maxDate)this.maxYear=this.maxMonth=this.maxDate=void 0;else{var i=new Date(t.maxDate);this.maxYear=i.getFullYear(),this.maxMonth=i.getMonth(),this.maxDate=I(this.maxYear,this.maxMonth+1,0)}void 0!==t.beforeShowMonth&&(this.beforeShow="function"==typeof t.beforeShowMonth?t.beforeShowMonth:void 0)}},{key:"updateFocus",value:function(){var t=new Date(this.picker.viewDate);this.year=t.getFullYear(),this.focused=t.getMonth()}},{key:"updateSelection",value:function(){var t=this.picker.datepicker,e=t.dates,i=t.rangepicker;this.selected=e.reduce((function(t,e){var i=new Date(e),n=i.getFullYear(),r=i.getMonth();return void 0===t[n]?t[n]=[r]:_(t[n],r),t}),{}),i&&i.dates&&(this.range=i.dates.map((function(t){var e=new Date(t);return isNaN(e)?void 0:[e.getFullYear(),e.getMonth()]})))}},{key:"render",value:function(){var t=this;this.disabled=[],this.picker.setViewSwitchLabel(this.year),this.picker.setPrevBtnDisabled(this.year<=this.minYear),this.picker.setNextBtnDisabled(this.year>=this.maxYear);var e=this.selected[this.year]||[],i=this.yearthis.maxYear,n=this.year===this.minYear,r=this.year===this.maxYear,o=_t(this.range,this.year);Array.from(this.grid.children).forEach((function(a,s){var d=a.classList,c=I(t.year,s,1);if(a.className="datepicker-cell hover:bg-gray-100 dark:hover:bg-gray-600 block flex-1 leading-9 border-0 rounded-lg cursor-pointer text-center text-gray-900 dark:text-white font-semibold text-sm ".concat(t.cellClass),t.isMinView&&(a.dataset.date=c),a.textContent=t.monthNames[s],(i||n&&st.maxMonth)&&d.add("disabled"),o){var l=h(o,2),u=l[0],p=l[1];s>u&&sn&&o1&&void 0!==arguments[1])||arguments[1];i&&(this.navStep=10*this.step,this.beforeShowOption="beforeShow".concat(kt(this.cellClass)),this.grid=this.element,this.element.classList.add(this.name,"datepicker-grid","w-64","grid","grid-cols-4"),this.grid.appendChild(nt(x("span",12)))),s(d(e.prototype),"init",this).call(this,t)}},{key:"setOptions",value:function(t){if(m(t,"minDate")&&(void 0===t.minDate?this.minYear=this.minDate=void 0:(this.minYear=H(t.minDate,this.step),this.minDate=I(this.minYear,0,1))),m(t,"maxDate")&&(void 0===t.maxDate?this.maxYear=this.maxDate=void 0:(this.maxYear=H(t.maxDate,this.step),this.maxDate=I(this.maxYear,11,31))),void 0!==t[this.beforeShowOption]){var e=t[this.beforeShowOption];this.beforeShow="function"==typeof e?e:void 0}}},{key:"updateFocus",value:function(){var t=new Date(this.picker.viewDate),e=H(t,this.navStep),i=e+9*this.step;this.first=e,this.last=i,this.start=e-this.step,this.focused=H(t,this.step)}},{key:"updateSelection",value:function(){var t=this,e=this.picker.datepicker,i=e.dates,n=e.rangepicker;this.selected=i.reduce((function(e,i){return _(e,H(i,t.step))}),[]),n&&n.dates&&(this.range=n.dates.map((function(e){if(void 0!==e)return H(e,t.step)})))}},{key:"render",value:function(){var t=this;this.disabled=[],this.picker.setViewSwitchLabel("".concat(this.first,"-").concat(this.last)),this.picker.setPrevBtnDisabled(this.first<=this.minYear),this.picker.setNextBtnDisabled(this.last>=this.maxYear),Array.from(this.grid.children).forEach((function(e,i){var n=e.classList,r=t.start+i*t.step,o=I(r,0,1);if(e.className="datepicker-cell hover:bg-gray-100 dark:hover:bg-gray-600 block flex-1 leading-9 border-0 rounded-lg cursor-pointer text-center text-gray-900 dark:text-white font-semibold text-sm ".concat(t.cellClass),t.isMinView&&(e.dataset.date=o),e.textContent=e.dataset.year=r,0===i?n.add("prev"):11===i&&n.add("next"),(rt.maxYear)&&n.add("disabled"),t.range){var a=h(t.range,2),s=a[0],d=a[1];r>s&&ri&&r0?b(e):i.defaultViewDate,i.minDate,i.maxDate)}function Bt(t,e){var i=new Date(t.viewDate),n=new Date(e),r=t.currentView,o=r.id,a=r.year,s=r.first,d=r.last,c=n.getFullYear();switch(t.viewDate=e,c!==i.getFullYear()&&xt(t.datepicker,"changeYear"),n.getMonth()!==i.getMonth()&&xt(t.datepicker,"changeMonth"),o){case 0:return ed;case 1:return c!==a;default:return cd}}function zt(t){return window.getComputedStyle(t).direction}var Ft=function(){return a((function t(e){r(this,t),this.datepicker=e;var i=gt.replace(/%buttonClass%/g,e.config.buttonClass),n=this.element=nt(i).firstChild,o=h(n.firstChild.children,3),a=o[0],s=o[1],d=o[2],c=a.firstElementChild,l=h(a.lastElementChild.children,3),u=l[0],p=l[1],f=l[2],g=h(d.firstChild.children,2),v={title:c,prevBtn:u,viewSwitch:p,nextBtn:f,todayBtn:g[0],clearBtn:g[1]};this.main=s,this.controls=v;var y=e.inline?"inline":"dropdown";n.classList.add("datepicker-".concat(y)),"dropdown"===y&&n.classList.add("dropdown","absolute","top-0","left-0","z-50","pt-2"),jt(this,e.config),this.viewDate=Vt(e),X(e,[[n,"click",Pt.bind(null,e),{capture:!0}],[s,"click",Ht.bind(null,e)],[v.viewSwitch,"click",St.bind(null,e)],[v.prevBtn,"click",Tt.bind(null,e)],[v.nextBtn,"click",Mt.bind(null,e)],[v.todayBtn,"click",At.bind(null,e)],[v.clearBtn,"click",Ct.bind(null,e)]]),this.views=[new bt(this),new wt(this),new Et(this,{id:2,name:"years",cellClass:"year",step:1}),new Et(this,{id:3,name:"decades",cellClass:"decade",step:10})],this.currentView=this.views[e.config.startView],this.currentView.render(),this.main.appendChild(this.currentView.element),e.config.container.appendChild(this.element)}),[{key:"setOptions",value:function(t){jt(this,t),this.views.forEach((function(e){e.init(t,!1)})),this.currentView.render()}},{key:"detach",value:function(){this.datepicker.config.container.removeChild(this.element)}},{key:"show",value:function(){if(!this.active){this.element.classList.add("active","block"),this.element.classList.remove("hidden"),this.active=!0;var t=this.datepicker;if(!t.inline){var e=zt(t.inputField);e!==zt(t.config.container)?this.element.dir=e:this.element.dir&&this.element.removeAttribute("dir"),this.place(),t.config.disableTouchKeyboard&&t.inputField.blur()}xt(t,"show")}}},{key:"hide",value:function(){this.active&&(this.datepicker.exitEditMode(),this.element.classList.remove("active","block"),this.element.classList.add("active","block","hidden"),this.active=!1,xt(this.datepicker,"hide"))}},{key:"place",value:function(){var t,e,i,n=this.element,r=n.classList,o=n.style,a=this.datepicker,s=a.config,d=a.inputField,c=s.container,l=this.element.getBoundingClientRect(),u=l.width,h=l.height,p=c.getBoundingClientRect(),f=p.left,g=p.top,v=p.width,y=d.getBoundingClientRect(),m=y.left,b=y.top,_=y.width,w=y.height,k=s.orientation,E=k.x,x=k.y;c===document.body?(t=window.scrollY,e=m+window.scrollX,i=b+t):(e=m-f,i=b-g+(t=c.scrollTop)),"auto"===E&&(e<0?(E="left",e=10):E=e+u>v||"rtl"===zt(d)?"right":"left"),"right"===E&&(e-=u-_),"auto"===x&&(x=i-h0&&void 0!==arguments[0])||arguments[0],e=t&&this._renderMethod||"render";delete this._renderMethod,this.currentView[e]()}}])}();function Nt(t,e,i,n,r,o){if(k(t,r,o))return n(t)?Nt(e(t,i),e,i,n,r,o):t}function Wt(t,e,i,n){var r,o,a=t.picker,s=a.currentView,d=s.step||1,c=a.viewDate;switch(s.id){case 0:c=n?A(c,7*i):e.ctrlKey||e.metaKey?S(c,i):A(c,i),r=A,o=function(t){return s.disabled.includes(t)};break;case 1:c=C(c,n?4*i:i),r=C,o=function(t){var e=new Date(t),i=s.year,n=s.disabled;return e.getFullYear()===i&&n.includes(e.getMonth())};break;default:c=S(c,i*(n?4:1)*d),r=S,o=function(t){return s.disabled.includes(H(t,d))}}void 0!==(c=Nt(c,r,i<0?-d:d,o,s.minDate,s.maxDate))&&a.changeFocus(c).render()}function qt(t,e){if("Tab"!==e.key){var i=t.picker,n=i.currentView,r=n.id,o=n.isMinView;if(i.active)if(t.editMode)switch(e.key){case"Escape":i.hide();break;case"Enter":t.exitEditMode({update:!0,autohide:t.config.autohide});break;default:return}else switch(e.key){case"Escape":i.hide();break;case"ArrowLeft":if(e.ctrlKey||e.metaKey)Dt(t,-1);else{if(e.shiftKey)return void t.enterEditMode();Wt(t,e,-1,!1)}break;case"ArrowRight":if(e.ctrlKey||e.metaKey)Dt(t,1);else{if(e.shiftKey)return void t.enterEditMode();Wt(t,e,1,!1)}break;case"ArrowUp":if(e.ctrlKey||e.metaKey)Ot(t);else{if(e.shiftKey)return void t.enterEditMode();Wt(t,e,-1,!0)}break;case"ArrowDown":if(e.shiftKey&&!e.ctrlKey&&!e.metaKey)return void t.enterEditMode();Wt(t,e,1,!0);break;case"Enter":o?t.setDate(i.viewDate):i.changeView(r-1).render();break;case"Backspace":case"Delete":return void t.enterEditMode();default:return void(1!==e.key.length||e.ctrlKey||e.metaKey||t.enterEditMode())}else switch(e.key){case"ArrowDown":case"Escape":i.show();break;case"Enter":t.update();break;default:return}e.preventDefault(),e.stopPropagation()}else Lt(t)}function Rt(t){t.config.showOnFocus&&!t._showing&&t.show()}function Yt(t,e){var i=e.target;(t.picker.active||t.config.showOnClick)&&(i._active=i===document.activeElement,i._clicking=setTimeout((function(){delete i._active,delete i._clicking}),2e3))}function Kt(t,e){var i=e.target;i._clicking&&(clearTimeout(i._clicking),delete i._clicking,i._active&&t.enterEditMode(),delete i._active,t.config.showOnClick&&t.show())}function Ut(t,e){e.clipboardData.types.includes("text/plain")&&t.enterEditMode()}function Jt(t,e){var i=t.element;if(i===document.activeElement){var n=t.picker.element;Z(e,(function(t){return t===i||t===n}))||Lt(t)}}function Xt(t,e){return t.map((function(t){return R(t,e.format,e.locale)})).join(e.dateDelimiter)}function $t(t,e){var i=arguments.length>2&&void 0!==arguments[2]&&arguments[2],n=t.config,r=t.dates,o=t.rangepicker;if(0===e.length)return i?[]:void 0;var a=o&&t===o.datepickers[1],s=e.reduce((function(t,e){var i=q(e,n.format,n.locale);if(void 0===i)return t;if(n.pickLevel>0){var r=new Date(i);i=1===n.pickLevel?a?r.setMonth(r.getMonth()+1,0):r.setDate(1):a?r.setFullYear(r.getFullYear()+1,0,0):r.setMonth(0,1)}return!k(i,n.minDate,n.maxDate)||t.includes(i)||n.datesDisabled.includes(i)||n.daysOfWeekDisabled.includes(new Date(i).getDay())||t.push(i),t}),[]);return 0!==s.length?(n.multidate&&!i&&(s=s.reduce((function(t,e){return r.includes(e)||t.push(e),t}),r.filter((function(t){return!s.includes(t)})))),n.maxNumberOfDates&&s.length>n.maxNumberOfDates?s.slice(-1*n.maxNumberOfDates):s):void 0}function Gt(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:3,i=!(arguments.length>2&&void 0!==arguments[2])||arguments[2],n=t.config,r=t.picker,o=t.inputField;if(2&e){var a=r.active?n.pickLevel:n.startView;r.update().changeView(a).render(i)}1&e&&o&&(o.value=Xt(t.dates,n))}function Qt(t,e,i){var n=i.clear,r=i.render,o=i.autohide;void 0===r&&(r=!0),r?void 0===o&&(o=t.config.autohide):o=!1;var a=$t(t,e,n);a&&(a.toString()!==t.dates.toString()?(t.dates=a,Gt(t,r?3:1),xt(t,"changeDate")):Gt(t,1),o&&t.hide())}var Zt=function(){return a((function t(e){var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:void 0;r(this,t),e.datepicker=this,this.element=e;var o=this.config=Object.assign({buttonClass:i.buttonClass&&String(i.buttonClass)||"button",container:document.body,defaultViewDate:L(),maxDate:void 0,minDate:void 0},ft(et,this));this._options=i,Object.assign(o,ft(i,this));var a,s,d=this.inline="INPUT"!==e.tagName;if(d)o.container=e,s=w(e.dataset.date,o.dateDelimiter),delete e.dataset.date;else{var c=i.container?document.querySelector(i.container):null;c&&(o.container=c),(a=this.inputField=e).classList.add("datepicker-input"),s=w(a.value,o.dateDelimiter)}if(n){var l=n.inputs.indexOf(a),u=n.datepickers;if(l<0||l>1||!Array.isArray(u))throw Error("Invalid rangepicker object.");u[l]=this,Object.defineProperty(this,"rangepicker",{get:function(){return n}})}this.dates=[];var h=$t(this,s);h&&h.length>0&&(this.dates=h),a&&(a.value=Xt(this.dates,o));var p=this.picker=new Ft(this);if(d)this.show();else{var f=Jt.bind(null,this),g=[[a,"keydown",qt.bind(null,this)],[a,"focus",Rt.bind(null,this)],[a,"mousedown",Yt.bind(null,this)],[a,"click",Kt.bind(null,this)],[a,"paste",Ut.bind(null,this)],[document,"mousedown",f],[document,"touchstart",f],[window,"resize",p.place.bind(p)]];X(this,g)}}),[{key:"active",get:function(){return!(!this.picker||!this.picker.active)}},{key:"pickerElement",get:function(){return this.picker?this.picker.element:void 0}},{key:"setOptions",value:function(t){var e=this.picker,i=ft(t,this);Object.assign(this._options,t),Object.assign(this.config,i),e.setOptions(i),Gt(this,3)}},{key:"show",value:function(){if(this.inputField){if(this.inputField.disabled)return;this.inputField!==document.activeElement&&(this._showing=!0,this.inputField.focus(),delete this._showing)}this.picker.show()}},{key:"hide",value:function(){this.inline||(this.picker.hide(),this.picker.update().changeView(this.config.startView).render())}},{key:"destroy",value:function(){return this.hide(),$(this),this.picker.detach(),this.inline||this.inputField.classList.remove("datepicker-input"),delete this.element.datepicker,this}},{key:"getDate",value:function(){var t=this,e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:void 0,i=e?function(i){return R(i,e,t.config.locale)}:function(t){return new Date(t)};return this.config.multidate?this.dates.map(i):this.dates.length>0?i(this.dates[0]):void 0}},{key:"setDate",value:function(){for(var t=arguments.length,e=new Array(t),i=0;i0&&void 0!==arguments[0]?arguments[0]:void 0;if(!this.inline){var e={clear:!0,autohide:!(!t||!t.autohide)},i=w(this.inputField.value,this.config.dateDelimiter);Qt(this,i,e)}}},{key:"refresh",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:void 0,e=arguments.length>1&&void 0!==arguments[1]&&arguments[1];t&&"string"!=typeof t&&(e=t,t=void 0),Gt(this,"picker"===t?2:"input"===t?1:3,!e)}},{key:"enterEditMode",value:function(){this.inline||!this.picker.active||this.editMode||(this.editMode=!0,this.inputField.classList.add("in-edit","border-blue-700","!border-primary-700"))}},{key:"exitEditMode",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:void 0;if(!this.inline&&this.editMode){var e=Object.assign({update:!1},t);delete this.editMode,this.inputField.classList.remove("in-edit","border-blue-700","!border-primary-700"),e.update&&this.update(e)}}}],[{key:"formatDate",value:function(t,e,i){return R(t,e,i&&tt[i]||tt.en)}},{key:"parseDate",value:function(t,e,i){return q(t,e,i&&tt[i]||tt.en)}},{key:"locales",get:function(){return tt}}])}();function te(t){var e=Object.assign({},t);return delete e.inputs,delete e.allowOneSidedRange,delete e.maxNumberOfDates,e}function ee(t,e,i,n){X(t,[[i,"changeDate",e]]),new Zt(i,n,t)}function ie(t,e){if(!t._updating){t._updating=!0;var i=e.target;if(void 0!==i.datepicker){var n=t.datepickers,r={render:!1},o=t.inputs.indexOf(i),a=0===o?1:0,s=n[o].dates[0],d=n[a].dates[0];void 0!==s&&void 0!==d?0===o&&s>d?(n[0].setDate(d,r),n[1].setDate(s,r)):1===o&&s1&&void 0!==arguments[1]?arguments[1]:{};r(this,t);var n=Array.isArray(i.inputs)?i.inputs:Array.from(e.querySelectorAll("input"));if(!(n.length<2)){e.rangepicker=this,this.element=e,this.inputs=n.slice(0,2),this.allowOneSidedRange=!!i.allowOneSidedRange;var o=ie.bind(null,this),a=te(i),s=[];Object.defineProperty(this,"datepickers",{get:function(){return s}}),ee(this,o,this.inputs[0],a),ee(this,o,this.inputs[1],a),Object.freeze(s),s[0].dates.length>0?ie(this,{target:this.inputs[0]}):s[1].dates.length>0&&ie(this,{target:this.inputs[1]})}}),[{key:"dates",get:function(){return 2===this.datepickers.length?[this.datepickers[0].dates[0],this.datepickers[1].dates[0]]:void 0}},{key:"setOptions",value:function(t){this.allowOneSidedRange=!!t.allowOneSidedRange;var e=te(t);this.datepickers[0].setOptions(e),this.datepickers[1].setOptions(e)}},{key:"destroy",value:function(){this.datepickers[0].destroy(),this.datepickers[1].destroy(),$(this),delete this.element.rangepicker}},{key:"getDates",value:function(){var t=this,e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:void 0,i=e?function(i){return R(i,e,t.datepickers[0].config.locale)}:function(t){return new Date(t)};return this.dates.map((function(t){return void 0===t?t:i(t)}))}},{key:"setDates",value:function(t,e){var i=h(this.datepickers,2),n=i[0],r=i[1],o=this.dates;this._updating=!0,n.setDate(t),r.setDate(e),delete this._updating,r.dates[0]!==o[1]?ie(this,{target:this.inputs[1]}):n.dates[0]!==o[0]&&ie(this,{target:this.inputs[0]})}}])}();e.DateRangePicker=ne,e.Datepicker=Zt},902:function(t,e,i){var n=this&&this.__assign||function(){return n=Object.assign||function(t){for(var e,i=1,n=arguments.length;it._options.maxValue&&(i.value=t._options.maxValue.toString()),null!==t._options.minValue&&parseInt(i.value)=this._options.maxValue||(this._targetEl.value=(this.getCurrentValue()+1).toString(),this._options.onIncrement(this))},t.prototype.decrement=function(){null!==this._options.minValue&&this.getCurrentValue()<=this._options.minValue||(this._targetEl.value=(this.getCurrentValue()-1).toString(),this._options.onDecrement(this))},t.prototype.updateOnIncrement=function(t){this._options.onIncrement=t},t.prototype.updateOnDecrement=function(t){this._options.onDecrement=t},t}();function d(){document.querySelectorAll("[data-input-counter]").forEach((function(t){var e=t.id,i=document.querySelector('[data-input-counter-increment="'+e+'"]'),n=document.querySelector('[data-input-counter-decrement="'+e+'"]'),o=t.getAttribute("data-input-counter-min"),a=t.getAttribute("data-input-counter-max");t?r.default.instanceExists("InputCounter",t.getAttribute("id"))||new s(t,i||null,n||null,{minValue:o?parseInt(o):null,maxValue:a?parseInt(a):null}):console.error('The target element with id "'.concat(e,'" does not exist. Please check the data-input-counter attribute.'))}))}e.initInputCounters=d,"undefined"!=typeof window&&(window.InputCounter=s,window.initInputCounters=d),e.default=s},16:function(t,e,i){var n=this&&this.__assign||function(){return n=Object.assign||function(t){for(var e,i=1,n=arguments.length;i{const t=document.querySelector("meta[name=csp-nonce]");return s=t&&t.content},d=()=>s||l(),m=Element.prototype.matches||Element.prototype.matchesSelector||Element.prototype.mozMatchesSelector||Element.prototype.msMatchesSelector||Element.prototype.oMatchesSelector||Element.prototype.webkitMatchesSelector,p=function(t,e){return e.exclude?m.call(t,e.selector)&&!m.call(t,e.exclude):m.call(t,e)},f="_ujsData",b=(t,e)=>t[f]?t[f][e]:void 0,h=function(t,e,n){return t[f]||(t[f]={}),t[f][e]=n},y=t=>Array.prototype.slice.call(document.querySelectorAll(t)),j=function(t){var e=!1;do{if(t.isContentEditable){e=!0;break}t=t.parentElement}while(t);return e},v=()=>{const t=document.querySelector("meta[name=csrf-token]");return t&&t.content},E=()=>{const t=document.querySelector("meta[name=csrf-param]");return t&&t.content},g=t=>{const e=v();if(e)return t.setRequestHeader("X-CSRF-Token",e)},w=()=>{const t=v(),e=E();if(t&&e)return y('form input[name="'+e+'"]').forEach((e=>e.value=t))},x={"*":"*/*",text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript",script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},S=t=>{t=k(t);var e=C(t,(function(){const n=T(null!=e.response?e.response:e.responseText,e.getResponseHeader("Content-Type"));return 2===Math.floor(e.status/100)?"function"==typeof t.success&&t.success(n,e.statusText,e):"function"==typeof t.error&&t.error(n,e.statusText,e),"function"==typeof t.complete?t.complete(e,e.statusText):void 0}));return!(t.beforeSend&&!t.beforeSend(e,t))&&(e.readyState===XMLHttpRequest.OPENED?e.send(t.data):void 0)};var k=function(t){return t.url=t.url||location.href,t.type=t.type.toUpperCase(),"GET"===t.type&&t.data&&(t.url.indexOf("?")<0?t.url+="?"+t.data:t.url+="&"+t.data),t.dataType in x||(t.dataType="*"),t.accept=x[t.dataType],"*"!==t.dataType&&(t.accept+=", */*; q=0.01"),t},C=function(t,e){const n=new XMLHttpRequest;return n.open(t.type,t.url,!0),n.setRequestHeader("Accept",t.accept),"string"==typeof t.data&&n.setRequestHeader("Content-Type","application/x-www-form-urlencoded; charset=UTF-8"),t.crossDomain||(n.setRequestHeader("X-Requested-With","XMLHttpRequest"),g(n)),n.withCredentials=!!t.withCredentials,n.onreadystatechange=function(){if(n.readyState===XMLHttpRequest.DONE)return e(n)},n},T=function(t,e){if("string"==typeof t&&"string"==typeof e)if(e.match(/\bjson\b/))try{t=JSON.parse(t)}catch(t){}else if(e.match(/\b(?:java|ecma)script\b/)){const e=document.createElement("script");e.setAttribute("nonce",d()),e.text=t,document.head.appendChild(e).parentNode.removeChild(e)}else if(e.match(/\b(xml|html|svg)\b/)){const n=new DOMParser;e=e.replace(/;.+/,"");try{t=n.parseFromString(t,e)}catch(t){}}return t};const A=function(t){const e=document.createElement("a");e.href=location.href;const n=document.createElement("a");try{return n.href=t,!((!n.protocol||":"===n.protocol)&&!n.host||e.protocol+"//"+e.host==n.protocol+"//"+n.host)}catch(t){return!0}};let D,{CustomEvent:M}=window;"function"!=typeof M&&(M=function(t,e){const n=document.createEvent("CustomEvent");return n.initCustomEvent(t,e.bubbles,e.cancelable,e.detail),n},M.prototype=window.Event.prototype,({preventDefault:D}=M.prototype),M.prototype.preventDefault=function(){const t=D.call(this);return this.cancelable&&!this.defaultPrevented&&Object.defineProperty(this,"defaultPrevented",{get:()=>!0}),t});const L=(t,e,n)=>{const a=new M(e,{bubbles:!0,cancelable:!0,detail:n});return t.dispatchEvent(a),!a.defaultPrevented},R=t=>{L(t.target,"ujs:everythingStopped"),t.preventDefault(),t.stopPropagation(),t.stopImmediatePropagation()},q=(t,e,n,a)=>t.addEventListener(n,(function(t){let{target:n}=t;for(;n instanceof Element&&!p(n,e);)n=n.parentNode;n instanceof Element&&!1===a.call(n,t)&&(t.preventDefault(),t.stopPropagation())})),H=t=>Array.prototype.slice.call(t),P=(t,e)=>{let n=[t];p(t,"form")&&(n=H(t.elements));const a=[];return n.forEach((function(t){t.name&&!t.disabled&&(p(t,"fieldset[disabled] *")||(p(t,"select")?H(t.options).forEach((function(e){e.selected&&a.push({name:t.name,value:e.value})})):(t.checked||-1===["radio","checkbox","submit"].indexOf(t.type))&&a.push({name:t.name,value:t.value})))})),e&&a.push(e),a.map((function(t){return t.name?`${encodeURIComponent(t.name)}=${encodeURIComponent(t.value)}`:t})).join("&")},O=(t,e)=>p(t,"form")?H(t.elements).filter((t=>p(t,e))):H(t.querySelectorAll(e));var I=function(t,e){let n;const a=t.getAttribute("data-confirm");if(!a)return!0;let o=!1;if(L(t,"confirm")){try{o=e.confirm(a,t)}catch(t){}n=L(t,"confirm:complete",[o])}return o&&n};const N=function(t){this.disabled&&R(t)},X=t=>{let e;if(t instanceof Event){if(K(t))return;e=t.target}else e=t;if(!j(e))return p(e,u)?F(e):p(e,c)||p(e,i)?z(e):p(e,a)?G(e):void 0},$=t=>{const e=t instanceof Event?t.target:t;if(!j(e))return p(e,u)?_(e):p(e,c)||p(e,r)?U(e):p(e,a)?Q(e):void 0};var _=function(t){if(b(t,"ujs:disabled"))return;const e=t.getAttribute("data-disable-with");return null!=e&&(h(t,"ujs:enable-with",t.innerHTML),t.innerHTML=e),t.addEventListener("click",R),h(t,"ujs:disabled",!0)},F=function(t){const e=b(t,"ujs:enable-with");return null!=e&&(t.innerHTML=e,h(t,"ujs:enable-with",null)),t.removeEventListener("click",R),h(t,"ujs:disabled",null)},Q=t=>O(t,r).forEach(U),U=function(t){if(b(t,"ujs:disabled"))return;const e=t.getAttribute("data-disable-with");return null!=e&&(p(t,"button")?(h(t,"ujs:enable-with",t.innerHTML),t.innerHTML=e):(h(t,"ujs:enable-with",t.value),t.value=e)),t.disabled=!0,h(t,"ujs:disabled",!0)},G=t=>O(t,i).forEach((t=>z(t))),z=function(t){const e=b(t,"ujs:enable-with");return null!=e&&(p(t,"button")?t.innerHTML=e:t.value=e,h(t,"ujs:enable-with",null)),t.disabled=!1,h(t,"ujs:disabled",null)},K=function(t){const e=t.detail?t.detail[0]:void 0;return e&&e.getResponseHeader("X-Xhr-Redirect")};const B=function(t){const e=this,{form:n}=e;if(n)return e.name&&h(n,"ujs:submit-button",{name:e.name,value:e.value}),h(n,"ujs:formnovalidate-button",e.formNoValidate),h(n,"ujs:submit-button-formaction",e.getAttribute("formaction")),h(n,"ujs:submit-button-formmethod",e.getAttribute("formmethod"))},J=function(t){const e=(this.getAttribute("data-method")||"GET").toUpperCase(),n=this.getAttribute("data-params"),a=(t.metaKey||t.ctrlKey)&&"GET"===e&&!n;(null!=t.button&&0!==t.button||a)&&t.stopImmediatePropagation()},V={$:y,ajax:S,buttonClickSelector:e,buttonDisableSelector:c,confirm:(t,e)=>window.confirm(t),cspNonce:d,csrfToken:v,csrfParam:E,CSRFProtection:g,delegate:q,disableElement:$,enableElement:X,fileInputSelector:"input[name][type=file]:not([disabled])",fire:L,formElements:O,formEnableSelector:i,formDisableSelector:r,formInputClickSelector:o,formSubmitButtonClick:B,formSubmitSelector:a,getData:b,handleDisabledElement:N,href:t=>t.href,inputChangeSelector:n,isCrossDomain:A,linkClickSelector:t,linkDisableSelector:u,loadCSPNonce:l,matches:p,preventInsignificantClick:J,refreshCSRFTokens:w,serializeElement:P,setData:h,stopEverything:R},W=(Y=V,function(t){I(this,Y)||R(t)});var Y;V.handleConfirm=W;const Z=(t=>function(e){const n=this,a=n.getAttribute("data-method");if(!a)return;if(j(this))return;const o=t.href(n),r=v(),i=E(),u=document.createElement("form");let c=``;i&&r&&!A(o)&&(c+=``),c+='',u.method="post",u.action=o,u.target=n.target,u.innerHTML=c,u.style.display="none",document.body.appendChild(u),u.querySelector('[type="submit"]').click(),R(e)})(V);V.handleMethod=Z;const tt=(t=>function(o){let r,i,u;const c=this;if(!function(t){const e=t.getAttribute("data-remote");return null!=e&&"false"!==e}(c))return!0;if(!L(c,"ajax:before"))return L(c,"ajax:stopped"),!1;if(j(c))return L(c,"ajax:stopped"),!1;const s=c.getAttribute("data-with-credentials"),l=c.getAttribute("data-type")||"script";if(p(c,a)){const t=b(c,"ujs:submit-button");i=b(c,"ujs:submit-button-formmethod")||c.getAttribute("method")||"get",u=b(c,"ujs:submit-button-formaction")||c.getAttribute("action")||location.href,"GET"===i.toUpperCase()&&(u=u.replace(/\?.*$/,"")),"multipart/form-data"===c.enctype?(r=new FormData(c),null!=t&&r.append(t.name,t.value)):r=P(c,t),h(c,"ujs:submit-button",null),h(c,"ujs:submit-button-formmethod",null),h(c,"ujs:submit-button-formaction",null)}else p(c,e)||p(c,n)?(i=c.getAttribute("data-method"),u=c.getAttribute("data-url"),r=P(c,c.getAttribute("data-params"))):(i=c.getAttribute("data-method"),u=t.href(c),r=c.getAttribute("data-params"));S({type:i||"GET",url:u,data:r,dataType:l,beforeSend:(t,e)=>L(c,"ajax:beforeSend",[t,e])?L(c,"ajax:send",[t]):(L(c,"ajax:stopped"),!1),success:(...t)=>L(c,"ajax:success",t),error:(...t)=>L(c,"ajax:error",t),complete:(...t)=>L(c,"ajax:complete",t),crossDomain:A(u),withCredentials:null!=s&&"false"!==s}),R(o)})(V);V.handleRemote=tt;if(V.start=function(){if(window._rails_loaded)throw new Error("rails-ujs has already been loaded!");return window.addEventListener("pageshow",(function(){y(i).forEach((function(t){b(t,"ujs:disabled")&&X(t)})),y(u).forEach((function(t){b(t,"ujs:disabled")&&X(t)}))})),q(document,u,"ajax:complete",X),q(document,u,"ajax:stopped",X),q(document,c,"ajax:complete",X),q(document,c,"ajax:stopped",X),q(document,t,"click",J),q(document,t,"click",N),q(document,t,"click",W),q(document,t,"click",$),q(document,t,"click",tt),q(document,t,"click",Z),q(document,e,"click",J),q(document,e,"click",N),q(document,e,"click",W),q(document,e,"click",$),q(document,e,"click",tt),q(document,n,"change",N),q(document,n,"change",W),q(document,n,"change",tt),q(document,a,"submit",N),q(document,a,"submit",W),q(document,a,"submit",tt),q(document,a,"submit",(t=>setTimeout((()=>$(t)),13))),q(document,a,"ajax:send",$),q(document,a,"ajax:complete",X),q(document,o,"click",J),q(document,o,"click",N),q(document,o,"click",W),q(document,o,"click",B),document.addEventListener("DOMContentLoaded",w),document.addEventListener("DOMContentLoaded",l),window._rails_loaded=!0},"undefined"!=typeof jQuery&&jQuery&&jQuery.ajax){if(jQuery.rails)throw new Error("If you load both jquery_ujs and rails-ujs, use rails-ujs only.");jQuery.rails=V,jQuery.ajaxPrefilter((function(t,e,n){if(!t.crossDomain)return g(n)}))}export{V as default};